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