Skip to main content

clean_dev_dirs/
cleaner.rs

1//! Build directory cleanup functionality.
2//!
3//! This module provides the core cleanup logic for removing build directories
4//! from detected development projects. It handles parallel processing, progress
5//! reporting, error handling, and provides detailed statistics about the
6//! cleanup operation.
7
8use anyhow::Result;
9use colored::Colorize;
10use humansize::{DECIMAL, format_size};
11use indicatif::{ProgressBar, ProgressStyle};
12use rayon::prelude::*;
13use std::fs;
14use std::sync::{Arc, Mutex};
15
16use crate::executables;
17use crate::project::{Project, Projects};
18
19/// Strategy for removing build directories.
20#[derive(Clone, Copy, Debug)]
21pub enum RemovalStrategy {
22    /// Permanently delete the directory (default, uses `fs::remove_dir_all`).
23    Permanent,
24
25    /// Move the directory to the system trash (recoverable deletion).
26    Trash,
27}
28
29impl RemovalStrategy {
30    /// Create a removal strategy from the `use_trash` boolean flag.
31    #[must_use]
32    pub const fn from_use_trash(use_trash: bool) -> Self {
33        if use_trash {
34            Self::Trash
35        } else {
36            Self::Permanent
37        }
38    }
39}
40
41/// Structured result returned after a cleanup operation.
42///
43/// Contains all the data needed to render either human-readable or JSON output.
44#[derive(Debug)]
45pub struct CleanResult {
46    /// Number of projects successfully cleaned.
47    pub success_count: usize,
48
49    /// Total bytes actually freed during cleanup.
50    pub total_freed: u64,
51
52    /// Estimated total size before cleanup (from cached scan data).
53    pub estimated_size: u64,
54
55    /// Error messages for projects that failed to clean.
56    pub errors: Vec<String>,
57}
58
59/// Handles the cleanup of build directories from development projects.
60///
61/// The `Cleaner` struct provides methods for removing build directories
62/// (such as `target/` for Rust projects and `node_modules/` for Node.js projects)
63/// with parallel processing, progress reporting, and comprehensive error handling.
64#[derive(Debug)]
65pub struct Cleaner;
66
67impl Cleaner {
68    /// Create a new cleaner instance.
69    ///
70    /// # Returns
71    ///
72    /// A new `Cleaner` instance ready to perform cleanup operations.
73    ///
74    /// # Examples
75    ///
76    /// ```
77    /// # use crate::Cleaner;
78    /// let cleaner = Cleaner::new();
79    /// ```
80    #[must_use]
81    pub const fn new() -> Self {
82        Self
83    }
84
85    /// Clean build directories from a collection of projects.
86    ///
87    /// This method performs the main cleanup operation by:
88    /// 1. Setting up a progress bar for user feedback (unless `quiet`)
89    /// 2. Processing projects in parallel for efficiency
90    /// 3. Collecting and reporting any errors that occur
91    /// 4. Returning a [`CleanResult`] with detailed statistics
92    ///
93    /// # Arguments
94    ///
95    /// * `projects` - A collection of projects to clean
96    /// * `keep_executables` - Whether to preserve compiled executables before cleaning
97    /// * `quiet` - When `true`, suppresses all human-readable output (progress bars, messages).
98    ///   Used by the `--json` flag so that only the final JSON is printed.
99    /// * `removal_strategy` - Whether to permanently delete or move to system trash
100    ///
101    /// # Panics
102    ///
103    /// This method may panic if the progress bar template string is invalid,
104    /// though this should not occur under normal circumstances as the template
105    /// is hardcoded and valid.
106    ///
107    /// # Returns
108    ///
109    /// A [`CleanResult`] containing success/failure counts, total freed bytes,
110    /// and any error messages.
111    ///
112    /// # Performance
113    ///
114    /// This method uses parallel processing to clean multiple projects
115    /// simultaneously, which can significantly reduce cleanup time for
116    /// large numbers of projects.
117    ///
118    /// # Error Handling
119    ///
120    /// Individual project cleanup failures do not stop the overall process.
121    /// All errors are collected and reported in the returned [`CleanResult`],
122    /// allowing the cleanup to proceed for projects that can be successfully processed.
123    #[must_use]
124    pub fn clean_projects(
125        projects: Projects,
126        keep_executables: bool,
127        quiet: bool,
128        removal_strategy: RemovalStrategy,
129    ) -> CleanResult {
130        let total_projects = projects.len();
131        let total_size: u64 = projects.get_total_size();
132
133        let progress = if quiet {
134            ProgressBar::hidden()
135        } else {
136            let action = match removal_strategy {
137                RemovalStrategy::Permanent => "Starting cleanup...",
138                RemovalStrategy::Trash => "Moving to trash...",
139            };
140            println!("\n{}", action.cyan());
141
142            let pb = ProgressBar::new(total_projects as u64);
143            if let Ok(style) = ProgressStyle::default_bar()
144                .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
145            {
146                pb.set_style(style.progress_chars("█▉▊▋▌▍▎▏  "));
147            }
148            pb
149        };
150
151        let cleaned_size = Arc::new(Mutex::new(0u64));
152        let errors = Arc::new(Mutex::new(Vec::new()));
153
154        // Clean projects in parallel
155        projects.into_par_iter().for_each(|project| {
156            let result = clean_single_project(&project, keep_executables, removal_strategy);
157
158            let action = match removal_strategy {
159                RemovalStrategy::Permanent => "Cleaned",
160                RemovalStrategy::Trash => "Trashed",
161            };
162
163            match result {
164                Ok(freed_size) => {
165                    if let Ok(mut size) = cleaned_size.lock() {
166                        *size += freed_size;
167                    }
168
169                    progress.set_message(format!(
170                        "{action} {} ({})",
171                        project
172                            .root_path
173                            .file_name()
174                            .and_then(|n| n.to_str())
175                            .unwrap_or("unknown"),
176                        format_size(freed_size, DECIMAL)
177                    ));
178                }
179                Err(e) => {
180                    if let Ok(mut errs) = errors.lock() {
181                        errs.push(format!(
182                            "Failed to clean {}: {e}",
183                            project.root_path.display()
184                        ));
185                    }
186                }
187            }
188
189            progress.inc(1);
190        });
191
192        let finish_msg = match removal_strategy {
193            RemovalStrategy::Permanent => "[OK] Cleanup complete",
194            RemovalStrategy::Trash => "[OK] Moved to trash",
195        };
196        progress.finish_with_message(finish_msg);
197
198        let final_cleaned_size = cleaned_size.lock().map_or(0, |s| *s);
199        let errors = Arc::try_unwrap(errors)
200            .unwrap_or_else(|arc| {
201                arc.lock()
202                    .map_or_else(|_| Mutex::new(Vec::new()), |g| Mutex::new(g.clone()))
203            })
204            .into_inner()
205            .unwrap_or_default();
206
207        let success_count = total_projects - errors.len();
208
209        CleanResult {
210            success_count,
211            total_freed: final_cleaned_size,
212            estimated_size: total_size,
213            errors,
214        }
215    }
216
217    /// Print a human-readable cleanup summary to stdout.
218    ///
219    /// This is called from `main` when `--json` is **not** active.
220    pub fn print_summary(result: &CleanResult) {
221        if !result.errors.is_empty() {
222            println!("\n{}", "[!] Some errors occurred during cleanup:".yellow());
223            for error in &result.errors {
224                eprintln!("  {}", error.red());
225            }
226        }
227
228        println!("\n{}", "Cleanup Summary:".bold());
229        println!(
230            "  [OK] Successfully cleaned: {} projects",
231            result.success_count.to_string().green()
232        );
233
234        if !result.errors.is_empty() {
235            println!(
236                "  [FAIL] Failed to clean: {} projects",
237                result.errors.len().to_string().red()
238            );
239        }
240
241        println!(
242            "  Total space freed: {}",
243            format_size(result.total_freed, DECIMAL)
244                .bright_green()
245                .bold()
246        );
247
248        if result.total_freed != result.estimated_size {
249            let difference = result.estimated_size.abs_diff(result.total_freed);
250            println!(
251                "  Difference from estimate: {}",
252                format_size(difference, DECIMAL).yellow()
253            );
254        }
255    }
256}
257
258/// Clean the build directory for a single project.
259///
260/// This function handles the cleanup of an individual project's build directory.
261/// It calculates the actual size before deletion and then removes the entire
262/// directory tree, either permanently or by moving it to the system trash.
263///
264/// # Arguments
265///
266/// * `project` - The project whose build directory should be cleaned
267/// * `keep_executables` - Whether to preserve compiled executables before cleaning
268/// * `removal_strategy` - Whether to permanently delete or move to system trash
269///
270/// # Returns
271///
272/// - `Ok(u64)` - The number of bytes freed by the cleanup
273/// - `Err(anyhow::Error)` - If the cleanup operation failed
274///
275/// # Behavior
276///
277/// 1. Checks if the build directory exists (returns 0 if not)
278/// 2. Optionally preserves compiled executables
279/// 3. Calculates the actual size of the directory before deletion
280/// 4. Removes the directory (permanently or via trash, based on `removal_strategy`)
281/// 5. Returns the amount of space freed
282///
283/// # Error Conditions
284///
285/// This function can fail if:
286/// - The build directory cannot be removed due to permission issues
287/// - Files within the directory are locked or in use by other processes
288/// - The file system encounters I/O errors during deletion
289/// - The system trash is not available (when using [`RemovalStrategy::Trash`])
290fn clean_single_project(
291    project: &Project,
292    keep_executables: bool,
293    removal_strategy: RemovalStrategy,
294) -> Result<u64> {
295    // Preserve executables before deletion if requested
296    if keep_executables {
297        match executables::preserve_executables(project) {
298            Ok(preserved) => {
299                if !preserved.is_empty() {
300                    eprintln!(
301                        "  Preserved {} executable(s) from {}",
302                        preserved.len(),
303                        project
304                            .root_path
305                            .file_name()
306                            .and_then(|n| n.to_str())
307                            .unwrap_or("unknown")
308                    );
309                }
310            }
311            Err(e) => {
312                eprintln!(
313                    "  Warning: failed to preserve executables for {}: {e}",
314                    project.root_path.display()
315                );
316            }
317        }
318    }
319
320    let mut total_freed = 0u64;
321
322    for artifact in &project.build_arts {
323        let build_dir = &artifact.path;
324
325        if !build_dir.exists() {
326            continue;
327        }
328
329        // Get the actual size before deletion (might be different from the cached size)
330        total_freed += crate::utils::calculate_dir_size(build_dir);
331
332        // Remove the build directory using the chosen strategy
333        match removal_strategy {
334            RemovalStrategy::Permanent => fs::remove_dir_all(build_dir)?,
335            RemovalStrategy::Trash => {
336                trash::delete(build_dir)
337                    .map_err(|e| anyhow::anyhow!("failed to move to trash: {e}"))?;
338            }
339        }
340    }
341
342    Ok(total_freed)
343}
344
345impl Default for Cleaner {
346    /// Create a default cleaner instance.
347    ///
348    /// This implementation allows `Cleaner::default()` to be used as an
349    /// alternative to `Cleaner::new()` for creating cleaner instances.
350    ///
351    /// # Returns
352    ///
353    /// A new `Cleaner` instance with default settings.
354    fn default() -> Self {
355        Self::new()
356    }
357}