Skip to main content

null_e/core/
cleaner.rs

1//! Cleaner trait and types
2//!
3//! Defines the interface for cleaning (removing) artifacts.
4
5use super::{Artifact, CleanResult, Project};
6use crate::error::Result;
7use std::path::PathBuf;
8use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
9use std::sync::Arc;
10use parking_lot::Mutex;
11
12/// Configuration for cleaning operations
13#[derive(Debug, Clone)]
14pub struct CleanConfig {
15    /// Use trash instead of permanent deletion
16    pub use_trash: bool,
17    /// Dry run - don't actually delete anything
18    pub dry_run: bool,
19    /// Force clean even with warnings
20    pub force: bool,
21    /// Skip git status checks
22    pub skip_git_check: bool,
23    /// Specific artifact kinds to clean (None = all)
24    pub artifact_kinds: Option<Vec<super::ArtifactKind>>,
25    /// Maximum concurrent delete operations
26    pub parallelism: Option<usize>,
27    /// Continue on errors
28    pub continue_on_error: bool,
29}
30
31impl Default for CleanConfig {
32    fn default() -> Self {
33        Self {
34            use_trash: true,
35            dry_run: false,
36            force: false,
37            skip_git_check: false,
38            artifact_kinds: None,
39            parallelism: None,
40            continue_on_error: true,
41        }
42    }
43}
44
45impl CleanConfig {
46    /// Create a config that permanently deletes (not trash)
47    pub fn permanent() -> Self {
48        Self {
49            use_trash: false,
50            ..Default::default()
51        }
52    }
53
54    /// Create a dry run config
55    pub fn dry_run() -> Self {
56        Self {
57            dry_run: true,
58            ..Default::default()
59        }
60    }
61
62    /// Set to force mode
63    pub fn with_force(mut self) -> Self {
64        self.force = true;
65        self
66    }
67
68    /// Skip git checks
69    pub fn without_git_check(mut self) -> Self {
70        self.skip_git_check = true;
71        self
72    }
73
74    /// Filter to specific artifact kinds
75    pub fn with_kinds(mut self, kinds: Vec<super::ArtifactKind>) -> Self {
76        self.artifact_kinds = Some(kinds);
77        self
78    }
79}
80
81/// What to clean - can be a whole project or specific artifacts
82#[derive(Debug, Clone)]
83pub enum CleanTarget {
84    /// Clean all artifacts in a project
85    Project(Project),
86    /// Clean specific artifacts
87    Artifacts(Vec<Artifact>),
88    /// Clean specific paths
89    Paths(Vec<PathBuf>),
90}
91
92impl CleanTarget {
93    /// Get total size to be cleaned
94    pub fn total_size(&self) -> u64 {
95        match self {
96            Self::Project(p) => p.cleanable_size,
97            Self::Artifacts(a) => a.iter().map(|a| a.size).sum(),
98            Self::Paths(_) => 0, // Unknown
99        }
100    }
101
102    /// Get number of items
103    pub fn count(&self) -> usize {
104        match self {
105            Self::Project(p) => p.artifacts.len(),
106            Self::Artifacts(a) => a.len(),
107            Self::Paths(p) => p.len(),
108        }
109    }
110}
111
112/// Progress tracking for clean operations
113#[derive(Debug, Default)]
114pub struct CleanProgress {
115    /// Total items to clean
116    pub total_items: AtomicUsize,
117    /// Items completed
118    pub completed_items: AtomicUsize,
119    /// Bytes cleaned so far
120    pub bytes_cleaned: AtomicU64,
121    /// Bytes that failed to clean
122    pub bytes_failed: AtomicU64,
123    /// Currently cleaning item
124    pub current_item: Mutex<String>,
125    /// Errors encountered
126    pub errors: Mutex<Vec<CleanError>>,
127    /// Whether operation is complete
128    pub is_complete: std::sync::atomic::AtomicBool,
129    /// Whether operation was cancelled
130    pub is_cancelled: std::sync::atomic::AtomicBool,
131}
132
133impl CleanProgress {
134    /// Create a new progress tracker
135    pub fn new(total: usize) -> Arc<Self> {
136        let progress = Arc::new(Self::default());
137        progress.total_items.store(total, Ordering::Relaxed);
138        progress
139    }
140
141    /// Mark an item as completed
142    pub fn complete_item(&self, bytes: u64) {
143        self.completed_items.fetch_add(1, Ordering::Relaxed);
144        self.bytes_cleaned.fetch_add(bytes, Ordering::Relaxed);
145    }
146
147    /// Mark an item as failed
148    pub fn fail_item(&self, bytes: u64, error: CleanError) {
149        self.completed_items.fetch_add(1, Ordering::Relaxed);
150        self.bytes_failed.fetch_add(bytes, Ordering::Relaxed);
151        self.errors.lock().push(error);
152    }
153
154    /// Set current item being cleaned
155    pub fn set_current(&self, item: impl Into<String>) {
156        *self.current_item.lock() = item.into();
157    }
158
159    /// Get completion percentage
160    pub fn percentage(&self) -> f32 {
161        let total = self.total_items.load(Ordering::Relaxed);
162        if total == 0 {
163            return 100.0;
164        }
165        let completed = self.completed_items.load(Ordering::Relaxed);
166        (completed as f32 / total as f32) * 100.0
167    }
168
169    /// Request cancellation
170    pub fn cancel(&self) {
171        self.is_cancelled.store(true, Ordering::Release);
172    }
173
174    /// Check if cancelled
175    pub fn is_cancelled(&self) -> bool {
176        self.is_cancelled.load(Ordering::Acquire)
177    }
178
179    /// Mark as complete
180    pub fn mark_complete(&self) {
181        self.is_complete.store(true, Ordering::Release);
182    }
183
184    /// Get snapshot
185    pub fn snapshot(&self) -> CleanProgressSnapshot {
186        CleanProgressSnapshot {
187            total_items: self.total_items.load(Ordering::Relaxed),
188            completed_items: self.completed_items.load(Ordering::Relaxed),
189            bytes_cleaned: self.bytes_cleaned.load(Ordering::Relaxed),
190            bytes_failed: self.bytes_failed.load(Ordering::Relaxed),
191            current_item: self.current_item.lock().clone(),
192            error_count: self.errors.lock().len(),
193            is_complete: self.is_complete.load(Ordering::Acquire),
194        }
195    }
196}
197
198/// Snapshot of clean progress
199#[derive(Debug, Clone)]
200pub struct CleanProgressSnapshot {
201    pub total_items: usize,
202    pub completed_items: usize,
203    pub bytes_cleaned: u64,
204    pub bytes_failed: u64,
205    pub current_item: String,
206    pub error_count: usize,
207    pub is_complete: bool,
208}
209
210impl CleanProgressSnapshot {
211    /// Get percentage complete
212    pub fn percentage(&self) -> f32 {
213        if self.total_items == 0 {
214            return 100.0;
215        }
216        (self.completed_items as f32 / self.total_items as f32) * 100.0
217    }
218}
219
220/// Error during clean operation
221#[derive(Debug, Clone)]
222pub struct CleanError {
223    /// Path that failed to clean
224    pub path: PathBuf,
225    /// Error message
226    pub message: String,
227    /// Whether the error is recoverable
228    pub recoverable: bool,
229}
230
231impl CleanError {
232    /// Create a new clean error
233    pub fn new(path: PathBuf, message: impl Into<String>) -> Self {
234        Self {
235            path,
236            message: message.into(),
237            recoverable: true,
238        }
239    }
240}
241
242/// Summary of a cleaning operation
243#[derive(Debug, Clone)]
244pub struct CleanSummary {
245    /// Total items attempted
246    pub total_items: usize,
247    /// Items successfully cleaned
248    pub succeeded: usize,
249    /// Items that failed
250    pub failed: usize,
251    /// Items skipped (e.g., due to warnings)
252    pub skipped: usize,
253    /// Total bytes freed
254    pub bytes_freed: u64,
255    /// Total bytes that failed to clean
256    pub bytes_failed: u64,
257    /// Whether trash was used
258    pub used_trash: bool,
259    /// Individual results
260    pub results: Vec<CleanResult>,
261    /// Errors encountered
262    pub errors: Vec<CleanError>,
263}
264
265impl CleanSummary {
266    /// Create an empty summary
267    pub fn empty() -> Self {
268        Self {
269            total_items: 0,
270            succeeded: 0,
271            failed: 0,
272            skipped: 0,
273            bytes_freed: 0,
274            bytes_failed: 0,
275            used_trash: false,
276            results: Vec::new(),
277            errors: Vec::new(),
278        }
279    }
280
281    /// Check if all items were successful
282    pub fn is_complete_success(&self) -> bool {
283        self.failed == 0 && self.skipped == 0
284    }
285
286    /// Check if any items failed
287    pub fn has_failures(&self) -> bool {
288        self.failed > 0
289    }
290
291    /// Get human-readable summary
292    pub fn to_string(&self) -> String {
293        let freed = humansize::format_size(self.bytes_freed, humansize::BINARY);
294
295        if self.is_complete_success() {
296            format!(
297                "Successfully cleaned {} items, freed {}",
298                self.succeeded, freed
299            )
300        } else {
301            format!(
302                "Cleaned {} items ({} freed), {} failed, {} skipped",
303                self.succeeded, freed, self.failed, self.skipped
304            )
305        }
306    }
307}
308
309/// Trait for implementing cleaners
310pub trait Cleaner: Send + Sync {
311    /// Clean the specified targets
312    fn clean(&self, targets: &[CleanTarget], config: &CleanConfig) -> Result<CleanSummary>;
313
314    /// Clean a single artifact
315    fn clean_artifact(&self, artifact: &Artifact, config: &CleanConfig) -> Result<CleanResult>;
316
317    /// Get the progress tracker
318    fn progress(&self) -> Arc<CleanProgress>;
319
320    /// Cancel the ongoing clean operation
321    fn cancel(&self) {
322        self.progress().cancel();
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_clean_config_builder() {
332        let config = CleanConfig::default().with_force().without_git_check();
333
334        assert!(config.use_trash);
335        assert!(config.force);
336        assert!(config.skip_git_check);
337    }
338
339    #[test]
340    fn test_clean_progress() {
341        let progress = CleanProgress::new(10);
342
343        progress.complete_item(1000);
344        progress.complete_item(500);
345
346        let snapshot = progress.snapshot();
347        assert_eq!(snapshot.completed_items, 2);
348        assert_eq!(snapshot.bytes_cleaned, 1500);
349        assert_eq!(snapshot.percentage(), 20.0);
350    }
351
352    #[test]
353    fn test_clean_summary() {
354        let summary = CleanSummary {
355            total_items: 10,
356            succeeded: 8,
357            failed: 1,
358            skipped: 1,
359            bytes_freed: 1024 * 1024,
360            bytes_failed: 1024,
361            used_trash: true,
362            results: Vec::new(),
363            errors: Vec::new(),
364        };
365
366        assert!(!summary.is_complete_success());
367        assert!(summary.has_failures());
368    }
369}