diskr-cli 1.0.0

Save your disk space, without fear of deleting the wrong thing.
Documentation
use anyhow::{bail, Result};
use std::fs;
use std::path::{Path, PathBuf};

const PROTECTED_PREFIXES: &[&str] = &[
    "/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/root",
    "/run", "/sbin", "/sys", "/usr", "/var/lib", "/var/log",
];

const NEVER_CLEAN_FRAGMENTS: &[&str] = &[
    "/.steam/",
    "/Steam/",
    "/.local/share/Steam/",
    "/lutris/",
    "/.local/share/lutris/",
    "/.local/share/flatpak/",
    "/.var/app/",
    "/snap/",
    "/.local/share/snap/",
    "/var/lib/snapd/",
];

pub fn is_game_or_store_dir(path: &Path) -> bool {
    let s = path.to_string_lossy();
    NEVER_CLEAN_FRAGMENTS.iter().any(|f| s.contains(f))
}

pub fn is_protected(path: &Path) -> bool {
    let canon = fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
    if canon.components().count() <= 1 {
        return true;
    }
    PROTECTED_PREFIXES.iter().any(|p| canon.starts_with(Path::new(p)))
}

pub struct CleanPlan {
    pub keep: Vec<PathBuf>,
    pub remove: Vec<PathBuf>,
    pub total_size: u64,
    pub blocked: Vec<PathBuf>,
}

pub fn build_plan(candidates: Vec<(PathBuf, u64)>) -> CleanPlan {
    let mut remove = Vec::new();
    let mut blocked = Vec::new();
    let mut total_size = 0u64;
    for (path, size) in candidates {
        if is_protected(&path) {
            blocked.push(path);
        } else {
            total_size += size;
            remove.push(path);
        }
    }
    CleanPlan { keep: Vec::new(), remove, total_size, blocked }
}

pub fn execute_plan(plan: &CleanPlan, dry_run: bool) -> Result<usize> {
    if dry_run {
        return Ok(plan.remove.len());
    }
    if !plan.blocked.is_empty() {
        bail!("{} protected paths were excluded from this plan and must not be removed", plan.blocked.len());
    }
    let refs: Vec<&Path> = plan.remove.iter().map(|p| p.as_path()).collect();
    super::trash::move_to_trash(&refs)
}

pub fn dedup_with_hardlink(keep: &Path, remove: &Path) -> Result<()> {
    if is_protected(keep) || is_protected(remove) {
        bail!("refusing to touch a protected path");
    }
    fs::remove_file(remove)?;
    fs::hard_link(keep, remove)?;
    Ok(())
}