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