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}