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}