Skip to main content

null_e/trash/
mod.rs

1//! Trash support - safe deletion with recovery
2//!
3//! Provides cross-platform trash functionality so users can recover
4//! accidentally deleted files.
5
6mod platform;
7mod record;
8
9pub use platform::*;
10pub use record::*;
11
12use crate::core::{Artifact, CleanResult};
13use crate::error::{DevSweepError, Result};
14use std::path::Path;
15
16/// Delete method for cleanup operations
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum DeleteMethod {
19    /// Move to system trash (recoverable)
20    #[default]
21    Trash,
22    /// Permanently delete (not recoverable!)
23    Permanent,
24    /// Just log what would be deleted (dry run)
25    DryRun,
26}
27
28impl DeleteMethod {
29    /// Parse from string
30    pub fn from_str(s: &str) -> Option<Self> {
31        match s.to_lowercase().as_str() {
32            "trash" => Some(Self::Trash),
33            "permanent" | "delete" | "rm" => Some(Self::Permanent),
34            "dry-run" | "dryrun" | "dry_run" => Some(Self::DryRun),
35            _ => None,
36        }
37    }
38}
39
40/// Delete a path using the specified method
41pub fn delete_path(path: &Path, method: DeleteMethod) -> Result<u64> {
42    if !path.exists() {
43        return Ok(0);
44    }
45
46    match method {
47        DeleteMethod::DryRun => {
48            // Just calculate size, don't delete
49            let size = calculate_size(path)?;
50            Ok(size)
51        }
52        DeleteMethod::Trash => {
53            let size = calculate_size(path)?;
54            trash::delete(path).map_err(|e| {
55                DevSweepError::Trash(format!("Failed to move to trash: {}", e))
56            })?;
57            Ok(size)
58        }
59        DeleteMethod::Permanent => {
60            let size = calculate_size(path)?;
61            if path.is_dir() {
62                std::fs::remove_dir_all(path)?;
63            } else {
64                std::fs::remove_file(path)?;
65            }
66            Ok(size)
67        }
68    }
69}
70
71/// Delete an artifact
72pub fn delete_artifact(artifact: &Artifact, method: DeleteMethod) -> CleanResult {
73    match delete_path(&artifact.path, method) {
74        Ok(_bytes) => CleanResult::success(artifact.clone(), method == DeleteMethod::Trash),
75        Err(e) => CleanResult::failure(artifact.clone(), e.to_string()),
76    }
77}
78
79/// Calculate size of a path
80fn calculate_size(path: &Path) -> Result<u64> {
81    if path.is_file() {
82        return Ok(path.metadata()?.len());
83    }
84
85    let mut size = 0u64;
86    for entry in walkdir::WalkDir::new(path) {
87        if let Ok(entry) = entry {
88            if entry.file_type().is_file() {
89                if let Ok(meta) = entry.metadata() {
90                    size += meta.len();
91                }
92            }
93        }
94    }
95    Ok(size)
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use tempfile::TempDir;
102
103    #[test]
104    fn test_delete_method_from_str() {
105        assert_eq!(DeleteMethod::from_str("trash"), Some(DeleteMethod::Trash));
106        assert_eq!(DeleteMethod::from_str("permanent"), Some(DeleteMethod::Permanent));
107        assert_eq!(DeleteMethod::from_str("dry-run"), Some(DeleteMethod::DryRun));
108        assert_eq!(DeleteMethod::from_str("invalid"), None);
109    }
110
111    #[test]
112    fn test_dry_run_doesnt_delete() {
113        let temp = TempDir::new().unwrap();
114        let file = temp.path().join("test.txt");
115        std::fs::write(&file, "hello world").unwrap();
116
117        let size = delete_path(&file, DeleteMethod::DryRun).unwrap();
118        assert!(size > 0);
119        assert!(file.exists()); // File should still exist
120    }
121
122    #[test]
123    fn test_permanent_delete() {
124        let temp = TempDir::new().unwrap();
125        let file = temp.path().join("test.txt");
126        std::fs::write(&file, "hello world").unwrap();
127
128        let size = delete_path(&file, DeleteMethod::Permanent).unwrap();
129        assert!(size > 0);
130        assert!(!file.exists()); // File should be gone
131    }
132
133    #[test]
134    fn test_delete_nonexistent() {
135        let path = Path::new("/nonexistent/path/that/doesnt/exist");
136        let size = delete_path(path, DeleteMethod::DryRun).unwrap();
137        assert_eq!(size, 0);
138    }
139
140    #[test]
141    fn test_delete_directory() {
142        let temp = TempDir::new().unwrap();
143        let dir = temp.path().join("subdir");
144        std::fs::create_dir(&dir).unwrap();
145        std::fs::write(dir.join("file1.txt"), "content1").unwrap();
146        std::fs::write(dir.join("file2.txt"), "content2").unwrap();
147
148        let size = delete_path(&dir, DeleteMethod::Permanent).unwrap();
149        assert!(size > 0);
150        assert!(!dir.exists());
151    }
152
153    #[test]
154    fn test_calculate_size() {
155        let temp = TempDir::new().unwrap();
156        let file = temp.path().join("test.txt");
157        std::fs::write(&file, "0123456789").unwrap(); // 10 bytes
158
159        let size = calculate_size(&file).unwrap();
160        assert_eq!(size, 10);
161    }
162}