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}