cargo-reclaim 0.2.0

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::path::{Path, PathBuf};

use crate::PolicyKind;

use super::model::{
    CARGO_HOME_PLAN_SCHEMA_VERSION, CargoHomeClass, CargoHomeEntry, CargoHomeError,
    CargoHomePathKind, CargoHomePlan, CargoHomePlanAction, CargoHomePlanEntry, CargoHomePlanTotals,
    CargoHomeProblem, CargoHomeReport,
};
use super::report::{CargoHomeReportRequest, build_cargo_home_report};

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CargoHomePlanRequest {
    pub cargo_home: Option<PathBuf>,
    pub policy: PolicyKind,
}

pub fn build_cargo_home_plan(
    request: CargoHomePlanRequest,
) -> Result<CargoHomePlan, CargoHomeError> {
    let report = build_cargo_home_report(CargoHomeReportRequest {
        cargo_home: request.cargo_home,
    })?;
    Ok(build_cargo_home_plan_from_report(report, request.policy))
}

pub fn build_cargo_home_plan_from_report(
    report: CargoHomeReport,
    policy: PolicyKind,
) -> CargoHomePlan {
    let entries = report
        .entries
        .iter()
        .map(|entry| plan_entry(entry, policy, &report.problems))
        .collect::<Vec<_>>();
    let totals = totals_for_entries(&entries, report.problems.len());
    CargoHomePlan {
        schema_version: CARGO_HOME_PLAN_SCHEMA_VERSION,
        input: report.input,
        policy,
        entries,
        totals,
        recommendations: report.recommendations,
        problems: report.problems,
    }
}

fn plan_entry(
    entry: &CargoHomeEntry,
    policy: PolicyKind,
    problems: &[CargoHomeProblem],
) -> CargoHomePlanEntry {
    let (action, reason) = if entry.skipped
        || entry.path_kind == CargoHomePathKind::Symlink
        || has_problem(entry, problems)
    {
        (
            CargoHomePlanAction::SkipProblem,
            skip_problem_reason(entry.path_kind),
        )
    } else if is_delete_candidate(entry.class, policy) {
        (
            CargoHomePlanAction::DeleteCandidate,
            delete_candidate_reason(entry.class, policy),
        )
    } else {
        (
            CargoHomePlanAction::Preserve,
            preserve_reason(entry.class, policy),
        )
    };

    CargoHomePlanEntry {
        path: entry.path.clone(),
        relative_path: entry.relative_path.clone(),
        class: entry.class,
        path_kind: entry.path_kind,
        size_bytes: entry.size_bytes,
        action,
        reason: reason.to_string(),
    }
}

fn has_problem(entry: &CargoHomeEntry, problems: &[CargoHomeProblem]) -> bool {
    problems
        .iter()
        .any(|problem| path_is_at_or_inside(&problem.path, &entry.path))
}

fn path_is_at_or_inside(path: &Path, root: &Path) -> bool {
    path == root || path.starts_with(root)
}

fn is_delete_candidate(class: CargoHomeClass, policy: PolicyKind) -> bool {
    match policy {
        PolicyKind::Observe | PolicyKind::Custom => false,
        PolicyKind::Conservative => matches!(class, CargoHomeClass::RegistryCache),
        PolicyKind::Balanced => matches!(
            class,
            CargoHomeClass::RegistryCache | CargoHomeClass::RegistrySource
        ),
        PolicyKind::Aggressive => is_known_cache_class(class),
    }
}

fn is_known_cache_class(class: CargoHomeClass) -> bool {
    matches!(
        class,
        CargoHomeClass::RegistryIndex
            | CargoHomeClass::RegistryCache
            | CargoHomeClass::RegistrySource
            | CargoHomeClass::GitDatabase
            | CargoHomeClass::GitCheckouts
    )
}

fn skip_problem_reason(path_kind: CargoHomePathKind) -> &'static str {
    if path_kind == CargoHomePathKind::Symlink {
        "skipped; Cargo home cleanup plan preserves symlinks"
    } else {
        "skipped; Cargo home cleanup plan preserves entries with incomplete inspection"
    }
}

