diskr-cli 1.0.0

Save your disk space, without fear of deleting the wrong thing.
Documentation
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,
    }
}