Skip to main content

diskr/scanner/
walker.rs

1use super::FsNode;
2use crate::scanner::ScanResult;
3use anyhow::{Context, Result};
4use indicatif::{ProgressBar, ProgressStyle};
5use jwalk::WalkDir;
6use std::collections::HashMap;
7use std::fs::Metadata;
8use std::os::unix::fs::MetadataExt;
9use std::path::{Path, PathBuf};
10use std::time::{Instant, SystemTime};
11
12pub struct WalkOptions {
13    pub follow_symlinks: bool,
14    pub cross_filesystems: bool,
15    pub exclude_patterns: Vec<String>,
16    pub show_progress: bool,
17}
18
19impl Default for WalkOptions {
20    fn default() -> Self {
21        Self { follow_symlinks: false, cross_filesystems: false, exclude_patterns: Vec::new(), show_progress: true }
22    }
23}
24
25pub fn scan_path(root: &Path, opts: &WalkOptions) -> Result<ScanResult> {
26    let start = Instant::now();
27    let root = root.canonicalize().with_context(|| format!("path not found: {}", root.display()))?;
28    let root_dev = std::fs::metadata(&root)?.dev();
29
30    let pb = if opts.show_progress {
31        let pb = ProgressBar::new_spinner();
32        pb.set_style(ProgressStyle::with_template("{spinner:.cyan} scanning... {pos} items").unwrap());
33        Some(pb)
34    } else {
35        None
36    };
37
38    let mut by_dir: HashMap<PathBuf, Vec<FsNode>> = HashMap::new();
39    let mut all_dirs: Vec<PathBuf> = Vec::new();
40    let mut file_count = 0u64;
41    let mut dir_count = 0u64;
42    let mut scanned = 0u64;
43
44    let walker = WalkDir::new(&root).follow_links(opts.follow_symlinks).skip_hidden(false);
45
46    for entry in walker {
47        let entry = match entry { Ok(e) => e, Err(_) => continue };
48        let path = entry.path();
49        if opts.exclude_patterns.iter().any(|p| path.to_string_lossy().contains(p.as_str())) {
50            continue;
51        }
52        let meta = match entry.metadata() { Ok(m) => m, Err(_) => continue };
53        if !opts.cross_filesystems && meta.dev() != root_dev {
54            continue;
55        }
56        scanned += 1;
57        if let Some(pb) = &pb {
58            if scanned % 500 == 0 { pb.set_position(scanned); }
59        }
60        let parent = path.parent().unwrap_or(&root).to_path_buf();
61        if meta.is_dir() {
62            dir_count += 1;
63            if path != root {
64                all_dirs.push(path.clone());
65            }
66            by_dir.entry(path.clone()).or_default();
67        } else {
68            file_count += 1;
69            let node = leaf_from_meta(&path, &meta);
70            by_dir.entry(parent).or_default().push(node);
71        }
72    }
73
74    if let Some(pb) = &pb { pb.finish_and_clear(); }
75
76    all_dirs.sort_by_key(|p| std::cmp::Reverse(p.components().count()));
77
78    for dir in &all_dirs {
79        let children = by_dir.remove(dir).unwrap_or_default();
80        let (size, apparent) = children.iter().fold((0u64, 0u64), |(s, a), c| (s + c.size, a + c.apparent_size));
81        let dir_meta = std::fs::symlink_metadata(dir).ok();
82        let node = FsNode {
83            path: dir.clone(),
84            is_dir: true,
85            size,
86            apparent_size: apparent,
87            inode: dir_meta.as_ref().map(|m| m.ino()).unwrap_or(0),
88            nlink: dir_meta.as_ref().map(|m| m.nlink()).unwrap_or(1),
89            modified: dir_meta.as_ref().and_then(|m| m.modified().ok()),
90            accessed: dir_meta.as_ref().and_then(|m| m.accessed().ok()),
91            children,
92        };
93        if let Some(parent) = dir.parent() {
94            by_dir.entry(parent.to_path_buf()).or_default().push(node);
95        } else {
96            by_dir.insert(dir.clone(), vec![node]);
97        }
98    }
99
100    let mut root_children = by_dir.remove(&root).unwrap_or_default();
101    let root_node = if root_children.len() == 1 && root_children[0].path == root {
102        root_children.pop().unwrap()
103    } else {
104        let (size, apparent) = root_children.iter().fold((0u64, 0u64), |(s, a), c| (s + c.size, a + c.apparent_size));
105        let root_meta = std::fs::symlink_metadata(&root).ok();
106        FsNode {
107            path: root,
108            is_dir: true,
109            size,
110            apparent_size: apparent,
111            inode: root_meta.as_ref().map(|m| m.ino()).unwrap_or(0),
112            nlink: 1,
113            modified: root_meta.as_ref().and_then(|m| m.modified().ok()),
114            accessed: None,
115            children: root_children,
116        }
117    };
118
119    Ok(ScanResult {
120        total_size: root_node.size,
121        file_count,
122        dir_count,
123        root: root_node,
124        scan_duration_ms: start.elapsed().as_millis(),
125    })
126}
127
128fn leaf_from_meta(path: &Path, meta: &Metadata) -> FsNode {
129    let real_size = meta.blocks().saturating_mul(512);
130    FsNode::leaf(path.to_path_buf(), real_size, meta.len(), meta.ino(), meta.nlink(), meta.modified().ok(), meta.accessed().ok())
131}
132
133pub fn age_days(t: SystemTime) -> i64 {
134    match SystemTime::now().duration_since(t) {
135        Ok(d) => (d.as_secs() / 86400) as i64,
136        Err(_) => 0,
137    }
138}