1use super::{Artifact, CleanResult, Project};
6use crate::error::Result;
7use std::path::PathBuf;
8use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
9use std::sync::Arc;
10use parking_lot::Mutex;
11
12#[derive(Debug, Clone)]
14pub struct CleanConfig {
15 pub use_trash: bool,
17 pub dry_run: bool,
19 pub force: bool,
21 pub skip_git_check: bool,
23 pub artifact_kinds: Option<Vec<super::ArtifactKind>>,
25 pub parallelism: Option<usize>,
27 pub continue_on_error: bool,
29}
30
31impl Default for CleanConfig {
32 fn default() -> Self {
33 Self {
34 use_trash: true,
35 dry_run: false,
36 force: false,
37 skip_git_check: false,
38 artifact_kinds: None,
39 parallelism: None,
40 continue_on_error: true,
41 }
42 }
43}
44
45impl CleanConfig {
46 pub fn permanent() -> Self {
48 Self {
49 use_trash: false,
50 ..Default::default()
51 }
52 }
53
54 pub fn dry_run() -> Self {
56 Self {
57 dry_run: true,
58 ..Default::default()
59 }
60 }
61
62 pub fn with_force(mut self) -> Self {
64 self.force = true;
65 self
66 }
67
68 pub fn without_git_check(mut self) -> Self {
70 self.skip_git_check = true;
71 self
72 }
73
74 pub fn with_kinds(mut self, kinds: Vec<super::ArtifactKind>) -> Self {
76 self.artifact_kinds = Some(kinds);
77 self
78 }
79}
80
81#[derive(Debug, Clone)]
83pub enum CleanTarget {
84 Project(Project),
86 Artifacts(Vec<Artifact>),
88 Paths(Vec<PathBuf>),
90}
91
92impl CleanTarget {
93 pub fn total_size(&self) -> u64 {
95 match self {
96 Self::Project(p) => p.cleanable_size,
97 Self::Artifacts(a) => a.iter().map(|a| a.size).sum(),
98 Self::Paths(_) => 0, }
100 }
101
102 pub fn count(&self) -> usize {
104 match self {
105 Self::Project(p) => p.artifacts.len(),
106 Self::Artifacts(a) => a.len(),
107 Self::Paths(p) => p.len(),
108 }
109 }
110}
111
112#[derive(Debug, Default)]
114pub struct CleanProgress {
115 pub total_items: AtomicUsize,
117 pub completed_items: AtomicUsize,
119 pub bytes_cleaned: AtomicU64,
121 pub bytes_failed: AtomicU64,
123 pub current_item: Mutex<String>,
125 pub errors: Mutex<Vec<CleanError>>,
127 pub is_complete: std::sync::atomic::AtomicBool,
129 pub is_cancelled: std::sync::atomic::AtomicBool,
131}
132
133impl CleanProgress {
134 pub fn new(total: usize) -> Arc<Self> {
136 let progress = Arc::new(Self::default());
137 progress.total_items.store(total, Ordering::Relaxed);
138 progress
139 }
140
141 pub fn complete_item(&self, bytes: u64) {
143 self.completed_items.fetch_add(1, Ordering::Relaxed);
144 self.bytes_cleaned.fetch_add(bytes, Ordering::Relaxed);
145 }
146
147 pub fn fail_item(&self, bytes: u64, error: CleanError) {
149 self.completed_items.fetch_add(1, Ordering::Relaxed);
150 self.bytes_failed.fetch_add(bytes, Ordering::Relaxed);
151 self.errors.lock().push(error);
152 }
153
154 pub fn set_current(&self, item: impl Into<String>) {
156 *self.current_item.lock() = item.into();
157 }
158
159 pub fn percentage(&self) -> f32 {
161 let total = self.total_items.load(Ordering::Relaxed);
162 if total == 0 {
163 return 100.0;
164 }
165 let completed = self.completed_items.load(Ordering::Relaxed);
166 (completed as f32 / total as f32) * 100.0
167 }
168
169 pub fn cancel(&self) {
171 self.is_cancelled.store(true, Ordering::Release);
172 }
173
174 pub fn is_cancelled(&self) -> bool {
176 self.is_cancelled.load(Ordering::Acquire)
177 }
178
179 pub fn mark_complete(&self) {
181 self.is_complete.store(true, Ordering::Release);
182 }
183
184 pub fn snapshot(&self) -> CleanProgressSnapshot {
186 CleanProgressSnapshot {
187 total_items: self.total_items.load(Ordering::Relaxed),
188 completed_items: self.completed_items.load(Ordering::Relaxed),
189 bytes_cleaned: self.bytes_cleaned.load(Ordering::Relaxed),
190 bytes_failed: self.bytes_failed.load(Ordering::Relaxed),
191 current_item: self.current_item.lock().clone(),
192 error_count: self.errors.lock().len(),
193 is_complete: self.is_complete.load(Ordering::Acquire),
194 }
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct CleanProgressSnapshot {
201 pub total_items: usize,
202 pub completed_items: usize,
203 pub bytes_cleaned: u64,
204 pub bytes_failed: u64,
205 pub current_item: String,
206 pub error_count: usize,
207 pub is_complete: bool,
208}
209
210impl CleanProgressSnapshot {
211 pub fn percentage(&self) -> f32 {
213 if self.total_items == 0 {
214 return 100.0;
215 }
216 (self.completed_items as f32 / self.total_items as f32) * 100.0
217 }
218}
219
220#[derive(Debug, Clone)]
222pub struct CleanError {
223 pub path: PathBuf,
225 pub message: String,
227 pub recoverable: bool,
229}
230
231impl CleanError {
232 pub fn new(path: PathBuf, message: impl Into<String>) -> Self {
234 Self {
235 path,
236 message: message.into(),
237 recoverable: true,
238 }
239 }
240}
241
242#[derive(Debug, Clone)]
244pub struct CleanSummary {
245 pub total_items: usize,
247 pub succeeded: usize,
249 pub failed: usize,
251 pub skipped: usize,
253 pub bytes_freed: u64,
255 pub bytes_failed: u64,
257 pub used_trash: bool,
259 pub results: Vec<CleanResult>,
261 pub errors: Vec<CleanError>,
263}
264
265impl CleanSummary {
266 pub fn empty() -> Self {
268 Self {
269 total_items: 0,
270 succeeded: 0,
271 failed: 0,
272 skipped: 0,
273 bytes_freed: 0,
274 bytes_failed: 0,
275 used_trash: false,
276 results: Vec::new(),
277 errors: Vec::new(),
278 }
279 }
280
281 pub fn is_complete_success(&self) -> bool {
283 self.failed == 0 && self.skipped == 0
284 }
285
286 pub fn has_failures(&self) -> bool {
288 self.failed > 0
289 }
290
291 pub fn to_string(&self) -> String {
293 let freed = humansize::format_size(self.bytes_freed, humansize::BINARY);
294
295 if self.is_complete_success() {
296 format!(
297 "Successfully cleaned {} items, freed {}",
298 self.succeeded, freed
299 )
300 } else {
301 format!(
302 "Cleaned {} items ({} freed), {} failed, {} skipped",
303 self.succeeded, freed, self.failed, self.skipped
304 )
305 }
306 }
307}
308
309pub trait Cleaner: Send + Sync {
311 fn clean(&self, targets: &[CleanTarget], config: &CleanConfig) -> Result<CleanSummary>;
313
314 fn clean_artifact(&self, artifact: &Artifact, config: &CleanConfig) -> Result<CleanResult>;
316
317 fn progress(&self) -> Arc<CleanProgress>;
319
320 fn cancel(&self) {
322 self.progress().cancel();
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_clean_config_builder() {
332 let config = CleanConfig::default().with_force().without_git_check();
333
334 assert!(config.use_trash);
335 assert!(config.force);
336 assert!(config.skip_git_check);
337 }
338
339 #[test]
340 fn test_clean_progress() {
341 let progress = CleanProgress::new(10);
342
343 progress.complete_item(1000);
344 progress.complete_item(500);
345
346 let snapshot = progress.snapshot();
347 assert_eq!(snapshot.completed_items, 2);
348 assert_eq!(snapshot.bytes_cleaned, 1500);
349 assert_eq!(snapshot.percentage(), 20.0);
350 }
351
352 #[test]
353 fn test_clean_summary() {
354 let summary = CleanSummary {
355 total_items: 10,
356 succeeded: 8,
357 failed: 1,
358 skipped: 1,
359 bytes_freed: 1024 * 1024,
360 bytes_failed: 1024,
361 used_trash: true,
362 results: Vec::new(),
363 errors: Vec::new(),
364 };
365
366 assert!(!summary.is_complete_success());
367 assert!(summary.has_failures());
368 }
369}