Skip to main content

null_e/core/
scanner.rs

1//! Scanner trait and types
2//!
3//! Defines the interface for scanning filesystems for projects and artifacts.
4
5use 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/// Configuration for scanning operations
14#[derive(Debug, Clone)]
15pub struct ScanConfig {
16    /// Root directories to scan
17    pub roots: Vec<PathBuf>,
18    /// Maximum depth to traverse (None = unlimited)
19    pub max_depth: Option<usize>,
20    /// Number of parallel threads (None = auto based on CPU)
21    pub parallelism: Option<usize>,
22    /// Skip hidden files and directories
23    pub skip_hidden: bool,
24    /// Respect .gitignore files
25    pub respect_gitignore: bool,
26    /// Custom ignore patterns (glob syntax)
27    pub ignore_patterns: Vec<String>,
28    /// Minimum artifact size to report (bytes)
29    pub min_size: Option<u64>,
30    /// Maximum number of projects to return
31    pub limit: Option<usize>,
32    /// Include git status check for each project
33    pub check_git_status: bool,
34    /// Timeout for the entire scan operation
35    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    /// Create a new config with a single root directory
57    pub fn new(root: impl Into<PathBuf>) -> Self {
58        Self {
59            roots: vec![root.into()],
60            ..Default::default()
61        }
62    }
63
64    /// Add a root directory
65    pub fn with_root(mut self, root: impl Into<PathBuf>) -> Self {
66        self.roots.push(root.into());
67        self
68    }
69
70    /// Set maximum depth
71    pub fn with_max_depth(mut self, depth: usize) -> Self {
72        self.max_depth = Some(depth);
73        self
74    }
75
76    /// Set parallelism
77    pub fn with_parallelism(mut self, threads: usize) -> Self {
78        self.parallelism = Some(threads);
79        self
80    }
81
82    /// Add ignore pattern
83    pub fn with_ignore(mut self, pattern: impl Into<String>) -> Self {
84        self.ignore_patterns.push(pattern.into());
85        self
86    }
87
88    /// Set minimum size filter
89    pub fn with_min_size(mut self, size: u64) -> Self {
90        self.min_size = Some(size);
91        self
92    }
93
94    /// Set result limit
95    pub fn with_limit(mut self, limit: usize) -> Self {
96        self.limit = Some(limit);
97        self
98    }
99
100    /// Disable git status checking
101    pub fn without_git_check(mut self) -> Self {
102        self.check_git_status = false;
103        self
104    }
105
106    /// Set timeout
107    pub fn with_timeout(mut self, timeout: Duration) -> Self {
108        self.timeout = Some(timeout);
109        self
110    }
111}
112
113/// Real-time scan progress information
114#[derive(Debug, Default)]
115pub struct ScanProgress {
116    /// Number of directories scanned
117    pub directories_scanned: AtomicUsize,
118    /// Number of projects found
119    pub projects_found: AtomicUsize,
120    /// Total cleanable size found so far
121    pub total_size_found: AtomicU64,
122    /// Currently scanning path
123    pub current_path: Mutex<PathBuf>,
124    /// Errors encountered during scan
125    pub errors: Mutex<Vec<ScanError>>,
126    /// Whether scan is complete
127    pub is_complete: std::sync::atomic::AtomicBool,
128    /// Whether scan was cancelled
129    pub is_cancelled: std::sync::atomic::AtomicBool,
130}
131
132impl ScanProgress {
133    /// Create a new progress tracker
134    pub fn new() -> Arc<Self> {
135        Arc::new(Self::default())
136    }
137
138    /// Increment directories scanned
139    pub fn inc_directories(&self) {
140        self.directories_scanned.fetch_add(1, Ordering::Relaxed);
141    }
142
143    /// Increment projects found
144    pub fn inc_projects(&self) {
145        self.projects_found.fetch_add(1, Ordering::Relaxed);
146    }
147
148    /// Add to total size
149    pub fn add_size(&self, size: u64) {
150        self.total_size_found.fetch_add(size, Ordering::Relaxed);
151    }
152
153    /// Update current path
154    pub fn set_current_path(&self, path: PathBuf) {
155        *self.current_path.lock() = path;
156    }
157
158    /// Add an error
159    pub fn add_error(&self, error: ScanError) {
160        self.errors.lock().push(error);
161    }
162
163    /// Mark as complete
164    pub fn mark_complete(&self) {
165        self.is_complete.store(true, Ordering::Release);
166    }
167
168    /// Request cancellation
169    pub fn cancel(&self) {
170        self.is_cancelled.store(true, Ordering::Release);
171    }
172
173    /// Check if cancelled
174    pub fn is_cancelled(&self) -> bool {
175        self.is_cancelled.load(Ordering::Acquire)
176    }
177
178    /// Get snapshot of current progress
179    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/// Snapshot of progress at a point in time
192#[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/// Error encountered during scan
203#[derive(Debug, Clone)]
204pub struct ScanError {
205    /// Path where error occurred
206    pub path: PathBuf,
207    /// Error description
208    pub message: String,
209    /// Whether this error is recoverable
210    pub recoverable: bool,
211}
212
213impl ScanError {
214    /// Create a new scan error
215    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    /// Create a non-recoverable error
224    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/// Result of a complete scan
234#[derive(Debug)]
235pub struct ScanResult {
236    /// Projects found
237    pub projects: Vec<Project>,
238    /// Total size of all artifacts
239    pub total_size: u64,
240    /// Total cleanable size
241    pub total_cleanable: u64,
242    /// Scan duration
243    pub duration: Duration,
244    /// Number of directories scanned
245    pub directories_scanned: usize,
246    /// Errors encountered
247    pub errors: Vec<ScanError>,
248    /// Statistics by artifact kind
249    pub stats: ArtifactStats,
250}
251
252impl ScanResult {
253    /// Get the number of projects
254    pub fn project_count(&self) -> usize {
255        self.projects.len()
256    }
257
258    /// Get total artifact count across all projects
259    pub fn artifact_count(&self) -> usize {
260        self.projects.iter().map(|p| p.artifacts.len()).sum()
261    }
262
263    /// Get projects sorted by cleanable size (descending)
264    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    /// Get human-readable summary
271    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
282/// Trait for implementing scanners
283pub trait Scanner: Send + Sync {
284    /// Run the scan with the given configuration
285    fn scan(&self, config: &ScanConfig) -> Result<ScanResult>;
286
287    /// Get the progress tracker
288    fn progress(&self) -> Arc<ScanProgress>;
289
290    /// Cancel the ongoing scan
291    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}