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