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 dry_run: bool,
19}
20
21impl LocalStore {
22 pub async fn sweep(
32 &self,
33 ns: &Namespace,
34 retained: &HashSet<String>,
35 grace: Duration,
36 dry_run: bool,
37 ) -> Result<SweepReport, Error> {
38 let mut report = SweepReport {
39 dry_run,
40 ..SweepReport::default()
41 };
42
43 let Ok(mut prefixes) = fs::read_dir(self.root.join(ns.org()).join(ns.repo())).await else {
44 return Ok(report);
45 };
46
47 while let Some(prefix) = prefixes.next_entry().await? {
48 let Ok(mut fanouts) = fs::read_dir(prefix.path()).await else {
49 continue;
50 };
51
52 while let Some(fanout) = fanouts.next_entry().await? {
53 self.sweep_directory(&fanout.path(), ns, retained, grace, &mut report)
54 .await?;
55 }
56 }
57
58 if !dry_run {
59 self.forget(ns).await;
60 }
61
62 Ok(report)
63 }
64
65 async fn referenced_elsewhere(&self, oid: &str, sweeping: &Namespace) -> bool {
69 let Ok(mut orgs) = fs::read_dir(&self.root).await else {
70 return false;
71 };
72
73 while let Ok(Some(org)) = orgs.next_entry().await {
74 let org_name = org.file_name().to_string_lossy().into_owned();
75 if org_name.starts_with('.') {
76 continue;
77 }
78
79 let Ok(mut repos) = fs::read_dir(org.path()).await else {
80 continue;
81 };
82
83 while let Ok(Some(repo)) = repos.next_entry().await {
84 let repo_name = repo.file_name().to_string_lossy().into_owned();
85 if org_name == sweeping.org() && repo_name == sweeping.repo() {
86 continue;
87 }
88
89 let candidate = repo.path().join(&oid[0..2]).join(&oid[2..4]).join(oid);
90 if fs::metadata(candidate).await.is_ok() {
91 return true;
92 }
93 }
94 }
95
96 false
97 }
98
99 async fn sweep_directory(
100 &self,
101 directory: &Path,
102 ns: &Namespace,
103 retained: &HashSet<String>,
104 grace: Duration,
105 report: &mut SweepReport,
106 ) -> Result<(), Error> {
107 let Ok(mut entries) = fs::read_dir(directory).await else {
108 return Ok(());
109 };
110
111 while let Some(entry) = entries.next_entry().await? {
112 let name = entry.file_name().to_string_lossy().into_owned();
113 if Self::validate_oid(&name).is_err() || retained.contains(&name) {
114 continue;
115 }
116
117 let metadata = entry.metadata().await?;
118 if age(&metadata) < grace {
119 report.within_grace += 1;
120 continue;
121 }
122
123 report.swept += 1;
124
125 if report.dry_run {
126 if !self.referenced_elsewhere(&name, ns).await {
129 report.bytes += metadata.len();
130 }
131 continue;
132 }
133
134 fs::remove_file(entry.path()).await?;
135
136 if !self.referenced_elsewhere(&name, ns).await {
140 let content = self.content_path(&name);
141 let size = fs::metadata(&content)
142 .await
143 .map(|content_metadata| content_metadata.len())
144 .unwrap_or_else(|_| metadata.len());
145
146 if fs::remove_file(&content).await.is_ok() {
151 report.bytes += size;
152 }
153 }
154 }
155
156 Ok(())
157 }
158}
159
160pub(super) fn age(metadata: &std::fs::Metadata) -> Duration {
161 metadata
162 .modified()
163 .ok()
164 .and_then(|modified| SystemTime::now().duration_since(modified).ok())
165 .unwrap_or_default()
166}
167
168const USAGE_TTL: Duration = Duration::from_secs(60);
169
170impl LocalStore {
171 pub async fn usage(&self) -> (u64, u64) {
172 let mut cached = self.usage.lock().await;
173
174 if let Some((measured_at, objects, bytes)) = *cached
175 && measured_at.elapsed() < USAGE_TTL
176 {
177 return (objects, bytes);
178 }
179
180 let measured = self.measure().await;
181 *cached = Some((Instant::now(), measured.0, measured.1));
182
183 measured
184 }
185
186 pub async fn usage_of(&self, ns: &Namespace) -> (u64, u64) {
187 let key = ns.to_string();
188 let mut cached = self.per_namespace.lock().await;
189
190 if let Some((measured_at, objects, bytes)) = cached.get(&key)
191 && measured_at.elapsed() < USAGE_TTL
192 {
193 return (*objects, *bytes);
194 }
195
196 let measured = self.walk(self.root.join(ns.org()).join(ns.repo())).await;
197 cached.insert(key, (Instant::now(), measured.0, measured.1));
198
199 measured
200 }
201
202 pub async fn stored(&self, ns: &Namespace, bytes: u64) {
209 if let Some((_, objects, held)) = self.per_namespace.lock().await.get_mut(&ns.to_string()) {
210 *objects += 1;
211 *held += bytes;
212 }
213 }
214
215 pub async fn forget(&self, ns: &Namespace) {
219 self.per_namespace.lock().await.remove(&ns.to_string());
220 *self.usage.lock().await = None;
221 }
222
223 async fn measure(&self) -> (u64, u64) {
229 #[cfg(unix)]
230 {
231 self.walk_unique(self.root.clone()).await
232 }
233 #[cfg(not(unix))]
234 {
235 let (shared_objects, shared_bytes) = self.walk(self.root.join(".content")).await;
236 let (loose_objects, loose_bytes) = self.walk_unshared(self.root.clone()).await;
237
238 (shared_objects + loose_objects, shared_bytes + loose_bytes)
239 }
240 }
241
242 #[cfg(unix)]
246 async fn walk_unique(&self, from: PathBuf) -> (u64, u64) {
247 use std::collections::HashSet;
248 use std::os::unix::fs::MetadataExt;
249
250 self.scan(from, |metadata, seen: &mut HashSet<(u64, u64)>| {
251 seen.insert((metadata.dev(), metadata.ino()))
252 })
253 .await
254 }
255
256 #[cfg(not(unix))]
260 async fn walk_unshared(&self, from: PathBuf) -> (u64, u64) {
261 let mut objects = 0;
262 let mut bytes = 0;
263 let mut directories = vec![from];
264
265 while let Some(directory) = directories.pop() {
266 let Ok(mut entries) = fs::read_dir(&directory).await else {
267 continue;
268 };
269
270 while let Ok(Some(entry)) = entries.next_entry().await {
271 let name = entry.file_name().to_string_lossy().into_owned();
272 if name.starts_with('.') && directory == self.root {
276 continue;
277 }
278
279 match entry.metadata().await {
280 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
281 Ok(metadata)
282 if LocalStore::validate_oid(&name).is_ok()
283 && fs::metadata(self.content_path(&name)).await.is_err() =>
284 {
285 objects += 1;
286 bytes += metadata.len();
287 }
288 _ => {}
289 }
290 }
291 }
292
293 (objects, bytes)
294 }
295
296 async fn walk(&self, from: PathBuf) -> (u64, u64) {
297 self.scan(from, |_, _: &mut ()| true).await
298 }
299
300 async fn scan<S: Default>(
301 &self,
302 from: PathBuf,
303 mut counts: impl FnMut(&std::fs::Metadata, &mut S) -> bool,
304 ) -> (u64, u64) {
305 self.scans.fetch_add(1, Ordering::Relaxed);
306
307 let mut objects = 0;
308 let mut bytes = 0;
309 let mut state = S::default();
310
311 let mut directories = vec![from];
312 while let Some(directory) = directories.pop() {
313 let Ok(mut entries) = fs::read_dir(&directory).await else {
314 continue;
315 };
316
317 while let Ok(Some(entry)) = entries.next_entry().await {
318 let name = entry.file_name().to_string_lossy().into_owned();
319 if name.starts_with('.') {
320 continue;
321 }
322
323 match entry.metadata().await {
324 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
325 Ok(metadata)
326 if LocalStore::validate_oid(&name).is_ok()
327 && counts(&metadata, &mut state) =>
328 {
329 objects += 1;
330 bytes += metadata.len();
331 }
332 _ => {}
333 }
334 }
335 }
336
337 (objects, bytes)
338 }
339}