fn delete_candidate_reason(class: CargoHomeClass, policy: PolicyKind) -> &'static str {
    match (policy, class) {
        (PolicyKind::Conservative, CargoHomeClass::RegistryCache) => {
            "delete candidate; conservative Cargo home policy includes registry package cache"
        }
        (PolicyKind::Balanced, CargoHomeClass::RegistryCache) => {
            "delete candidate; balanced Cargo home policy includes registry package cache"
        }
        (PolicyKind::Balanced, CargoHomeClass::RegistrySource) => {
            "delete candidate; balanced Cargo home policy includes unpacked registry sources"
        }
        (PolicyKind::Aggressive, CargoHomeClass::RegistryIndex) => {
            "delete candidate; aggressive Cargo home policy includes registry indexes"
        }
        (PolicyKind::Aggressive, CargoHomeClass::RegistryCache) => {
            "delete candidate; aggressive Cargo home policy includes registry package cache"
        }
        (PolicyKind::Aggressive, CargoHomeClass::RegistrySource) => {
            "delete candidate; aggressive Cargo home policy includes unpacked registry sources"
        }
        (PolicyKind::Aggressive, CargoHomeClass::GitDatabase) => {
            "delete candidate; aggressive Cargo home policy includes cached git databases"
        }
        (PolicyKind::Aggressive, CargoHomeClass::GitCheckouts) => {
            "delete candidate; aggressive Cargo home policy includes cached git checkouts"
        }
        _ => "delete candidate; Cargo home policy selected this cache entry",
    }
}

fn preserve_reason(class: CargoHomeClass, policy: PolicyKind) -> &'static str {
    match class {
        CargoHomeClass::Config => "preserved; Cargo home configuration is never selected",
        CargoHomeClass::Credentials => "preserved; Cargo home credentials are never selected",
        CargoHomeClass::InstalledBinaries => {
            "preserved; Cargo-installed binaries are never selected"
        }
        CargoHomeClass::InstallMetadata => "preserved; Cargo install metadata is never selected",
        CargoHomeClass::UnknownUserAuthored => {
            "preserved; unrecognized Cargo home paths may be user-authored"
        }
        _ if policy == PolicyKind::Observe => {
            "preserved; observe Cargo home policy does not select cleanup candidates"
        }
        _ if policy == PolicyKind::Custom => {
            "preserved; Cargo home custom selectors are not configured"
        }
        CargoHomeClass::RegistryIndex => {
            "preserved; selected Cargo home policy keeps registry indexes"
        }
        CargoHomeClass::RegistryCache => {
            "preserved; selected Cargo home policy keeps registry package cache"
        }
        CargoHomeClass::RegistrySource => {
            "preserved; selected Cargo home policy keeps unpacked registry sources"
        }
        CargoHomeClass::GitDatabase => {
            "preserved; selected Cargo home policy keeps cached git databases"
        }
        CargoHomeClass::GitCheckouts => {
            "preserved; selected Cargo home policy keeps cached git checkouts"
        }
    }
}

fn totals_for_entries(entries: &[CargoHomePlanEntry], problem_count: usize) -> CargoHomePlanTotals {
    let mut total_bytes = 0u64;
    let mut delete_candidate_count = 0usize;
    let mut delete_candidate_bytes = 0u64;
    let mut preserved_count = 0usize;
    let mut preserved_bytes = 0u64;
    let mut skipped_count = 0usize;

    for entry in entries {
        total_bytes = total_bytes.saturating_add(entry.size_bytes);
        match entry.action {
            CargoHomePlanAction::DeleteCandidate => {
                delete_candidate_count += 1;
                delete_candidate_bytes = delete_candidate_bytes.saturating_add(entry.size_bytes);
            }
            CargoHomePlanAction::Preserve => {
                preserved_count += 1;
                preserved_bytes = preserved_bytes.saturating_add(entry.size_bytes);
            }
            CargoHomePlanAction::SkipProblem => {
                skipped_count += 1;
            }
        }
    }

    CargoHomePlanTotals {
        entry_count: entries.len(),
        total_bytes,
        delete_candidate_count,
        delete_candidate_bytes,
        preserved_count,
        preserved_bytes,
        skipped_count,
        problem_count,
    }
}