lfsx_server/storage/
sweep.rs1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::sync::atomic::Ordering;
4use std::time::{Duration, Instant, SystemTime};
5
6use serde::Serialize;
7use tokio::fs;
8
9use super::LocalStore;
10use crate::error::Error;
11use crate::namespace::Namespace;
12
13#[derive(Debug, Default, Serialize)]
14pub struct SweepReport {
15 pub swept: usize,
16 pub bytes: u64,
17 pub within_grace: usize,
18 pub incomplete: bool,
19 pub dry_run: bool,
20}
21
22impl LocalStore {
23 pub async fn sweep(
33 &self,
34 ns: &Namespace,
35 retained: &HashSet<String>,
36 grace: Duration,
37 dry_run: bool,
38 ) -> Result<SweepReport, Error> {
39 let walk = self.objects_of(ns).await;
40 let mut report = SweepReport {
41 dry_run,
42 incomplete: !walk.complete,
43 ..SweepReport::default()
44 };
45
46 let elsewhere = self.other_repositories(ns).await;
51
52 for found in walk.objects {
53 if retained.contains(&found.oid) {
54 continue;
55 }
56
57 let metadata = fs::metadata(&found.path).await?;
58 if age(&metadata) < grace {
59 report.within_grace += 1;
60 continue;
61 }
62
63 self.collect(
64 &found.path,
65 &found.oid,
66 metadata.len(),
67 &elsewhere,
68 &mut report,
69 )
70 .await?;
71 }
72
73 if !dry_run {
74 self.forget(ns).await;
75 }
76
77 Ok(report)
78 }
79
80 async fn collect(
85 &self,
86 path: &Path,
87 oid: &str,
88 held: u64,
89 elsewhere: &[PathBuf],
90 report: &mut SweepReport,
91 ) -> Result<(), Error> {
92 report.swept += 1;
93
94 if report.dry_run {
95 if !self.referenced_elsewhere(oid, elsewhere, 1).await {
98 report.bytes += held;
99 }
100
101 return Ok(());
102 }
103
104 fs::remove_file(path).await?;
105
106 if self.referenced_elsewhere(oid, elsewhere, 0).await {
107 return Ok(());
108 }
109
110 let content = self.content_path(oid);
111 let size = fs::metadata(&content)
112 .await
113 .map(|shared| shared.len())
114 .unwrap_or(held);
115
116 if fs::remove_file(&content).await.is_ok() {
121 report.bytes += size;
122 }
123
124 Ok(())
125 }
126
127 async fn referenced_elsewhere(&self, oid: &str, elsewhere: &[PathBuf], ours: u64) -> bool {
135 if let Some(links) = links_to(&self.content_path(oid)).await {
136 return links > 1 + ours;
137 }
138
139 for repo in elsewhere {
143 let candidate = repo.join(&oid[0..2]).join(&oid[2..4]).join(oid);
144 if fs::metadata(candidate).await.is_ok() {
145 return true;
146 }
147 }
148
149 false
150 }
151
152 async fn other_repositories(&self, sweeping: &Namespace) -> Vec<PathBuf> {
154 let mut out = Vec::new();
155 let Ok(mut orgs) = fs::read_dir(&self.root).await else {
156 return out;
157 };
158
159 while let Ok(Some(org)) = orgs.next_entry().await {
160 let org_name = org.file_name().to_string_lossy().into_owned();
161 if org_name.starts_with('.') {
162 continue;
163 }
164
165 let Ok(mut repos) = fs::read_dir(org.path()).await else {
166 continue;
167 };
168
169 while let Ok(Some(repo)) = repos.next_entry().await {
170 let repo_name = repo.file_name().to_string_lossy().into_owned();
171 if org_name == sweeping.org() && repo_name == sweeping.repo() {
172 continue;
173 }
174
175 out.push(repo.path());
176 }
177 }
178
179 out
180 }
181}
182
183#[cfg(unix)]
186async fn links_to(content: &Path) -> Option<u64> {
187 use std::os::unix::fs::MetadataExt;
188
189 fs::metadata(content)
190 .await
191 .ok()
192 .map(|shared| shared.nlink())
193}
194
195#[cfg(not(unix))]
196async fn links_to(_content: &Path) -> Option<u64> {
197 None
198}
199
200pub(super) fn age(metadata: &std::fs::Metadata) -> Duration {
201 metadata
202 .modified()
203 .ok()
204 .and_then(|modified| SystemTime::now().duration_since(modified).ok())
205 .unwrap_or_default()
206}
207
208const USAGE_TTL: Duration = Duration::from_secs(60);
209
210impl LocalStore {
211 pub async fn usage(&self) -> (u64, u64) {
212 let mut cached = self.usage.lock().await;
213
214 if let Some((measured_at, objects, bytes)) = *cached
215 && measured_at.elapsed() < USAGE_TTL
216 {
217 return (objects, bytes);
218 }
219
220 let measured = self.measure().await;
221 *cached = Some((Instant::now(), measured.0, measured.1));
222
223 measured
224 }
225
226 pub async fn usage_of(&self, ns: &Namespace) -> (u64, u64) {
227 let key = ns.to_string();
228 let mut cached = self.per_namespace.lock().await;
229
230 if let Some((measured_at, objects, bytes)) = cached.get(&key)
231 && measured_at.elapsed() < USAGE_TTL
232 {
233 return (*objects, *bytes);
234 }
235
236 let measured = self.walk(self.root.join(ns.org()).join(ns.repo())).await;
237 cached.insert(key, (Instant::now(), measured.0, measured.1));
238
239 measured
240 }
241
242 pub async fn stored(&self, ns: &Namespace, bytes: u64) {
249 if let Some((_, objects, held)) = self.per_namespace.lock().await.get_mut(&ns.to_string()) {
250 *objects += 1;
251 *held += bytes;
252 }
253 }
254
255 pub async fn forget(&self, ns: &Namespace) {
259 self.per_namespace.lock().await.remove(&ns.to_string());
260 *self.usage.lock().await = None;
261 }
262
263 async fn measure(&self) -> (u64, u64) {
269 #[cfg(unix)]
270 {
271 self.walk_unique(self.root.clone()).await
272 }
273 #[cfg(not(unix))]
274 {
275 let (shared_objects, shared_bytes) = self.walk(self.root.join(".content")).await;
276 let (loose_objects, loose_bytes) = self.walk_unshared(self.root.clone()).await;
277
278 (shared_objects + loose_objects, shared_bytes + loose_bytes)
279 }
280 }
281
282 #[cfg(unix)]
286 async fn walk_unique(&self, from: PathBuf) -> (u64, u64) {
287 use std::collections::HashSet;
288 use std::os::unix::fs::MetadataExt;
289
290 self.scan(from, |metadata, seen: &mut HashSet<(u64, u64)>| {
291 seen.insert((metadata.dev(), metadata.ino()))
292 })
293 .await
294 }
295
296 #[cfg(not(unix))]
300 async fn walk_unshared(&self, from: PathBuf) -> (u64, u64) {
301 let mut objects = 0;
302 let mut bytes = 0;
303 let mut directories = vec![from];
304
305 while let Some(directory) = directories.pop() {
306 let Ok(mut entries) = fs::read_dir(&directory).await else {
307 continue;
308 };
309
310 while let Ok(Some(entry)) = entries.next_entry().await {
311 let name = entry.file_name().to_string_lossy().into_owned();
312 if name.starts_with('.') && directory == self.root {
316 continue;
317 }
318
319 match entry.metadata().await {
320 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
321 Ok(metadata)
322 if LocalStore::validate_oid(&name).is_ok()
323 && fs::metadata(self.content_path(&name)).await.is_err() =>
324 {
325 objects += 1;
326 bytes += metadata.len();
327 }
328 _ => {}
329 }
330 }
331 }
332
333 (objects, bytes)
334 }
335
336 async fn walk(&self, from: PathBuf) -> (u64, u64) {
337 self.scan(from, |_, _: &mut ()| true).await
338 }
339
340 async fn scan<S: Default>(
341 &self,
342 from: PathBuf,
343 mut counts: impl FnMut(&std::fs::Metadata, &mut S) -> bool,
344 ) -> (u64, u64) {
345 self.scans.fetch_add(1, Ordering::Relaxed);
346
347 let mut objects = 0;
348 let mut bytes = 0;
349 let mut state = S::default();
350
351 let mut directories = vec![from];
352 while let Some(directory) = directories.pop() {
353 let Ok(mut entries) = fs::read_dir(&directory).await else {
354 continue;
355 };
356
357 while let Ok(Some(entry)) = entries.next_entry().await {
358 let name = entry.file_name().to_string_lossy().into_owned();
359 if name.starts_with('.') {
360 continue;
361 }
362
363 match entry.metadata().await {
364 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
365 Ok(metadata)
366 if LocalStore::validate_oid(&name).is_ok()
367 && counts(&metadata, &mut state) =>
368 {
369 objects += 1;
370 bytes += metadata.len();
371 }
372 _ => {}
373 }
374 }
375 }
376
377 (objects, bytes)
378 }
379}