//! Git LFS over HTTP: the batch API, object GET and PUT, and verify. See //! `docs/wave-b-interfaces.md`, section 2. //! //! Objects are content-addressed under `/lfs///` and shared by //! every repo. The `lfs_objects` table says which repo may use which object. Uploads stream into //! `/lfs/tmp` first and are renamed into place once their SHA-256 matches. Locks are not //! implemented, so the lock paths answer 404. `admin lfs-gc` lives in [`gc`]. pub mod gc; #[cfg(test)] mod route_tests; mod routes; use std::path::{Path, PathBuf}; use forge_core::Result; use crate::state::AppState; /// True for a 64-character lowercase hex SHA-256. pub fn is_valid_oid(oid: &str) -> bool { oid.len() == 64 && oid .bytes() .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) } /// Root of the object store: `/lfs`. pub fn lfs_root(data_dir: &Path) -> PathBuf { data_dir.join("lfs") } /// Where in-flight uploads are written before they are renamed into place. pub fn tmp_dir(data_dir: &Path) -> PathBuf { lfs_root(data_dir).join("tmp") } /// The on-disk path of object `oid`. Callers pass a validated oid (see [`is_valid_oid`]); for any /// other input the path is still inside the store, just not in a fan-out directory. pub fn object_path(data_dir: &Path, oid: &str) -> PathBuf { match (oid.get(0..2), oid.get(2..4)) { (Some(first), Some(second)) => lfs_root(data_dir).join(first).join(second).join(oid), _ => lfs_root(data_dir).join(oid), } } /// The size of object `oid` as linked to `repo_id`, or `None` when that repo does not link it. /// The UI uses this for pointer display. It reads the link only, not the file. pub async fn has_object(state: &AppState, repo_id: &str, oid: &str) -> Result> { state.store.lfs_object_size(repo_id, oid).await } #[cfg(test)] mod tests { use super::*; #[test] fn oid_must_be_64_lowercase_hex() { let good = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393"; assert!(is_valid_oid(good)); assert!(!is_valid_oid(&good[..63])); assert!(!is_valid_oid(&format!("{good}0"))); assert!(!is_valid_oid(&good.to_uppercase())); assert!(!is_valid_oid(&format!("{}g", &good[..63]))); assert!(!is_valid_oid("")); } #[test] fn object_path_fans_out_by_prefix() { let oid = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393"; let path = object_path(Path::new("/data"), oid); assert_eq!( path, Path::new("/data/lfs/4d/7a").join(oid), "two-level fan-out under lfs/" ); assert_eq!( object_path(Path::new("/data"), "x"), Path::new("/data/lfs/x") ); } }