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(
23 &self,
24 ns: &Namespace,
25 retained: &HashSet<String>,
26 grace: Duration,
27 dry_run: bool,
28 ) -> Result<SweepReport, Error> {
29 let mut report = SweepReport {
30 dry_run,
31 ..SweepReport::default()
32 };
33
34 let Ok(mut prefixes) = fs::read_dir(self.root.join(ns.org()).join(ns.repo())).await else {
35 return Ok(report);
36 };
37
38 while let Some(prefix) = prefixes.next_entry().await? {
39 let Ok(mut fanouts) = fs::read_dir(prefix.path()).await else {
40 continue;
41 };
42
43 while let Some(fanout) = fanouts.next_entry().await? {
44 self.sweep_directory(&fanout.path(), ns, retained, grace, &mut report)
45 .await?;
46 }
47
48 if !dry_run {
49 let _ = fs::remove_dir(prefix.path()).await;
50 }
51 }
52
53 Ok(report)
54 }
55
56 async fn referenced_elsewhere(&self, oid: &str, sweeping: &Namespace) -> bool {
60 let Ok(mut orgs) = fs::read_dir(&self.root).await else {
61 return false;
62 };
63
64 while let Ok(Some(org)) = orgs.next_entry().await {
65 let org_name = org.file_name().to_string_lossy().into_owned();
66 if org_name.starts_with('.') {
67 continue;
68 }
69
70 let Ok(mut repos) = fs::read_dir(org.path()).await else {
71 continue;
72 };
73
74 while let Ok(Some(repo)) = repos.next_entry().await {
75 let repo_name = repo.file_name().to_string_lossy().into_owned();
76 if org_name == sweeping.org() && repo_name == sweeping.repo() {
77 continue;
78 }
79
80 let candidate = repo.path().join(&oid[0..2]).join(&oid[2..4]).join(oid);
81 if fs::metadata(candidate).await.is_ok() {
82 return true;
83 }
84 }
85 }
86
87 false
88 }
89
90 async fn sweep_directory(
91 &self,
92 directory: &Path,
93 ns: &Namespace,
94 retained: &HashSet<String>,
95 grace: Duration,
96 report: &mut SweepReport,
97 ) -> Result<(), Error> {
98 let Ok(mut entries) = fs::read_dir(directory).await else {
99 return Ok(());
100 };
101
102 while let Some(entry) = entries.next_entry().await? {
103 let name = entry.file_name().to_string_lossy().into_owned();
104 if Self::validate_oid(&name).is_err() || retained.contains(&name) {
105 continue;
106 }
107
108 let metadata = entry.metadata().await?;
109 if age(&metadata) < grace {
110 report.within_grace += 1;
111 continue;
112 }
113
114 report.swept += 1;
115
116 if report.dry_run {
117 if !self.referenced_elsewhere(&name, ns).await {
120 report.bytes += metadata.len();
121 }
122 continue;
123 }
124
125 fs::remove_file(entry.path()).await?;
126
127 if !self.referenced_elsewhere(&name, ns).await {
131 let content = self.content_path(&name);
132 let size = fs::metadata(&content)
133 .await
134 .map(|content_metadata| content_metadata.len())
135 .unwrap_or_else(|_| metadata.len());
136
137 if fs::remove_file(&content).await.is_ok() {
142 report.bytes += size;
143 }
144 }
145 }
146
147 if !report.dry_run {
148 let _ = fs::remove_dir(directory).await;
149 }
150
151 Ok(())
152 }
153}
154
155fn age(metadata: &std::fs::Metadata) -> Duration {
156 metadata
157 .modified()
158 .ok()
159 .and_then(|modified| SystemTime::now().duration_since(modified).ok())
160 .unwrap_or_default()
161}
162
163const USAGE_TTL: Duration = Duration::from_secs(60);
164
165impl LocalStore {
166 pub async fn usage(&self) -> (u64, u64) {
167 let mut cached = self.usage.lock().await;
168
169 if let Some((measured_at, objects, bytes)) = *cached
170 && measured_at.elapsed() < USAGE_TTL
171 {
172 return (objects, bytes);
173 }
174
175 let measured = self.measure().await;
176 *cached = Some((Instant::now(), measured.0, measured.1));
177
178 measured
179 }
180
181 pub async fn usage_of(&self, ns: &Namespace) -> (u64, u64) {
182 let key = ns.to_string();
183 let mut cached = self.per_namespace.lock().await;
184
185 if let Some((measured_at, objects, bytes)) = cached.get(&key)
186 && measured_at.elapsed() < USAGE_TTL
187 {
188 return (*objects, *bytes);
189 }
190
191 let measured = self.walk(self.root.join(ns.org()).join(ns.repo())).await;
192 cached.insert(key, (Instant::now(), measured.0, measured.1));
193
194 measured
195 }
196
197 async fn measure(&self) -> (u64, u64) {
203 #[cfg(unix)]
204 {
205 self.walk_unique(self.root.clone()).await
206 }
207 #[cfg(not(unix))]
208 {
209 let (shared_objects, shared_bytes) = self.walk(self.root.join(".content")).await;
210 let (loose_objects, loose_bytes) = self.walk_unshared(self.root.clone()).await;
211
212 (shared_objects + loose_objects, shared_bytes + loose_bytes)
213 }
214 }
215
216 #[cfg(unix)]
220 async fn walk_unique(&self, from: PathBuf) -> (u64, u64) {
221 use std::collections::HashSet;
222 use std::os::unix::fs::MetadataExt;
223
224 self.scan(from, |metadata, seen: &mut HashSet<(u64, u64)>| {
225 seen.insert((metadata.dev(), metadata.ino()))
226 })
227 .await
228 }
229
230 #[cfg(not(unix))]
234 async fn walk_unshared(&self, from: PathBuf) -> (u64, u64) {
235 let mut objects = 0;
236 let mut bytes = 0;
237 let mut directories = vec![from];
238
239 while let Some(directory) = directories.pop() {
240 let Ok(mut entries) = fs::read_dir(&directory).await else {
241 continue;
242 };
243
244 while let Ok(Some(entry)) = entries.next_entry().await {
245 let name = entry.file_name().to_string_lossy().into_owned();
246 if name.starts_with('.') && directory == self.root {
250 continue;
251 }
252
253 match entry.metadata().await {
254 Ok(metadata) if metadata.is_dir() => directories.push(entry.path()),
255 Ok(metadata)
256 if LocalStore::validate_oid(&name).is_ok()
257 && fs::metadata(self.content_path(&name)).await.is_err() =>
258 {
259 objects += 1;
260 bytes += metadata.len();
261 }
262 _ => {}
263 }
264 }
265 }
266
267 (objects, bytes)
268 }
269
270 async fn walk(&self, from: PathBuf) -> (u64, u64) {
271 self.scan(from, |_, _: &mut ()| true).await
272 }
273
274 async fn scan<S: Default>(
275 &self,
276 from: PathBuf,
277 mut counts: impl FnMut(&std::fs::Metadata, &mut S) -> bool,
278 ) -> (u64, u64) {
279 self.scans.fetch_add(1, Ordering::Relaxed);
280
281 let mut objects = 0;
282 let mut bytes = 0;
283 let mut state = S::default();
284
285 let mut directories = vec![from];
286 while let Some(directory) = directories.pop() {
287 let Ok(mut entries) = fs::read_dir(&directory).await else {
288 continue;
289 };
290
291 while let Ok(Some(entry)) = entries.next_entry().await {
292 let name = entry.file_name().to_string_lossy().into_owned();
293 if name.starts_with('.') {
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 && counts(&metadata, &mut state) =>
302 {
303 objects += 1;
304 bytes += metadata.len();
305 }
306 _ => {}
307 }
308 }
309 }
310
311 (objects, bytes)
312 }
313}