lfsx_server/storage/
staging.rs1use std::path::{Path, PathBuf};
2use std::time::Duration;
3
4use tokio::fs;
5
6use super::LocalStore;
7use super::sweep::age;
8
9#[derive(Debug, Default, PartialEq, Eq)]
10pub struct Reclaimed {
11 pub files: u64,
12 pub bytes: u64,
13}
14
15impl LocalStore {
16 pub async fn reclaim_staging(&self, older_than: Duration) -> Reclaimed {
25 let mut reclaimed = Reclaimed::default();
26 let mut directories = vec![self.root.clone()];
27
28 while let Some(directory) = directories.pop() {
29 let Ok(mut entries) = fs::read_dir(&directory).await else {
30 continue;
31 };
32
33 while let Ok(Some(entry)) = entries.next_entry().await {
34 let path = entry.path();
35 let Ok(metadata) = entry.metadata().await else {
36 continue;
37 };
38
39 if metadata.is_dir() {
40 if directory != self.root || !is_dotted(&path) {
46 directories.push(path);
47 }
48 continue;
49 }
50
51 let is_staging = path
52 .extension()
53 .is_some_and(|extension| extension == "part");
54 if !is_staging || age(&metadata) < older_than {
55 continue;
56 }
57
58 if fs::remove_file(&path).await.is_ok() {
59 reclaimed.files += 1;
60 reclaimed.bytes += metadata.len();
61 }
62 }
63 }
64
65 reclaimed
66 }
67}
68
69fn is_dotted(path: &Path) -> bool {
70 path.file_name()
71 .is_some_and(|name| name.to_string_lossy().starts_with('.'))
72}
73
74pub async fn reclaim(root: PathBuf, older_than: Duration) {
75 let reclaimed = LocalStore::new(root).reclaim_staging(older_than).await;
76
77 if reclaimed.files > 0 {
78 tracing::info!(
79 files = reclaimed.files,
80 bytes = reclaimed.bytes,
81 "reclaimed staging files left by interrupted uploads"
82 );
83 }
84}
85
86#[cfg(test)]
87mod tests;