Skip to main content

diskforge_core/
scanner.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use rayon::prelude::*;
5
6use crate::disk;
7use crate::rules::{
8    android, dev_artifacts, docker, known_paths, simulator, system_junk, user_rules,
9};
10use crate::safety;
11use crate::sizing;
12use crate::types::ScanResult;
13
14/// Options for controlling the scan
15pub struct ScanOptions {
16    /// Minimum item size to report (bytes)
17    pub min_size: u64,
18    /// Only report items older than this many seconds
19    pub older_than: Option<u64>,
20    /// Directories to scan for dev artifacts
21    pub project_dirs: Vec<PathBuf>,
22}
23
24impl Default for ScanOptions {
25    fn default() -> Self {
26        let home = std::env::var("HOME").unwrap_or_else(|_| "/Users".into());
27        Self {
28            min_size: 1024 * 1024, // 1 MB
29            older_than: None,
30            project_dirs: vec![PathBuf::from(&home)],
31        }
32    }
33}
34
35/// Run a full scan combining all rule categories
36pub fn scan(options: &ScanOptions) -> Result<ScanResult> {
37    let home = std::env::var("HOME")?;
38    let home_path = Path::new(&home);
39
40    // Get disk info
41    let disk_info = disk::get_disk_info(home_path)?;
42
43    let mut items = Vec::new();
44
45    // Phase 1: Known paths — parallel size computation via rayon
46    let rules = known_paths::all_known_path_rules(&home);
47    let known_items: Vec<_> = rules
48        .into_par_iter()
49        .filter(|rule| rule.path.exists())
50        .filter_map(|rule| {
51            let stats = sizing::dir_stats(&rule.path);
52            if stats.size >= options.min_size {
53                Some(rule.into_item_with_stats(stats.size, stats.last_modified))
54            } else {
55                None
56            }
57        })
58        .collect();
59    items.extend(known_items);
60
61    // Phase 2: Dev artifacts (filesystem walk)
62    for project_dir in &options.project_dirs {
63        if project_dir.exists() {
64            let dev_items = dev_artifacts::scan_dev_artifacts(project_dir);
65            items.extend(dev_items);
66        }
67    }
68
69    // Phase 3: iOS Simulator (devices + runtimes)
70    items.extend(simulator::scan_simulators(&home));
71
72    // Phase 4: Android SDK (NDK versions, platforms, build-tools, system-images, AVDs)
73    items.extend(android::scan_android(&home));
74
75    // Phase 5: Docker (images, containers, volumes, build cache via docker system df)
76    items.extend(docker::scan_docker(&home));
77
78    // Phase 6: User-defined rules (~/.config/diskforge/rules.toml)
79    items.extend(user_rules::scan_user_rules(&home));
80
81    // Phase 7: System junk (.DS_Store, crash reports, device support, broken symlinks, brew cache, language packs)
82    items.extend(system_junk::scan_system_junk(&home));
83
84    // Filter by age if requested
85    if let Some(older_than_secs) = options.older_than {
86        let now = std::time::SystemTime::now();
87        items.retain(|item| {
88            item.last_modified
89                .and_then(|lm| now.duration_since(lm).ok())
90                .is_some_and(|age| age.as_secs() >= older_than_secs)
91        });
92    }
93
94    // Safety pass: elevate risk for critical app data (messaging, passwords, etc.)
95    for item in &mut items {
96        item.risk = safety::adjust_risk(&item.path, item.risk);
97    }
98
99    let mut result = ScanResult {
100        items,
101        disk_total: disk_info.total,
102        disk_used: disk_info.used,
103        disk_free: disk_info.free,
104    };
105
106    result.sort_by_size();
107    Ok(result)
108}