1use crate::{StorageResult, StorageError};
4use std::path::{Path, PathBuf};
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6use tokio::time::interval;
7use tracing::{info, warn, error};
8
9#[derive(Debug)]
11pub struct CleanupManager {
12 temp_directories: Vec<PathBuf>,
13 max_age: Duration,
14 cleanup_interval: Duration,
15 running: std::sync::Arc<std::sync::atomic::AtomicBool>,
16}
17
18impl CleanupManager {
19 pub fn new() -> Self {
21 Self {
22 temp_directories: vec![std::env::temp_dir()],
23 max_age: Duration::from_secs(24 * 60 * 60), cleanup_interval: Duration::from_secs(60 * 60), running: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
26 }
27 }
28
29 pub fn add_temp_directory<P: Into<PathBuf>>(mut self, path: P) -> Self {
31 self.temp_directories.push(path.into());
32 self
33 }
34
35 pub fn with_max_age(mut self, max_age: Duration) -> Self {
37 self.max_age = max_age;
38 self
39 }
40
41 pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
43 self.cleanup_interval = interval;
44 self
45 }
46
47 pub async fn start(&self) -> StorageResult<tokio::task::JoinHandle<()>> {
49 if self.running.swap(true, std::sync::atomic::Ordering::SeqCst) {
50 return Err(StorageError::Configuration("Cleanup manager already running".to_string()));
51 }
52
53 let temp_directories = self.temp_directories.clone();
54 let max_age = self.max_age;
55 let cleanup_interval = self.cleanup_interval;
56 let running = self.running.clone();
57
58 let handle = tokio::spawn(async move {
59 let mut interval = interval(cleanup_interval);
60
61 info!("Starting cleanup manager with {} directories to monitor", temp_directories.len());
62
63 while running.load(std::sync::atomic::Ordering::SeqCst) {
64 interval.tick().await;
65
66 for dir in &temp_directories {
67 if let Err(e) = cleanup_directory(dir, max_age).await {
68 error!("Failed to cleanup directory {}: {}", dir.display(), e);
69 }
70 }
71 }
72
73 info!("Cleanup manager stopped");
74 });
75
76 Ok(handle)
77 }
78
79 pub fn stop(&self) {
81 self.running.store(false, std::sync::atomic::Ordering::SeqCst);
82 }
83
84 pub async fn run_once(&self) -> StorageResult<CleanupStats> {
86 let mut total_stats = CleanupStats::default();
87
88 for dir in &self.temp_directories {
89 let stats = cleanup_directory_with_stats(dir, self.max_age).await?;
90 total_stats.files_deleted += stats.files_deleted;
91 total_stats.bytes_freed += stats.bytes_freed;
92 total_stats.directories_removed += stats.directories_removed;
93 total_stats.errors += stats.errors;
94 }
95
96 Ok(total_stats)
97 }
98}
99
100impl Default for CleanupManager {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106#[derive(Debug, Default, Clone)]
108pub struct CleanupStats {
109 pub files_deleted: u64,
111
112 pub bytes_freed: u64,
114
115 pub directories_removed: u64,
117
118 pub errors: u64,
120}
121
122async fn cleanup_directory(dir: &Path, max_age: Duration) -> StorageResult<()> {
124 let _stats = cleanup_directory_with_stats(dir, max_age).await?;
125 Ok(())
126}
127
128async fn cleanup_directory_with_stats(dir: &Path, max_age: Duration) -> StorageResult<CleanupStats> {
130 if !dir.exists() {
131 return Ok(CleanupStats::default());
132 }
133
134 let mut stats = CleanupStats::default();
135 let cutoff_time = SystemTime::now()
136 .checked_sub(max_age)
137 .unwrap_or(UNIX_EPOCH);
138
139 match cleanup_recursive(dir, cutoff_time, &mut stats).await {
140 Ok(_) => {
141 if stats.files_deleted > 0 || stats.directories_removed > 0 {
142 info!(
143 "Cleanup completed for {}: {} files deleted, {} bytes freed, {} directories removed",
144 dir.display(), stats.files_deleted, stats.bytes_freed, stats.directories_removed
145 );
146 }
147 Ok(stats)
148 }
149 Err(e) => {
150 error!("Cleanup failed for {}: {}", dir.display(), e);
151 stats.errors += 1;
152 Ok(stats)
153 }
154 }
155}
156
157fn cleanup_recursive<'a>(dir: &'a Path, cutoff_time: SystemTime, stats: &'a mut CleanupStats) -> std::pin::Pin<Box<dyn std::future::Future<Output = StorageResult<bool>> + Send + 'a>> {
159 Box::pin(async move {
160 let mut entries = tokio::fs::read_dir(dir).await?;
161 let mut dir_empty = true;
162
163 while let Some(entry) = entries.next_entry().await? {
164 let path = entry.path();
165 let metadata = match entry.metadata().await {
166 Ok(metadata) => metadata,
167 Err(e) => {
168 warn!("Failed to get metadata for {}: {}", path.display(), e);
169 stats.errors += 1;
170 dir_empty = false; continue;
172 }
173 };
174
175 if metadata.is_file() {
176 match metadata.modified() {
178 Ok(modified) if modified < cutoff_time => {
179 match tokio::fs::remove_file(&path).await {
181 Ok(_) => {
182 stats.files_deleted += 1;
183 stats.bytes_freed += metadata.len();
184 info!("Deleted old file: {}", path.display());
185 }
186 Err(e) => {
187 warn!("Failed to delete file {}: {}", path.display(), e);
188 stats.errors += 1;
189 dir_empty = false;
190 }
191 }
192 }
193 Ok(_) => {
194 dir_empty = false;
196 }
197 Err(e) => {
198 warn!("Failed to get modification time for {}: {}", path.display(), e);
199 stats.errors += 1;
200 dir_empty = false;
201 }
202 }
203 } else if metadata.is_dir() {
204 match cleanup_recursive(&path, cutoff_time, stats).await {
206 Ok(subdir_empty) => {
207 if subdir_empty {
208 match tokio::fs::remove_dir(&path).await {
210 Ok(_) => {
211 stats.directories_removed += 1;
212 info!("Removed empty directory: {}", path.display());
213 }
214 Err(e) => {
215 warn!("Failed to remove empty directory {}: {}", path.display(), e);
216 stats.errors += 1;
217 dir_empty = false;
218 }
219 }
220 } else {
221 dir_empty = false;
222 }
223 }
224 Err(e) => {
225 warn!("Failed to cleanup directory {}: {}", path.display(), e);
226 stats.errors += 1;
227 dir_empty = false;
228 }
229 }
230 }
231 }
232
233 Ok(dir_empty)
234 })
235}
236
237#[derive(Debug)]
239pub struct TempFile {
240 path: PathBuf,
241 keep_on_drop: bool,
242}
243
244impl TempFile {
245 pub async fn new() -> StorageResult<Self> {
247 let temp_dir = std::env::temp_dir();
248 let filename = format!("elif-temp-{}", uuid::Uuid::new_v4());
249 let path = temp_dir.join(filename);
250
251 tokio::fs::File::create(&path).await?;
253
254 Ok(Self {
255 path,
256 keep_on_drop: false,
257 })
258 }
259
260 pub async fn with_extension(extension: &str) -> StorageResult<Self> {
262 let temp_dir = std::env::temp_dir();
263 let filename = format!("elif-temp-{}.{}", uuid::Uuid::new_v4(), extension);
264 let path = temp_dir.join(filename);
265
266 tokio::fs::File::create(&path).await?;
268
269 Ok(Self {
270 path,
271 keep_on_drop: false,
272 })
273 }
274
275 pub fn path(&self) -> &Path {
277 &self.path
278 }
279
280 pub fn keep_on_drop(mut self) -> Self {
282 self.keep_on_drop = true;
283 self
284 }
285
286 pub async fn persist<P: AsRef<Path>>(mut self, path: P) -> StorageResult<()> {
288 self.keep_on_drop = true;
289 tokio::fs::rename(&self.path, path).await?;
290 Ok(())
291 }
292}
293
294impl Drop for TempFile {
295 fn drop(&mut self) {
296 if !self.keep_on_drop && self.path.exists() {
297 let path_to_delete = self.path.clone();
298 tokio::task::spawn_blocking(move || {
300 if let Err(e) = std::fs::remove_file(&path_to_delete) {
301 warn!("Failed to cleanup temporary file {}: {}", path_to_delete.display(), e);
302 }
303 });
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use std::io::Write;
312 use tempfile::tempdir;
313
314 #[tokio::test]
315 async fn test_cleanup_manager() {
316 let temp_dir = tempdir().unwrap();
317 let temp_path = temp_dir.path().to_path_buf();
318
319 let old_file = temp_path.join("old_file.txt");
321 let new_file = temp_path.join("new_file.txt");
322
323 std::fs::write(&old_file, "old content").unwrap();
325 let two_days_ago = SystemTime::now() - Duration::from_secs(2 * 24 * 60 * 60);
326 filetime::set_file_mtime(&old_file, filetime::FileTime::from(two_days_ago)).unwrap();
327
328 std::fs::write(&new_file, "new content").unwrap();
330
331 let manager = CleanupManager::new()
333 .add_temp_directory(temp_path)
334 .with_max_age(Duration::from_secs(24 * 60 * 60)); let stats = manager.run_once().await.unwrap();
337
338 assert!(!old_file.exists());
340 assert!(new_file.exists());
341 assert!(stats.files_deleted > 0);
343 assert!(stats.bytes_freed > 0);
344 }
345
346 #[tokio::test]
347 async fn test_temp_file() {
348 let temp_file = TempFile::new().await.unwrap();
349 let path = temp_file.path().to_path_buf();
350
351 assert!(path.exists());
352
353 let mut file = std::fs::OpenOptions::new()
355 .write(true)
356 .open(&path)
357 .unwrap();
358 file.write_all(b"test content").unwrap();
359 drop(file);
360
361 drop(temp_file);
363
364 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
366
367 assert!(!path.exists());
369 }
370
371 #[tokio::test]
372 async fn test_temp_file_keep_on_drop() {
373 let temp_file = TempFile::new().await.unwrap();
374 let path = temp_file.path().to_path_buf();
375
376 assert!(path.exists());
377
378 let temp_file = temp_file.keep_on_drop();
380 drop(temp_file);
381
382 assert!(path.exists());
384
385 std::fs::remove_file(&path).unwrap();
387 }
388
389 #[tokio::test]
390 async fn test_temp_file_with_extension() {
391 let temp_file = TempFile::with_extension("txt").await.unwrap();
392 let path = temp_file.path().to_path_buf();
393
394 assert!(path.exists());
395 assert_eq!(path.extension().unwrap(), "txt");
396
397 drop(temp_file);
398
399 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
401
402 assert!(!path.exists());
403 }
404}