1use super::{Project, ArtifactStats};
6use crate::error::Result;
7use std::path::PathBuf;
8use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
9use std::sync::Arc;
10use std::time::Duration;
11use parking_lot::Mutex;
12
13#[derive(Debug, Clone)]
15pub struct ScanConfig {
16 pub roots: Vec<PathBuf>,
18 pub max_depth: Option<usize>,
20 pub parallelism: Option<usize>,
22 pub skip_hidden: bool,
24 pub respect_gitignore: bool,
26 pub ignore_patterns: Vec<String>,
28 pub min_size: Option<u64>,
30 pub limit: Option<usize>,
32 pub check_git_status: bool,
34 pub timeout: Option<Duration>,
36}
37
38impl Default for ScanConfig {
39 fn default() -> Self {
40 Self {
41 roots: vec![],
42 max_depth: None,
43 parallelism: None,
44 skip_hidden: true,
45 respect_gitignore: true,
46 ignore_patterns: vec![],
47 min_size: None,
48 limit: None,
49 check_git_status: true,
50 timeout: None,
51 }
52 }
53}
54
55impl ScanConfig {
56 pub fn new(root: impl Into<PathBuf>) -> Self {
58 Self {
59 roots: vec![root.into()],
60 ..Default::default()
61 }
62 }
63
64 pub fn with_root(mut self, root: impl Into<PathBuf>) -> Self {
66 self.roots.push(root.into());
67 self
68 }
69
70 pub fn with_max_depth(mut self, depth: usize) -> Self {
72 self.max_depth = Some(depth);
73 self
74 }
75
76 pub fn with_parallelism(mut self, threads: usize) -> Self {
78 self.parallelism = Some(threads);
79 self
80 }
81
82 pub fn with_ignore(mut self, pattern: impl Into<String>) -> Self {
84 self.ignore_patterns.push(pattern.into());
85 self
86 }
87
88 pub fn with_min_size(mut self, size: u64) -> Self {
90 self.min_size = Some(size);
91 self
92 }
93
94 pub fn with_limit(mut self, limit: usize) -> Self {
96 self.limit = Some(limit);
97 self
98 }
99
100 pub fn without_git_check(mut self) -> Self {
102 self.check_git_status = false;
103 self
104 }
105
106 pub fn with_timeout(mut self, timeout: Duration) -> Self {
108 self.timeout = Some(timeout);
109 self
110 }
111}
112
113#[derive(Debug, Default)]
115pub struct ScanProgress {
116 pub directories_scanned: AtomicUsize,
118 pub projects_found: AtomicUsize,
120 pub total_size_found: AtomicU64,
122 pub current_path: Mutex<PathBuf>,
124 pub errors: Mutex<Vec<ScanError>>,
126 pub is_complete: std::sync::atomic::AtomicBool,
128 pub is_cancelled: std::sync::atomic::AtomicBool,
130}
131
132impl ScanProgress {
133 pub fn new() -> Arc<Self> {
135 Arc::new(Self::default())
136 }
137
138 pub fn inc_directories(&self) {
140 self.directories_scanned.fetch_add(1, Ordering::Relaxed);
141 }
142
143 pub fn inc_projects(&self) {
145 self.projects_found.fetch_add(1, Ordering::Relaxed);
146 }
147
148 pub fn add_size(&self, size: u64) {
150 self.total_size_found.fetch_add(size, Ordering::Relaxed);
151 }
152
153 pub fn set_current_path(&self, path: PathBuf) {
155 *self.current_path.lock() = path;
156 }
157
158 pub fn add_error(&self, error: ScanError) {
160 self.errors.lock().push(error);
161 }
162
163 pub fn mark_complete(&self) {
165 self.is_complete.store(true, Ordering::Release);
166 }
167
168 pub fn cancel(&self) {
170 self.is_cancelled.store(true, Ordering::Release);
171 }
172
173 pub fn is_cancelled(&self) -> bool {
175 self.is_cancelled.load(Ordering::Acquire)
176 }
177
178 pub fn snapshot(&self) -> ProgressSnapshot {
180 ProgressSnapshot {
181 directories_scanned: self.directories_scanned.load(Ordering::Relaxed),
182 projects_found: self.projects_found.load(Ordering::Relaxed),
183 total_size_found: self.total_size_found.load(Ordering::Relaxed),
184 current_path: self.current_path.lock().clone(),
185 error_count: self.errors.lock().len(),
186 is_complete: self.is_complete.load(Ordering::Acquire),
187 }
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct ProgressSnapshot {
194 pub directories_scanned: usize,
195 pub projects_found: usize,
196 pub total_size_found: u64,
197 pub current_path: PathBuf,
198 pub error_count: usize,
199 pub is_complete: bool,
200}
201
202#[derive(Debug, Clone)]
204pub struct ScanError {
205 pub path: PathBuf,
207 pub message: String,
209 pub recoverable: bool,
211}
212
213impl ScanError {
214 pub fn new(path: PathBuf, message: impl Into<String>) -> Self {
216 Self {
217 path,
218 message: message.into(),
219 recoverable: true,
220 }
221 }
222
223 pub fn fatal(path: PathBuf, message: impl Into<String>) -> Self {
225 Self {
226 path,
227 message: message.into(),
228 recoverable: false,
229 }
230 }
231}
232
233#[derive(Debug)]
235pub struct ScanResult {
236 pub projects: Vec<Project>,
238 pub total_size: u64,
240 pub total_cleanable: u64,
242 pub duration: Duration,
244 pub directories_scanned: usize,
246 pub errors: Vec<ScanError>,
248 pub stats: ArtifactStats,
250}
251
252impl ScanResult {
253 pub fn project_count(&self) -> usize {
255 self.projects.len()
256 }
257
258 pub fn artifact_count(&self) -> usize {
260 self.projects.iter().map(|p| p.artifacts.len()).sum()
261 }
262
263 pub fn projects_by_size(&self) -> Vec<&Project> {
265 let mut projects: Vec<_> = self.projects.iter().collect();
266 projects.sort_by(|a, b| b.cleanable_size.cmp(&a.cleanable_size));
267 projects
268 }
269
270 pub fn summary(&self) -> String {
272 format!(
273 "Found {} projects with {} cleanable across {} artifacts in {:.2}s",
274 self.projects.len(),
275 humansize::format_size(self.total_cleanable, humansize::BINARY),
276 self.artifact_count(),
277 self.duration.as_secs_f64()
278 )
279 }
280}
281
282pub trait Scanner: Send + Sync {
284 fn scan(&self, config: &ScanConfig) -> Result<ScanResult>;
286
287 fn progress(&self) -> Arc<ScanProgress>;
289
290 fn cancel(&self) {
292 self.progress().cancel();
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_scan_config_builder() {
302 let config = ScanConfig::new("/home/user")
303 .with_root("/tmp")
304 .with_max_depth(5)
305 .with_min_size(1024)
306 .with_ignore("*.bak");
307
308 assert_eq!(config.roots.len(), 2);
309 assert_eq!(config.max_depth, Some(5));
310 assert_eq!(config.min_size, Some(1024));
311 assert_eq!(config.ignore_patterns.len(), 1);
312 }
313
314 #[test]
315 fn test_scan_progress() {
316 let progress = ScanProgress::new();
317
318 progress.inc_directories();
319 progress.inc_directories();
320 progress.inc_projects();
321 progress.add_size(1000);
322
323 let snapshot = progress.snapshot();
324 assert_eq!(snapshot.directories_scanned, 2);
325 assert_eq!(snapshot.projects_found, 1);
326 assert_eq!(snapshot.total_size_found, 1000);
327 }
328
329 #[test]
330 fn test_scan_progress_cancellation() {
331 let progress = ScanProgress::new();
332 assert!(!progress.is_cancelled());
333
334 progress.cancel();
335 assert!(progress.is_cancelled());
336 }
337}