use super::FsNode;
use crate::scanner::ScanResult;
use anyhow::{Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use jwalk::WalkDir;
use std::collections::HashMap;
use std::fs::Metadata;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime};
pub struct WalkOptions {
pub follow_symlinks: bool,
pub cross_filesystems: bool,
pub exclude_patterns: Vec<String>,
pub show_progress: bool,
}
impl Default for WalkOptions {
fn default() -> Self {
Self { follow_symlinks: false, cross_filesystems: false, exclude_patterns: Vec::new(), show_progress: true }
}
}
pub fn scan_path(root: &Path, opts: &WalkOptions) -> Result<ScanResult> {
let start = Instant::now();
let root = root.canonicalize().with_context(|| format!("path not found: {}", root.display()))?;
let root_dev = std::fs::metadata(&root)?.dev();
let pb = if opts.show_progress {
let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::with_template("{spinner:.cyan} scanning... {pos} items").unwrap());
Some(pb)
} else {
None
};
let mut by_dir: HashMap<PathBuf, Vec<FsNode>> = HashMap::new();
let mut all_dirs: Vec<PathBuf> = Vec::new();
let mut file_count = 0u64;
let mut dir_count = 0u64;
let mut scanned = 0u64;
let walker = WalkDir::new(&root).follow_links(opts.follow_symlinks).skip_hidden(false);
for entry in walker {
let entry = match entry { Ok(e) => e, Err(_) => continue };
let path = entry.path();
if opts.exclude_patterns.iter().any(|p| path.to_string_lossy().contains(p.as_str())) {
continue;
}
let meta = match entry.metadata() { Ok(m) => m, Err(_) => continue };
if !opts.cross_filesystems && meta.dev() != root_dev {
continue;
}
scanned += 1;
if let Some(pb) = &pb {
if scanned % 500 == 0 { pb.set_position(scanned); }
}
let parent = path.parent().unwrap_or(&root).to_path_buf();
if meta.is_dir() {
dir_count += 1;
if path != root {
all_dirs.push(path.clone());
}
by_dir.entry(path.clone()).or_default();
} else {
file_count += 1;
let node = leaf_from_meta(&path, &meta);
by_dir.entry(parent).or_default().push(node);
}
}
if let Some(pb) = &pb { pb.finish_and_clear(); }
all_dirs.sort_by_key(|p| std::cmp::Reverse(p.components().count()));
for dir in &all_dirs {
let children = by_dir.remove(dir).unwrap_or_default();
let (size, apparent) = children.iter().fold((0u64, 0u64), |(s, a), c| (s + c.size, a + c.apparent_size));
let dir_meta = std::fs::symlink_metadata(dir).ok();
let node = FsNode {
path: dir.clone(),
is_dir: true,
size,
apparent_size: apparent,
inode: dir_meta.as_ref().map(|m| m.ino()).unwrap_or(0),
nlink: dir_meta.as_ref().map(|m| m.nlink()).unwrap_or(1),
modified: dir_meta.as_ref().and_then(|m| m.modified().ok()),
accessed: dir_meta.as_ref().and_then(|m| m.accessed().ok()),
children,
};
if let Some(parent) = dir.parent() {
by_dir.entry(parent.to_path_buf()).or_default().push(node);
} else {
by_dir.insert(dir.clone(), vec![node]);
}
}
let mut root_children = by_dir.remove(&root).unwrap_or_default();
let root_node = if root_children.len() == 1 && root_children[0].path == root {
root_children.pop().unwrap()
} else {
let (size, apparent) = root_children.iter().fold((0u64, 0u64), |(s, a), c| (s + c.size, a + c.apparent_size));
let root_meta = std::fs::symlink_metadata(&root).ok();
FsNode {
path: root,
is_dir: true,
size,
apparent_size: apparent,
inode: root_meta.as_ref().map(|m| m.ino()).unwrap_or(0),
nlink: 1,
modified: root_meta.as_ref().and_then(|m| m.modified().ok()),
accessed: None,
children: root_children,
}
};
Ok(ScanResult {
total_size: root_node.size,
file_count,
dir_count,
root: root_node,
scan_duration_ms: start.elapsed().as_millis(),
})
}
fn leaf_from_meta(path: &Path, meta: &Metadata) -> FsNode {
let real_size = meta.blocks().saturating_mul(512);
FsNode::leaf(path.to_path_buf(), real_size, meta.len(), meta.ino(), meta.nlink(), meta.modified().ok(), meta.accessed().ok())
}
pub fn age_days(t: SystemTime) -> i64 {
match SystemTime::now().duration_since(t) {
Ok(d) => (d.as_secs() / 86400) as i64,
Err(_) => 0,
}
}