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