1mod staging;
2mod sweep;
3
4#[cfg(test)]
5mod tests;
6
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::Instant;
11
12use tokio::sync::Mutex;
13
14use futures_util::{Stream, StreamExt};
15use sha2::{Digest, Sha256};
16use tokio::fs;
17use tokio::io::AsyncWriteExt;
18
19use crate::error::Error;
20use crate::namespace::Namespace;
21
22pub use staging::{Reclaimed, reclaim};
23pub use sweep::SweepReport;
24
25pub struct LocalStore {
26 root: PathBuf,
27 counter: AtomicU64,
28 usage: Mutex<Option<(Instant, u64, u64)>>,
29 per_namespace: Mutex<HashMap<String, (Instant, u64, u64)>>,
30 scans: AtomicU64,
31 max_object_size: Option<u64>,
32}
33
34impl LocalStore {
35 pub fn new(root: impl Into<PathBuf>) -> Self {
36 Self {
37 root: root.into(),
38 counter: AtomicU64::new(0),
39 usage: Mutex::new(None),
40 per_namespace: Mutex::new(HashMap::new()),
41 scans: AtomicU64::new(0),
42 max_object_size: None,
43 }
44 }
45
46 pub fn with_max_object_size(mut self, limit: Option<u64>) -> Self {
47 self.max_object_size = limit;
48 self
49 }
50
51 pub fn validate_oid(oid: &str) -> Result<(), Error> {
52 let well_formed = oid.len() == 64
53 && oid
54 .bytes()
55 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
56
57 well_formed.then_some(()).ok_or(Error::MalformedOid)
58 }
59
60 fn object_path(&self, ns: &Namespace, oid: &str) -> PathBuf {
61 self.root
62 .join(ns.org())
63 .join(ns.repo())
64 .join(&oid[0..2])
65 .join(&oid[2..4])
66 .join(oid)
67 }
68
69 fn content_path(&self, oid: &str) -> PathBuf {
70 self.root
71 .join(".content")
72 .join(&oid[0..2])
73 .join(&oid[2..4])
74 .join(oid)
75 }
76
77 pub fn scans(&self) -> u64 {
78 self.scans.load(Ordering::Relaxed)
79 }
80
81 pub async fn writable(&self) -> Result<(), Error> {
82 fs::create_dir_all(&self.root).await?;
83
84 let ticket = self.counter.fetch_add(1, Ordering::Relaxed);
85 let probe = self.root.join(format!(".readiness.{ticket}"));
86
87 fs::write(&probe, b"").await?;
88 fs::remove_file(&probe).await?;
89
90 Ok(())
91 }
92
93 pub async fn exists(&self, ns: &Namespace, oid: &str) -> bool {
94 Self::validate_oid(oid).is_ok() && fs::metadata(self.object_path(ns, oid)).await.is_ok()
95 }
96
97 pub async fn open(&self, ns: &Namespace, oid: &str) -> Result<(fs::File, u64), Error> {
98 Self::validate_oid(oid)?;
99 let path = self.object_path(ns, oid);
100 let file = fs::File::open(&path).await.map_err(|_| Error::NotFound)?;
101 let size = file.metadata().await?.len();
102 Ok((file, size))
103 }
104
105 pub async fn write<S, E>(
106 &self,
107 ns: &Namespace,
108 oid: &str,
109 expected_size: Option<u64>,
110 mut chunks: S,
111 ) -> Result<u64, Error>
112 where
113 S: Stream<Item = Result<axum::body::Bytes, E>> + Unpin,
114 E: std::error::Error + Send + Sync + 'static,
115 {
116 Self::validate_oid(oid)?;
117
118 if let Some(limit) = self.max_object_size
119 && expected_size.is_some_and(|declared| declared > limit)
120 {
121 return Err(Error::TooLarge { limit });
122 }
123
124 let path = self.object_path(ns, oid);
125 let parent = path.parent().expect("object paths always have a parent");
126 fs::create_dir_all(parent).await?;
127
128 let staged = self.staging_path(parent, oid);
129 let outcome = self.stream_to(&staged, &mut chunks).await;
130
131 match outcome {
132 Ok((digest, written)) => {
133 self.finish(&staged, &path, oid, expected_size, &digest, written)
134 .await?;
135 Ok(written)
136 }
137 Err(error) => {
138 let _ = fs::remove_file(&staged).await;
139 Err(error)
140 }
141 }
142 }
143
144 fn staging_path(&self, parent: &Path, oid: &str) -> PathBuf {
145 let ticket = self.counter.fetch_add(1, Ordering::Relaxed);
146 parent.join(format!("{oid}.{ticket}.part"))
147 }
148
149 async fn stream_to<S, E>(&self, staged: &Path, chunks: &mut S) -> Result<(String, u64), Error>
150 where
151 S: Stream<Item = Result<axum::body::Bytes, E>> + Unpin,
152 E: std::error::Error + Send + Sync + 'static,
153 {
154 let mut file = fs::File::create(staged).await?;
155 let mut hasher = Sha256::new();
156 let mut written = 0u64;
157
158 while let Some(chunk) = chunks.next().await {
159 let chunk = chunk.map_err(std::io::Error::other)?;
160 hasher.update(&chunk);
161 written += chunk.len() as u64;
162
163 if let Some(limit) = self.max_object_size.filter(|limit| written > *limit) {
168 return Err(Error::TooLarge { limit });
169 }
170
171 file.write_all(&chunk).await?;
172 }
173
174 file.flush().await?;
175 file.sync_all().await?;
176
177 Ok((hex::encode(hasher.finalize()), written))
178 }
179
180 async fn finish(
181 &self,
182 staged: &Path,
183 final_path: &Path,
184 oid: &str,
185 expected_size: Option<u64>,
186 digest: &str,
187 written: u64,
188 ) -> Result<(), Error> {
189 if let Some(declared) = expected_size.filter(|declared| *declared != written) {
190 let _ = fs::remove_file(staged).await;
191 return Err(Error::SizeMismatch {
192 declared,
193 actual: written,
194 });
195 }
196
197 if digest != oid {
198 let _ = fs::remove_file(staged).await;
199 return Err(Error::OidMismatch {
200 declared: oid.to_owned(),
201 actual: digest.to_owned(),
202 });
203 }
204
205 self.link_or_move(staged, final_path, oid).await
206 }
207
208 async fn link_or_move(&self, staged: &Path, final_path: &Path, oid: &str) -> Result<(), Error> {
215 let content = self.content_path(oid);
216 let parent = content.parent().expect("content paths have a parent");
217 fs::create_dir_all(parent).await?;
218
219 if fs::metadata(&content).await.is_err() {
220 fs::rename(staged, &content).await?;
221 }
222
223 match self.link(&content, final_path).await {
224 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
230 fs::rename(staged, &content).await?;
231 self.link(&content, final_path).await?;
232 }
233 outcome => outcome?,
234 }
235
236 let _ = fs::remove_file(staged).await;
237 Ok(())
238 }
239
240 async fn link(&self, content: &Path, final_path: &Path) -> Result<(), std::io::Error> {
241 let from = content.to_path_buf();
242 let to = final_path.to_path_buf();
243 let linked = tokio::task::spawn_blocking(move || std::fs::hard_link(&from, &to))
244 .await
245 .map_err(std::io::Error::other)?;
246
247 match linked {
248 Ok(()) => Ok(()),
249 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
250 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Err(error),
251 Err(_) => fs::copy(content, final_path).await.map(|_| ()),
255 }
256 }
257}