forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
2.7 KiB
1//! Git LFS over HTTP: the batch API, object GET and PUT, and verify.
2//!
3//! Objects are content-addressed under `<data_dir>/lfs/<oid[0..2]>/<oid[2..4]>/<oid>` and shared by
4//! every repo. The `lfs_objects` table says which repo may use which object. Uploads stream into
5//! `<data_dir>/lfs/tmp` first and are renamed into place once their SHA-256 matches. Locks are not
6//! implemented, so the lock paths answer 404. `admin lfs-gc` lives in [`gc`].
7
8pub mod gc;
9#[cfg(test)]
10mod route_tests;
11mod routes;
12
13use std::path::{Path, PathBuf};
14
15use forge_core::Result;
16
17use crate::state::AppState;
18
19/// True for a 64-character lowercase hex SHA-256.
20pub fn is_valid_oid(oid: &str) -> bool {
21 oid.len() == 64
22 && oid
23 .bytes()
24 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
25}
26
27/// Root of the object store: `<data_dir>/lfs`.
28pub fn lfs_root(data_dir: &Path) -> PathBuf {
29 data_dir.join("lfs")
30}
31
32/// Where in-flight uploads are written before they are renamed into place.
33pub fn tmp_dir(data_dir: &Path) -> PathBuf {
34 lfs_root(data_dir).join("tmp")
35}
36
37/// The on-disk path of object `oid`. Callers pass a validated oid (see [`is_valid_oid`]); for any
38/// other input the path is still inside the store, just not in a fan-out directory.
39pub fn object_path(data_dir: &Path, oid: &str) -> PathBuf {
40 match (oid.get(0..2), oid.get(2..4)) {
41 (Some(first), Some(second)) => lfs_root(data_dir).join(first).join(second).join(oid),
42 _ => lfs_root(data_dir).join(oid),
43 }
44}
45
46/// The size of object `oid` as linked to `repo_id`, or `None` when that repo does not link it.
47/// The UI uses this for pointer display. It reads the link only, not the file.
48pub async fn has_object(state: &AppState, repo_id: &str, oid: &str) -> Result<Option<u64>> {
49 state.store.lfs_object_size(repo_id, oid).await
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn oid_must_be_64_lowercase_hex() {
58 let good = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
59 assert!(is_valid_oid(good));
60 assert!(!is_valid_oid(&good[..63]));
61 assert!(!is_valid_oid(&format!("{good}0")));
62 assert!(!is_valid_oid(&good.to_uppercase()));
63 assert!(!is_valid_oid(&format!("{}g", &good[..63])));
64 assert!(!is_valid_oid(""));
65 }
66
67 #[test]
68 fn object_path_fans_out_by_prefix() {
69 let oid = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
70 let path = object_path(Path::new("/data"), oid);
71 assert_eq!(
72 path,
73 Path::new("/data/lfs/4d/7a").join(oid),
74 "two-level fan-out under lfs/"
75 );
76 assert_eq!(
77 object_path(Path::new("/data"), "x"),
78 Path::new("/data/lfs/x")
79 );
80 }
81}