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::project::{Project, Projects};
17
18/// Handles the cleanup of build directories from development projects.
19///
20/// The `Cleaner` struct provides methods for removing build directories
21/// (such as `target/` for Rust projects and `node_modules/` for Node.js projects)
22/// with parallel processing, progress reporting, and comprehensive error handling.
23pub struct Cleaner;
24
25impl Cleaner {
26    /// Create a new cleaner instance.
27    ///
28    /// # Returns
29    ///
30    /// A new `Cleaner` instance ready to perform cleanup operations.
31    ///
32    /// # Examples
33    ///
34    /// ```
35    /// # use crate::Cleaner;
36    /// let cleaner = Cleaner::new();
37    /// ```
38    #[must_use]
39    pub fn new() -> Self {
40        Self
41    }
42
43    /// Clean build directories from a collection of projects.
44    ///
45    /// This method performs the main cleanup operation by:
46    /// 1. Setting up a progress bar for user feedback
47    /// 2. Processing projects in parallel for efficiency
48    /// 3. Collecting and reporting any errors that occur
49    /// 4. Providing detailed statistics about the cleanup results
50    ///
51    /// # Arguments
52    ///
53    /// * `projects` - A collection of projects to clean
54    ///
55    /// # Panics
56    ///
57    /// This method may panic if the progress bar template string is invalid,
58    /// though this should not occur under normal circumstances as the template
59    /// is hardcoded and valid.
60    ///
61    /// # Output
62    ///
63    /// This method prints progress information and final statistics to stdout,
64    /// including
65    /// - Real-time progress during cleanup
66    /// - Number of successfully cleaned projects
67    /// - Number of failed projects (if any)
68    /// - Total disk space freed
69    /// - Difference between estimated and actual space freed
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// # use crate::{Cleaner, Projects};
75    /// let projects = Projects::from(vec![/* project instances */]);
76    /// Cleaner::clean_projects(projects);
77    /// ```
78    ///
79    /// # Performance
80    ///
81    /// This method uses parallel processing to clean multiple projects
82    /// simultaneously, which can significantly reduce cleanup time for
83    /// large numbers of projects.
84    ///
85    /// # Error Handling
86    ///
87    /// Individual project cleanup failures do not stop the overall process.
88    /// All errors are collected and reported at the end, allowing the
89    /// cleanup to proceed for projects that can be successfully processed.
90    pub fn clean_projects(projects: Projects) {
91        let total_projects = projects.len();
92        let total_size: u64 = projects.get_total_size();
93
94        println!("\n{}", "๐Ÿงน Starting cleanup...".cyan());
95
96        // Create a progress bar
97        let progress = ProgressBar::new(total_projects as u64);
98        progress.set_style(
99            ProgressStyle::default_bar()
100                .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
101                .unwrap()
102                .progress_chars("โ–ˆโ–‰โ–Šโ–‹โ–Œโ–โ–Žโ–  "),
103        );
104
105        let cleaned_size = Arc::new(Mutex::new(0u64));
106        let errors = Arc::new(Mutex::new(Vec::new()));
107
108        // Clean projects in parallel
109        projects.into_par_iter().for_each(|project| {
110            let result = clean_single_project(&project);
111
112            match result {
113                Ok(freed_size) => {
114                    let mut total_cleaned = cleaned_size.lock().unwrap();
115                    *total_cleaned += freed_size;
116
117                    progress.set_message(format!(
118                        "Cleaned {} ({})",
119                        project
120                            .root_path
121                            .file_name()
122                            .and_then(|n| n.to_str())
123                            .unwrap_or("unknown"),
124                        format_size(freed_size, DECIMAL)
125                    ));
126                }
127                Err(e) => {
128                    let mut errors = errors.lock().unwrap();
129                    errors.push(format!(
130                        "Failed to clean {}: {e}",
131                        project.build_arts.path.display()
132                    ));
133                }
134            }
135
136            progress.inc(1);
137        });
138
139        progress.finish_with_message("โœ… Cleanup complete");
140
141        // Report results
142        let final_cleaned_size = *cleaned_size.lock().unwrap();
143        let errors = errors.lock().unwrap();
144
145        if !errors.is_empty() {
146            println!("\n{}", "โš ๏ธ  Some errors occurred during cleanup:".yellow());
147            for error in errors.iter() {
148                eprintln!("  {}", error.red());
149            }
150        }
151
152        let success_count = total_projects - errors.len();
153        println!("\n{}", "๐Ÿ“Š Cleanup Summary:".bold());
154        println!(
155            "  โœ… Successfully cleaned: {} projects",
156            success_count.to_string().green()
157        );
158
159        if !errors.is_empty() {
160            println!(
161                "  โŒ Failed to clean: {} projects",
162                errors.len().to_string().red()
163            );
164        }
165
166        println!(
167            "  ๐Ÿ’พ Total space freed: {}",
168            format_size(final_cleaned_size, DECIMAL)
169                .bright_green()
170                .bold()
171        );
172
173        if final_cleaned_size != total_size {
174            let difference = total_size.abs_diff(final_cleaned_size);
175            println!(
176                "  ๐Ÿ“‹ Difference from estimate: {}",
177                format_size(difference, DECIMAL).yellow()
178            );
179        }
180    }
181}
182
183/// Clean the build directory for a single project.
184///
185/// This function handles the cleanup of an individual project's build directory.
186/// It calculates the actual size before deletion and then removes the entire
187/// directory tree.
188///
189/// # Arguments
190///
191/// * `project` - The project whose build directory should be cleaned
192///
193/// # Returns
194///
195/// - `Ok(u64)` - The number of bytes freed by the cleanup
196/// - `Err(anyhow::Error)` - If the cleanup operation failed
197///
198/// # Behavior
199///
200/// 1. Checks if the build directory exists (returns 0 if not)
201/// 2. Calculates the actual size of the directory before deletion
202/// 3. Removes the entire directory tree
203/// 4. Returns the amount of space freed
204///
205/// # Error Conditions
206///
207/// This function can fail if:
208/// - The build directory cannot be removed due to permission issues
209/// - Files within the directory are locked or in use by other processes
210/// - The file system encounters I/O errors during deletion
211///
212/// # Examples
213///
214/// ```
215/// # use crate::{Project, clean_single_project};
216/// # use anyhow::Result;
217/// let result = clean_single_project(&project);
218/// match result {
219///     Ok(freed_bytes) => println!("Freed {} bytes", freed_bytes),
220///     Err(e) => eprintln!("Cleanup failed: {}", e),
221/// }
222/// ```
223fn clean_single_project(project: &Project) -> Result<u64> {
224    let build_dir = &project.build_arts.path;
225
226    if !build_dir.exists() {
227        return Ok(0);
228    }
229
230    // Get the actual size before deletion (might be different from the cached size)
231    let actual_size = calculate_directory_size(build_dir);
232
233    // Remove the build directory
234    fs::remove_dir_all(build_dir)?;
235
236    Ok(actual_size)
237}
238
239/// Calculate the total size of a directory and all its contents.
240///
241/// This function recursively traverses a directory tree and sums up the sizes
242/// of all files within it. It handles errors gracefully by skipping files
243/// that cannot be accessed.
244///
245/// # Arguments
246///
247/// * `path` - The directory path to measure
248///
249/// # Returns
250///
251/// The total size of all files in the directory tree, in bytes.
252///
253/// # Error Handling
254///
255/// This function is designed to be robust and will continue processing even
256/// if individual files cannot be accessed. It silently skips:
257/// - Files that cannot be read due to permission issues
258/// - Broken symbolic links
259/// - Files that are deleted while the scan is in progress
260///
261/// # Performance
262///
263/// This function can be I/O intensive for large directories with many files.
264/// It processes files sequentially within each directory but may be called
265/// in parallel for different directories by the cleanup process.
266///
267/// # Examples
268///
269/// ```
270/// # use std::path::Path;
271/// # use crate::calculate_directory_size;
272/// let size = calculate_directory_size(Path::new("/path/to/directory"));
273/// println!("Directory size: {} bytes", size);
274/// ```
275fn calculate_directory_size(path: &std::path::Path) -> u64 {
276    let mut total_size = 0u64;
277
278    for entry in walkdir::WalkDir::new(path) {
279        if let Ok(entry) = entry {
280            if entry.file_type().is_file()
281                && let Ok(metadata) = entry.metadata()
282            {
283                total_size += metadata.len();
284            }
285        } else {
286            // Skip errors for individual files
287        }
288    }
289
290    total_size
291}
292
293impl Default for Cleaner {
294    /// Create a default cleaner instance.
295    ///
296    /// This implementation allows `Cleaner::default()` to be used as an
297    /// alternative to `Cleaner::new()` for creating cleaner instances.
298    ///
299    /// # Returns
300    ///
301    /// A new `Cleaner` instance with default settings.
302    fn default() -> Self {
303        Self::new()
304    }
305}