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) {
216 self.per_namespace.lock().await.remove(&ns.to_string());
217 }
218
219 async fn measure(&self) -> (u64, u64) {
225 #[cfg(unix)]
226 {
227 self.walk_unique(self.root.clone()).await
228 }
229 #[cfg(not(unix))]
230 {
231 let (shared_objects, shared_bytes) = self.walk(self.root.join(".content")).await;
232 let (loose_objects, loose_bytes) = self.walk_unshared(self.root.clone()).await;
233
234 (shared_objects + loose_objects, shared_bytes + loose_bytes)
235 }
236 }
237
238 #[cfg(unix)]
242 async fn walk_unique(&self, from: PathBuf) -> (u64, u64) {
243 use std::collections::HashSet;
244 use std::os::unix::fs::MetadataExt;
245
246 self.scan(from, |metadata, seen: &mut HashSet<(u64, u64)>| {
247 seen.insert((metadata.dev(), metadata.ino()))
248 })
249 .await
250 }
251
252 #[cfg(not(unix))]
256 async fn walk_unshared(&self, from: PathBuf) -> (u64, u64) {
257 let mut objects = 0;
258 let mut bytes = 0;
259 let mut directories = vec![from];
260
261 while let Some(directory) = directories.pop() {
262 let Ok(mut entries) = fs::read_dir(&directory).await else {
263 continue;
264 };
265
266 while let Ok(Some(entry)) = entries.next_entry().await {
267 let name = entry.file_name().to_string_lossy().into_owned();
268 if name.starts_with('.') && directory == self.root {
272 continue;
273 }
274
275 match entry.metadata().await {
276 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
277 Ok(metadata)
278 if LocalStore::validate_oid(&name).is_ok()
279 && fs::metadata(self.content_path(&name)).await.is_err() =>
280 {
281 objects += 1;
282 bytes += metadata.len();
283 }
284 _ => {}
285 }
286 }
287 }
288
289 (objects, bytes)
290 }
291
292 async fn walk(&self, from: PathBuf) -> (u64, u64) {
293 self.scan(from, |_, _: &mut ()| true).await
294 }
295
296 async fn scan<S: Default>(
297 &self,
298 from: PathBuf,
299 mut counts: impl FnMut(&std::fs::Metadata, &mut S) -> bool,
300 ) -> (u64, u64) {
301 self.scans.fetch_add(1, Ordering::Relaxed);
302
303 let mut objects = 0;
304 let mut bytes = 0;
305 let mut state = S::default();
306
307 let mut directories = vec![from];
308 while let Some(directory) = directories.pop() {
309 let Ok(mut entries) = fs::read_dir(&directory).await else {
310 continue;
311 };
312
313 while let Ok(Some(entry)) = entries.next_entry().await {
314 let name = entry.file_name().to_string_lossy().into_owned();
315 if name.starts_with('.') {
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 && counts(&metadata, &mut state) =>
324 {
325 objects += 1;
326 bytes += metadata.len();
327 }
328 _ => {}
329 }
330 }
331 }
332
333 (objects, bytes)
334 }
335}