waxpkg 0.15.9

Fast Homebrew-compatible package manager
use crate::error::Result;
use crate::install::InstallState;
use crate::version::sort_versions;
use console::style;

pub async fn cleanup(dry_run: bool) -> Result<()> {
    let state = InstallState::new()?;
    state.sync_from_cellar().await.ok();
    let installed = state.load().await?;

    let mut total_freed: u64 = 0;
    let mut removed_count = 0;

    for pkg in installed.values() {
        let cellar = pkg.install_mode.cellar_path()?;
        let pkg_dir = cellar.join(&pkg.name);

        if !pkg_dir.exists() {
            continue;
        }

        let mut versions: Vec<String> = match std::fs::read_dir(&pkg_dir) {
            Ok(entries) => entries
                .filter_map(|e| e.ok())
                .filter(|e| e.path().is_dir())
                .map(|e| e.file_name().to_string_lossy().to_string())
                .collect(),
            Err(_) => continue,
        };

        if versions.len() <= 1 {
            continue;
        }

        sort_versions(&mut versions);
        let old_versions = &versions[..versions.len() - 1];

        for old_ver in old_versions {
            let old_path = pkg_dir.join(old_ver);
            let size = dir_size(&old_path);

            if dry_run {
                println!(
                    "  would remove {}@{} ({})",
                    style(&pkg.name).magenta(),
                    style(old_ver).dim(),
                    format_bytes(size)
                );
            } else {
                if let Err(e) = std::fs::remove_dir_all(&old_path) {
                    eprintln!(
                        "  {} failed to remove {}@{}: {}",
                        style("").red(),
                        style(&pkg.name).magenta(),
                        old_ver,
                        e
                    );
                    continue;
                }
                println!(
                    "  {} {}@{} ({})",
                    style("removed").green(),
                    style(&pkg.name).magenta(),
                    style(old_ver).dim(),
                    format_bytes(size)
                );
            }
            total_freed += size;
            removed_count += 1;
        }
    }

    if removed_count == 0 {
        println!("nothing to clean up");
    } else if dry_run {
        println!(
            "\nwould free {} across {} old version{}",
            format_bytes(total_freed),
            removed_count,
            if removed_count == 1 { "" } else { "s" }
        );
        println!("run without --dry-run to remove");
    } else {
        println!(
            "\nfreed {} ({} old version{} removed)",
            format_bytes(total_freed),
            removed_count,
            if removed_count == 1 { "" } else { "s" }
        );
    }

    Ok(())
}

fn dir_size(path: &std::path::Path) -> u64 {
    let mut total = 0u64;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.filter_map(|e| e.ok()) {
            let p = entry.path();
            if p.is_dir() {
                total += dir_size(&p);
            } else if let Ok(meta) = std::fs::metadata(&p) {
                total += meta.len();
            }
        }
    }
    total
}

fn format_bytes(bytes: u64) -> String {
    if bytes >= 1_073_741_824 {
        format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
    } else if bytes >= 1_048_576 {
        format!("{:.1} MB", bytes as f64 / 1_048_576.0)
    } else if bytes >= 1_024 {
        format!("{:.1} KB", bytes as f64 / 1_024.0)
    } else {
        format!("{} B", bytes)
    }
}