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_capacity().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(super) async fn measure_of(&self, ns: &Namespace) -> (u64, u64) {
230 self.walk(self.root.join(ns.org()).join(ns.repo())).await
231 }
232
233 pub(super) async fn forget_capacity(&self) {
238 *self.usage.lock().await = None;
239 }
240
241 async fn measure(&self) -> (u64, u64) {
247 #[cfg(unix)]
248 {
249 self.walk_unique(self.root.clone()).await
250 }
251 #[cfg(not(unix))]
252 {
253 let (shared_objects, shared_bytes) = self.walk(self.root.join(".content")).await;
254 let (loose_objects, loose_bytes) = self.walk_unshared(self.root.clone()).await;
255
256 (shared_objects + loose_objects, shared_bytes + loose_bytes)
257 }
258 }
259
260 #[cfg(unix)]
264 async fn walk_unique(&self, from: PathBuf) -> (u64, u64) {
265 use std::collections::HashSet;
266 use std::os::unix::fs::MetadataExt;
267
268 self.scan(from, |metadata, seen: &mut HashSet<(u64, u64)>| {
269 seen.insert((metadata.dev(), metadata.ino()))
270 })
271 .await
272 }
273
274 #[cfg(not(unix))]
278 async fn walk_unshared(&self, from: PathBuf) -> (u64, u64) {
279 let mut objects = 0;
280 let mut bytes = 0;
281 let mut directories = vec![from];
282
283 while let Some(directory) = directories.pop() {
284 let Ok(mut entries) = fs::read_dir(&directory).await else {
285 continue;
286 };
287
288 while let Ok(Some(entry)) = entries.next_entry().await {
289 let name = entry.file_name().to_string_lossy().into_owned();
290 if name.starts_with('.') && directory == self.root {
294 continue;
295 }
296
297 match entry.metadata().await {
298 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
299 Ok(metadata)
300 if LocalStore::validate_oid(&name).is_ok()
301 && fs::metadata(self.content_path(&name)).await.is_err() =>
302 {
303 objects += 1;
304 bytes += metadata.len();
305 }
306 _ => {}
307 }
308 }
309 }
310
311 (objects, bytes)
312 }
313
314 async fn walk(&self, from: PathBuf) -> (u64, u64) {
315 self.scan(from, |_, _: &mut ()| true).await
316 }
317
318 async fn scan<S: Default>(
319 &self,
320 from: PathBuf,
321 mut counts: impl FnMut(&std::fs::Metadata, &mut S) -> bool,
322 ) -> (u64, u64) {
323 self.scans.fetch_add(1, Ordering::Relaxed);
324
325 let mut objects = 0;
326 let mut bytes = 0;
327 let mut state = S::default();
328
329 let mut directories = vec![from];
330 while let Some(directory) = directories.pop() {
331 let Ok(mut entries) = fs::read_dir(&directory).await else {
332 continue;
333 };
334
335 while let Ok(Some(entry)) = entries.next_entry().await {
336 let name = entry.file_name().to_string_lossy().into_owned();
337 if name.starts_with('.') {
338 continue;
339 }
340
341 match entry.metadata().await {
342 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
343 Ok(metadata)
344 if LocalStore::validate_oid(&name).is_ok()
345 && counts(&metadata, &mut state) =>
346 {
347 objects += 1;
348 bytes += metadata.len();
349 }
350 _ => {}
351 }
352 }
353 }
354
355 (objects, bytes)
356 }
357}