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.build_arts.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 let build_dir = &project.build_arts.path;
288
289 if !build_dir.exists() {
290 return Ok(0);
291 }
292
293 // Preserve executables before deletion if requested
294 if keep_executables {
295 match executables::preserve_executables(project) {
296 Ok(preserved) => {
297 if !preserved.is_empty() {
298 eprintln!(
299 " Preserved {} executable(s) from {}",
300 preserved.len(),
301 project
302 .root_path
303 .file_name()
304 .and_then(|n| n.to_str())
305 .unwrap_or("unknown")
306 );
307 }
308 }
309 Err(e) => {
310 eprintln!(
311 " Warning: failed to preserve executables for {}: {e}",
312 project.root_path.display()
313 );
314 }
315 }
316 }
317
318 // Get the actual size before deletion (might be different from the cached size)
319 let actual_size = crate::utils::calculate_dir_size(build_dir);
320
321 // Remove the build directory using the chosen strategy
322 match removal_strategy {
323 RemovalStrategy::Permanent => fs::remove_dir_all(build_dir)?,
324 RemovalStrategy::Trash => {
325 trash::delete(build_dir)
326 .map_err(|e| anyhow::anyhow!("failed to move to trash: {e}"))?;
327 }
328 }
329
330 Ok(actual_size)
331}
332
333impl Default for Cleaner {
334 /// Create a default cleaner instance.
335 ///
336 /// This implementation allows `Cleaner::default()` to be used as an
337 /// alternative to `Cleaner::new()` for creating cleaner instances.
338 ///
339 /// # Returns
340 ///
341 /// A new `Cleaner` instance with default settings.
342 fn default() -> Self {
343 Self::new()
344 }
345}