diskard 0.2.0

Developer-aware disk cleanup CLI
use anyhow::Result;
use diskard_core::config::Config;
use diskard_core::recognizers::all_recognizers;
use diskard_core::scanner::{self, ScanOptions};

use crate::cli::{CategoryFilter, OutputFormat, RiskFilter, SortField};
use crate::output;

pub fn run(
    risk: RiskFilter,
    min_size: Option<String>,
    category: Option<CategoryFilter>,
    sort: SortField,
    older_than: Option<String>,
    format: OutputFormat,
) -> Result<()> {
    let config = Config::load()?;
    let recognizers = all_recognizers();

    let min_size_bytes = match min_size {
        Some(s) => parse_size(&s)?,
        None => config.defaults.min_size,
    };

    let older_duration = match older_than {
        Some(s) => Some(parse_duration(&s)?),
        None => None,
    };

    let options = ScanOptions {
        max_risk: risk.to_risk_level(),
        min_size: min_size_bytes,
        category: category.map(|c| c.to_category()),
        older_than: older_duration,
        sort: sort.to_sort_order(),
    };

    let result = scanner::scan(&recognizers, &config, &options);

    match format {
        OutputFormat::Table => output::print_table(&result),
        OutputFormat::Json => output::print_json(&result),
    }

    Ok(())
}

pub fn parse_size(s: &str) -> Result<u64> {
    let s = s.trim().to_uppercase();

    if let Ok(n) = s.parse::<u64>() {
        return Ok(n);
    }

    let (num_str, multiplier) = if let Some(n) = s.strip_suffix("GB") {
        (n.trim(), 1_073_741_824u64)
    } else if let Some(n) = s.strip_suffix("MB") {
        (n.trim(), 1_048_576u64)
    } else if let Some(n) = s.strip_suffix("KB") {
        (n.trim(), 1_024u64)
    } else if let Some(n) = s.strip_suffix('B') {
        (n.trim(), 1u64)
    } else {
        anyhow::bail!("Invalid size format: {s}. Use e.g. 10MB, 1GB");
    };

    let num: f64 = num_str
        .parse()
        .map_err(|_| anyhow::anyhow!("Invalid size number: {num_str}"))?;

    Ok((num * multiplier as f64) as u64)
}

pub fn parse_duration(s: &str) -> Result<std::time::Duration> {
    let s = s.trim().to_lowercase();

    let (num_str, multiplier) = if let Some(n) = s.strip_suffix('d') {
        (n, 86_400u64)
    } else if let Some(n) = s.strip_suffix('h') {
        (n, 3_600u64)
    } else if let Some(n) = s.strip_suffix('m') {
        (n, 60u64)
    } else if let Some(n) = s.strip_suffix('w') {
        (n, 604_800u64)
    } else {
        anyhow::bail!("Invalid duration: {s}. Use e.g. 7d, 30d, 1h, 2w");
    };

    let num: u64 = num_str
        .parse()
        .map_err(|_| anyhow::anyhow!("Invalid duration number: {num_str}"))?;

    Ok(std::time::Duration::from_secs(num * multiplier))
}