rebecca-core 0.3.0

Core planning, safety, scanning, and history models for Rebecca.
Documentation
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::TargetStatus;
use crate::plan::{CleanupTarget, CleanupTargetDeletionStyle};

const EXECUTION_TARGET_SHADOWED_REASON: &str = "execution-target-shadowed";

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionReport {
    #[serde(default)]
    pub dry_run: bool,
    pub summary: ExecutionSummary,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<ExecutionActionReport>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<ExecutionWarning>,
}

impl ExecutionReport {
    pub fn dry_run() -> Self {
        Self {
            dry_run: true,
            ..Self::default()
        }
    }

    pub fn from_targets(targets: &[CleanupTarget]) -> Self {
        let actions = targets
            .iter()
            .enumerate()
            .filter(|(_, target)| target.status != TargetStatus::Allowed)
            .map(|(index, target)| ExecutionActionReport::from_target(index, target))
            .collect::<Vec<_>>();

        Self::from_actions(actions)
    }

    pub fn from_actions(actions: Vec<ExecutionActionReport>) -> Self {
        Self::from_actions_with_dry_run(actions, false)
    }

    pub fn from_actions_with_dry_run(actions: Vec<ExecutionActionReport>, dry_run: bool) -> Self {
        let summary = ExecutionSummary::from_actions(&actions);
        Self {
            dry_run,
            summary,
            actions,
            warnings: Vec::new(),
        }
    }

    pub fn push_warning(&mut self, warning: ExecutionWarning) {
        self.warnings.push(warning);
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionSummary {
    pub total_actions: usize,
    pub completed_actions: usize,
    pub skipped_actions: usize,
    pub blocked_actions: usize,
    pub failed_actions: usize,
    pub estimated_bytes: u64,
    pub confirmed_reclaimed_bytes: u64,
    pub pending_reclaim_bytes: u64,
    pub skipped_bytes: u64,
    pub failed_bytes: u64,
    pub shadowed_bytes: u64,
}

impl ExecutionSummary {
    pub fn from_actions(actions: &[ExecutionActionReport]) -> Self {
        let mut summary = Self {
            total_actions: actions.len(),
            ..Self::default()
        };

        for action in actions {
            summary.estimated_bytes = summary
                .estimated_bytes
                .saturating_add(action.estimated_bytes);
            summary.confirmed_reclaimed_bytes = summary
                .confirmed_reclaimed_bytes
                .saturating_add(action.confirmed_reclaimed_bytes);
            summary.pending_reclaim_bytes = summary
                .pending_reclaim_bytes
                .saturating_add(action.pending_reclaim_bytes);

            match action.status {
                TargetStatus::Completed => summary.completed_actions += 1,
                TargetStatus::Skipped => {
                    summary.skipped_actions += 1;
                    summary.skipped_bytes =
                        summary.skipped_bytes.saturating_add(action.estimated_bytes);
                    if action.reason_code.as_deref() == Some(EXECUTION_TARGET_SHADOWED_REASON) {
                        summary.shadowed_bytes = summary
                            .shadowed_bytes
                            .saturating_add(action.estimated_bytes);
                    }
                }
                TargetStatus::Blocked => {
                    summary.blocked_actions += 1;
                    summary.skipped_bytes =
                        summary.skipped_bytes.saturating_add(action.estimated_bytes);
                }
                TargetStatus::Failed => {
                    summary.failed_actions += 1;
                    summary.failed_bytes =
                        summary.failed_bytes.saturating_add(action.estimated_bytes);
                }
                TargetStatus::Allowed => {}
            }
        }

        summary
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionActionReport {
    pub target_index: usize,
    pub rule_id: String,
    pub path: PathBuf,
    pub deletion_style: CleanupTargetDeletionStyle,
    pub estimated_bytes: u64,
    pub status: TargetStatus,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason_code: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub attempted_paths: Vec<PathBuf>,
    pub confirmed_reclaimed_bytes: u64,
    pub pending_reclaim_bytes: u64,
}

impl ExecutionActionReport {
    pub fn from_target(target_index: usize, target: &CleanupTarget) -> Self {
        let attempted_paths = match target.status {
            TargetStatus::Completed | TargetStatus::Failed => vec![target.path.clone()],
            TargetStatus::Allowed | TargetStatus::Skipped | TargetStatus::Blocked => Vec::new(),
        };

        Self {
            target_index,
            rule_id: target.rule_id.clone(),
            path: target.path.clone(),
            deletion_style: target.deletion_style,
            estimated_bytes: target.estimated_bytes,
            status: target.status,
            reason: target.reason.clone(),
            reason_code: target.reason_code.map(|reason| reason.label().to_string()),
            attempted_paths,
            confirmed_reclaimed_bytes: target.freed_bytes,
            pending_reclaim_bytes: target.pending_reclaim_bytes,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionWarning {
    pub kind: ExecutionWarningKind,
    pub message: String,
}

impl ExecutionWarning {
    pub fn history_write_failed(message: impl Into<String>) -> Self {
        Self {
            kind: ExecutionWarningKind::HistoryWriteFailed,
            message: message.into(),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExecutionWarningKind {
    HistoryWriteFailed,
}