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 for found in walk.objects {
47 if retained.contains(&found.oid) {
48 continue;
49 }
50
51 let metadata = fs::metadata(&found.path).await?;
52 if age(&metadata) < grace {
53 report.within_grace += 1;
54 continue;
55 }
56
57 self.collect(&found.path, &found.oid, metadata.len(), ns, &mut report)
58 .await?;
59 }
60
61 if !dry_run {
62 self.forget(ns).await;
63 }
64
65 Ok(report)
66 }
67
68 async fn collect(
73 &self,
74 path: &Path,
75 oid: &str,
76 held: u64,
77 ns: &Namespace,
78 report: &mut SweepReport,
79 ) -> Result<(), Error> {
80 report.swept += 1;
81
82 if report.dry_run {
83 if !self.referenced_elsewhere(oid, ns).await {
84 report.bytes += held;
85 }
86
87 return Ok(());
88 }
89
90 fs::remove_file(path).await?;
91
92 if self.referenced_elsewhere(oid, ns).await {
93 return Ok(());
94 }
95
96 let content = self.content_path(oid);
97 let size = fs::metadata(&content)
98 .await
99 .map(|shared| shared.len())
100 .unwrap_or(held);
101
102 if fs::remove_file(&content).await.is_ok() {
107 report.bytes += size;
108 }
109
110 Ok(())
111 }
112
113 async fn referenced_elsewhere(&self, oid: &str, sweeping: &Namespace) -> bool {
117 let Ok(mut orgs) = fs::read_dir(&self.root).await else {
118 return false;
119 };
120
121 while let Ok(Some(org)) = orgs.next_entry().await {
122 let org_name = org.file_name().to_string_lossy().into_owned();
123 if org_name.starts_with('.') {
124 continue;
125 }
126
127 let Ok(mut repos) = fs::read_dir(org.path()).await else {
128 continue;
129 };
130
131 while let Ok(Some(repo)) = repos.next_entry().await {
132 let repo_name = repo.file_name().to_string_lossy().into_owned();
133 if org_name == sweeping.org() && repo_name == sweeping.repo() {
134 continue;
135 }
136
137 let candidate = repo.path().join(&oid[0..2]).join(&oid[2..4]).join(oid);
138 if fs::metadata(candidate).await.is_ok() {
139 return true;
140 }
141 }
142 }
143
144 false
145 }
146}
147
148pub(super) fn age(metadata: &std::fs::Metadata) -> Duration {
149 metadata
150 .modified()
151 .ok()
152 .and_then(|modified| SystemTime::now().duration_since(modified).ok())
153 .unwrap_or_default()
154}
155
156const USAGE_TTL: Duration = Duration::from_secs(60);
157
158impl LocalStore {
159 pub async fn usage(&self) -> (u64, u64) {
160 let mut cached = self.usage.lock().await;
161
162 if let Some((measured_at, objects, bytes)) = *cached
163 && measured_at.elapsed() < USAGE_TTL
164 {
165 return (objects, bytes);
166 }
167
168 let measured = self.measure().await;
169 *cached = Some((Instant::now(), measured.0, measured.1));
170
171 measured
172 }
173
174 pub async fn usage_of(&self, ns: &Namespace) -> (u64, u64) {
175 let key = ns.to_string();
176 let mut cached = self.per_namespace.lock().await;
177
178 if let Some((measured_at, objects, bytes)) = cached.get(&key)
179 && measured_at.elapsed() < USAGE_TTL
180 {
181 return (*objects, *bytes);
182 }
183
184 let measured = self.walk(self.root.join(ns.org()).join(ns.repo())).await;
185 cached.insert(key, (Instant::now(), measured.0, measured.1));
186
187 measured
188 }
189
190 pub async fn stored(&self, ns: &Namespace, bytes: u64) {
197 if let Some((_, objects, held)) = self.per_namespace.lock().await.get_mut(&ns.to_string()) {
198 *objects += 1;
199 *held += bytes;
200 }
201 }
202
203 pub async fn forget(&self, ns: &Namespace) {
207 self.per_namespace.lock().await.remove(&ns.to_string());
208 *self.usage.lock().await = None;
209 }
210
211 async fn measure(&self) -> (u64, u64) {
217 #[cfg(unix)]
218 {
219 self.walk_unique(self.root.clone()).await
220 }
221 #[cfg(not(unix))]
222 {
223 let (shared_objects, shared_bytes) = self.walk(self.root.join(".content")).await;
224 let (loose_objects, loose_bytes) = self.walk_unshared(self.root.clone()).await;
225
226 (shared_objects + loose_objects, shared_bytes + loose_bytes)
227 }
228 }
229
230 #[cfg(unix)]
234 async fn walk_unique(&self, from: PathBuf) -> (u64, u64) {
235 use std::collections::HashSet;
236 use std::os::unix::fs::MetadataExt;
237
238 self.scan(from, |metadata, seen: &mut HashSet<(u64, u64)>| {
239 seen.insert((metadata.dev(), metadata.ino()))
240 })
241 .await
242 }
243
244 #[cfg(not(unix))]
248 async fn walk_unshared(&self, from: PathBuf) -> (u64, u64) {
249 let mut objects = 0;
250 let mut bytes = 0;
251 let mut directories = vec![from];
252
253 while let Some(directory) = directories.pop() {
254 let Ok(mut entries) = fs::read_dir(&directory).await else {
255 continue;
256 };
257
258 while let Ok(Some(entry)) = entries.next_entry().await {
259 let name = entry.file_name().to_string_lossy().into_owned();
260 if name.starts_with('.') && directory == self.root {
264 continue;
265 }
266
267 match entry.metadata().await {
268 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
269 Ok(metadata)
270 if LocalStore::validate_oid(&name).is_ok()
271 && fs::metadata(self.content_path(&name)).await.is_err() =>
272 {
273 objects += 1;
274 bytes += metadata.len();
275 }
276 _ => {}
277 }
278 }
279 }
280
281 (objects, bytes)
282 }
283
284 async fn walk(&self, from: PathBuf) -> (u64, u64) {
285 self.scan(from, |_, _: &mut ()| true).await
286 }
287
288 async fn scan<S: Default>(
289 &self,
290 from: PathBuf,
291 mut counts: impl FnMut(&std::fs::Metadata, &mut S) -> bool,
292 ) -> (u64, u64) {
293 self.scans.fetch_add(1, Ordering::Relaxed);
294
295 let mut objects = 0;
296 let mut bytes = 0;
297 let mut state = S::default();
298
299 let mut directories = vec![from];
300 while let Some(directory) = directories.pop() {
301 let Ok(mut entries) = fs::read_dir(&directory).await else {
302 continue;
303 };
304
305 while let Ok(Some(entry)) = entries.next_entry().await {
306 let name = entry.file_name().to_string_lossy().into_owned();
307 if name.starts_with('.') {
308 continue;
309 }
310
311 match entry.metadata().await {
312 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
313 Ok(metadata)
314 if LocalStore::validate_oid(&name).is_ok()
315 && counts(&metadata, &mut state) =>
316 {
317 objects += 1;
318 bytes += metadata.len();
319 }
320 _ => {}
321 }
322 }
323 }
324
325 (objects, bytes)
326 }
327}