forge
ExploreLog in

Tahinli/forge

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