cargo-reclaim 0.2.1

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

use serde::Deserialize;

use super::error::ConfigError;
use super::parse::{BackgroundDocument, ConfigDocument, PlannerConfig, PolicyConfig};
use super::values::{
    parse_config_duration, parse_config_percentage_basis_points, parse_config_size,
    resolve_config_path,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReclaimConfig {
    pub version: u16,
    pub roots: Vec<PathBuf>,
    pub ignored_paths: Vec<PathBuf>,
    pub skipped_paths: Vec<PathBuf>,
    pub policy: Option<String>,
    pub whole_target: Option<WholeTargetConfig>,
    pub allow_unattended_whole_target_delete: Option<bool>,
    pub policy_thresholds: PolicyThresholdConfig,
    pub background: BackgroundConfig,
    pub scanner: ScannerConfig,
    pub scheduler: SchedulerConfig,
    pub recent_write_keep_window: Option<Duration>,
    pub keep_size_bytes: Option<u64>,
    pub keep_rustc_hashes: Vec<u64>,
    pub keep_installed_toolchains: bool,
    pub keep_toolchains: Vec<String>,
}

impl ReclaimConfig {
    pub(super) fn from_document(
        document: ConfigDocument,
        relative_base: Option<&Path>,
    ) -> Result<Self, ConfigError> {
        let ConfigDocument {
            version,
            roots,
            ignore,
            skip,
            policy,
            scanner,
            planner,
            scheduler,
            background,
        } = document;
        if version != 1 {
            return Err(ConfigError::UnsupportedVersion(version));
        }

        let policy_keep_recent_projects = policy
            .as_ref()
            .and_then(|policy| policy.keep_recent_projects.as_deref())
            .map(parse_config_duration)
            .transpose()?;
        let PlannerConfig {
            recent_write_keep_window,
            keep_days,
            keep_size,
            keep_rustc_hashes,
            keep_installed_toolchains,
            keep_toolchains,
        } = planner.unwrap_or_default();
        let planner_recent_write_keep_window = recent_write_keep_window
            .as_deref()
            .map(parse_config_duration)
            .transpose()?;
        let planner_keep_days_window = keep_days
            .map(|days| {
                if days == 0 {
                    return Err(ConfigError::InvalidDuration(days.to_string()));
                }
                Ok(Duration::from_secs(days.saturating_mul(24 * 60 * 60)))
            })
            .transpose()?;
        let planner_keep_size_bytes = keep_size.as_deref().map(parse_config_size).transpose()?;
        let policy_thresholds = policy
            .as_ref()
            .map(PolicyThresholdConfig::from_document)
            .transpose()?
            .unwrap_or_default();
        let background = background
            .map(BackgroundConfig::from_document)
            .transpose()?
            .unwrap_or_default();

        Ok(Self {
            version,
            roots: roots
                .into_iter()
                .map(|path| resolve_config_path(path, relative_base))
                .collect(),
            ignored_paths: ignore
                .into_iter()
                .map(|path| resolve_config_path(path, relative_base))
                .collect(),
            skipped_paths: skip
                .into_iter()
                .map(|path| resolve_config_path(path, relative_base))
                .collect(),
            policy: policy.as_ref().and_then(|policy| policy.mode.clone()),
            whole_target: policy
                .as_ref()
                .and_then(|policy| policy.whole_target.as_deref())
                .map(WholeTargetConfig::parse)
                .transpose()?,
            allow_unattended_whole_target_delete: policy
                .as_ref()
                .and_then(|policy| policy.allow_unattended_whole_target_delete),
            policy_thresholds,
            background,
            scanner: scanner.unwrap_or_default(),
            scheduler: scheduler.unwrap_or_default().resolve_paths(relative_base),
            recent_write_keep_window: planner_recent_write_keep_window
                .or(planner_keep_days_window)
                .or(policy_keep_recent_projects),
            keep_size_bytes: planner_keep_size_bytes,
            keep_rustc_hashes,
            keep_installed_toolchains,
            keep_toolchains,
        })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WholeTargetConfig {
    Off,
    Confirm,
    Delete,
}

impl WholeTargetConfig {
    fn parse(value: &str) -> Result<Self, ConfigError> {
        match value {
            "off" => Ok(Self::Off),
            "confirm" => Ok(Self::Confirm),
            "delete" => Ok(Self::Delete),
            _ => Err(ConfigError::InvalidWholeTargetMode(value.to_string())),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PolicyThresholdConfig {
    pub max_target_size_bytes: Option<u64>,
    pub target_size_goal_bytes: Option<u64>,
    pub unattended_allowed: Option<bool>,
}

impl PolicyThresholdConfig {
    fn from_document(document: &PolicyConfig) -> Result<Self, ConfigError> {
        Ok(Self {
            max_target_size_bytes: document
                .max_target_size
                .as_deref()
                .map(parse_config_size)
                .transpose()?,
            target_size_goal_bytes: document
                .target_size_goal
                .as_deref()
                .map(parse_config_size)
                .transpose()?,
            unattended_allowed: document.unattended_allowed,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BackgroundConfig {
    pub enabled: Option<bool>,
    pub mode: Option<BackgroundMode>,
    pub check_every: Option<Duration>,
    pub only_when_disk_free_below_basis_points: Option<u16>,
    pub min_free_disk_bytes: Option<u64>,
    pub target_free_disk_bytes: Option<u64>,
}

impl BackgroundConfig {
    fn from_document(document: BackgroundDocument) -> Result<Self, ConfigError> {
        Ok(Self {
            enabled: document.enabled,
            mode: document.mode.map(BackgroundMode::parse).transpose()?,
            check_every: document
                .check_every
                .as_deref()
                .map(parse_config_duration)
                .transpose()?,
            only_when_disk_free_below_basis_points: document
                .only_when_disk_free_below
                .as_deref()
                .map(parse_config_percentage_basis_points)
                .transpose()?,
            min_free_disk_bytes: document
                .min_free_disk
                .as_deref()
                .map(parse_config_size)
                .transpose()?,
            target_free_disk_bytes: document
                .target_free_disk
                .as_deref()
                .map(parse_config_size)
                .transpose()?,
        })
    }
}

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

impl BackgroundMode {
    fn parse(value: String) -> Result<Self, ConfigError> {
        match value.as_str() {
            "periodic" => Ok(Self::Periodic),
            "threshold" => Ok(Self::Threshold),
            _ => Err(ConfigError::InvalidBackgroundMode(value)),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub struct ScannerConfig {
    pub follow_symlinks: Option<bool>,
    pub allow_name_only_targets: Option<bool>,
    pub cross_filesystems: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
pub struct SchedulerConfig {
    pub name: Option<String>,
    pub at: Option<String>,
    pub mode: Option<String>,
    pub policy: Option<String>,
    pub allow_unattended_cleanup: Option<bool>,
    pub allow_unattended_high_policy: Option<bool>,
    pub state_dir: Option<PathBuf>,
    pub log_dir: Option<PathBuf>,
}

impl SchedulerConfig {
    pub(super) fn resolve_paths(mut self, relative_base: Option<&Path>) -> Self {
        self.state_dir = self
            .state_dir
            .map(|path| resolve_config_path(path, relative_base));
        self.log_dir = self
            .log_dir
            .map(|path| resolve_config_path(path, relative_base));
        self
    }
}