cargo-reclaim 0.2.0

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

use crate::policy::PolicyKind;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WatcherMode {
    Periodic,
    Threshold,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WatcherThresholds {
    pub max_target_size_bytes: Option<u64>,
    pub disk_free_below_basis_points: Option<u16>,
    pub min_free_disk_bytes: Option<u64>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatcherDecisionInput {
    pub enabled: bool,
    pub mode: WatcherMode,
    pub thresholds: WatcherThresholds,
    pub observed_targets: Vec<WatcherObservedTarget>,
    pub disk_free_basis_points: Option<u16>,
    pub disk_free_bytes: Option<u64>,
    pub selected_policy: PolicyKind,
    pub unattended_allowed: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatcherObservedTarget {
    pub path: PathBuf,
    pub size_bytes: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatcherDecision {
    pub state: WatcherDecisionState,
    pub reasons: Vec<WatcherTriggerReason>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WatcherDecisionState {
    Inactive,
    NonThresholdMode,
    NotTriggered,
    TriggeredPlanOnly,
    TriggeredPlanAndApply,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatcherTriggerReason {
    TargetSizeExceeded {
        path: PathBuf,
        size_bytes: u64,
        max_target_size_bytes: u64,
    },
    DiskFreeBelow {
        free_basis_points: u16,
        threshold_basis_points: u16,
    },
    DiskFreeBytesBelow {
        free_bytes: u64,
        min_free_disk_bytes: u64,
    },
}

pub fn decide_watcher_thresholds(input: WatcherDecisionInput) -> WatcherDecision {
    if !input.enabled {
        return WatcherDecision {
            state: WatcherDecisionState::Inactive,
            reasons: Vec::new(),
        };
    }

    if input.mode != WatcherMode::Threshold {
        return WatcherDecision {
            state: WatcherDecisionState::NonThresholdMode,
            reasons: Vec::new(),
        };
    }

    let reasons = threshold_reasons(&input);
    if reasons.is_empty() {
        return WatcherDecision {
            state: WatcherDecisionState::NotTriggered,
            reasons,
        };
    }

    let state = if input.selected_policy != PolicyKind::Observe && input.unattended_allowed {
        WatcherDecisionState::TriggeredPlanAndApply
    } else {
        WatcherDecisionState::TriggeredPlanOnly
    };

    WatcherDecision { state, reasons }
}

fn threshold_reasons(input: &WatcherDecisionInput) -> Vec<WatcherTriggerReason> {
    let mut reasons = Vec::new();

    if let Some(max_target_size_bytes) = input.thresholds.max_target_size_bytes {
        reasons.extend(
            input
                .observed_targets
                .iter()
                .filter(|target| target.size_bytes > max_target_size_bytes)
                .map(|target| WatcherTriggerReason::TargetSizeExceeded {
                    path: target.path.clone(),
                    size_bytes: target.size_bytes,
                    max_target_size_bytes,
                }),
        );
    }

    if let (Some(free_basis_points), Some(threshold_basis_points)) = (
        input.disk_free_basis_points,
        input.thresholds.disk_free_below_basis_points,
    ) && free_basis_points < threshold_basis_points
    {
        reasons.push(WatcherTriggerReason::DiskFreeBelow {
            free_basis_points,
            threshold_basis_points,
        });
    }

    if let (Some(free_bytes), Some(min_free_disk_bytes)) =
        (input.disk_free_bytes, input.thresholds.min_free_disk_bytes)
        && free_bytes < min_free_disk_bytes
    {
        reasons.push(WatcherTriggerReason::DiskFreeBytesBelow {
            free_bytes,
            min_free_disk_bytes,
        });
    }

    reasons
}