1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5use crate::TargetStatus;
6use crate::plan::{CleanupTarget, CleanupTargetDeletionStyle};
7
8const EXECUTION_TARGET_SHADOWED_REASON: &str = "execution-target-shadowed";
9
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ExecutionReport {
12 #[serde(default)]
13 pub dry_run: bool,
14 pub summary: ExecutionSummary,
15 #[serde(default, skip_serializing_if = "Vec::is_empty")]
16 pub actions: Vec<ExecutionActionReport>,
17 #[serde(default, skip_serializing_if = "Vec::is_empty")]
18 pub warnings: Vec<ExecutionWarning>,
19}
20
21impl ExecutionReport {
22 pub fn dry_run() -> Self {
23 Self {
24 dry_run: true,
25 ..Self::default()
26 }
27 }
28
29 pub fn from_targets(targets: &[CleanupTarget]) -> Self {
30 let actions = targets
31 .iter()
32 .enumerate()
33 .filter(|(_, target)| target.status != TargetStatus::Allowed)
34 .map(|(index, target)| ExecutionActionReport::from_target(index, target))
35 .collect::<Vec<_>>();
36
37 Self::from_actions(actions)
38 }
39
40 pub fn from_actions(actions: Vec<ExecutionActionReport>) -> Self {
41 Self::from_actions_with_dry_run(actions, false)
42 }
43
44 pub fn from_actions_with_dry_run(actions: Vec<ExecutionActionReport>, dry_run: bool) -> Self {
45 let summary = ExecutionSummary::from_actions(&actions);
46 Self {
47 dry_run,
48 summary,
49 actions,
50 warnings: Vec::new(),
51 }
52 }
53
54 pub fn push_warning(&mut self, warning: ExecutionWarning) {
55 self.warnings.push(warning);
56 }
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
60pub struct ExecutionSummary {
61 pub total_actions: usize,
62 pub completed_actions: usize,
63 pub skipped_actions: usize,
64 pub blocked_actions: usize,
65 pub failed_actions: usize,
66 pub estimated_bytes: u64,
67 pub confirmed_reclaimed_bytes: u64,
68 pub pending_reclaim_bytes: u64,
69 pub skipped_bytes: u64,
70 pub failed_bytes: u64,
71 pub shadowed_bytes: u64,
72}
73
74impl ExecutionSummary {
75 pub fn from_actions(actions: &[ExecutionActionReport]) -> Self {
76 let mut summary = Self {
77 total_actions: actions.len(),
78 ..Self::default()
79 };
80
81 for action in actions {
82 summary.estimated_bytes = summary
83 .estimated_bytes
84 .saturating_add(action.estimated_bytes);
85 summary.confirmed_reclaimed_bytes = summary
86 .confirmed_reclaimed_bytes
87 .saturating_add(action.confirmed_reclaimed_bytes);
88 summary.pending_reclaim_bytes = summary
89 .pending_reclaim_bytes
90 .saturating_add(action.pending_reclaim_bytes);
91
92 match action.status {
93 TargetStatus::Completed => summary.completed_actions += 1,
94 TargetStatus::Skipped => {
95 summary.skipped_actions += 1;
96 summary.skipped_bytes =
97 summary.skipped_bytes.saturating_add(action.estimated_bytes);
98 if action.reason_code.as_deref() == Some(EXECUTION_TARGET_SHADOWED_REASON) {
99 summary.shadowed_bytes = summary
100 .shadowed_bytes
101 .saturating_add(action.estimated_bytes);
102 }
103 }
104 TargetStatus::Blocked => {
105 summary.blocked_actions += 1;
106 summary.skipped_bytes =
107 summary.skipped_bytes.saturating_add(action.estimated_bytes);
108 }
109 TargetStatus::Failed => {
110 summary.failed_actions += 1;
111 summary.failed_bytes =
112 summary.failed_bytes.saturating_add(action.estimated_bytes);
113 }
114 TargetStatus::Allowed => {}
115 }
116 }
117
118 summary
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct ExecutionActionReport {
124 pub target_index: usize,
125 pub rule_id: String,
126 pub path: PathBuf,
127 pub deletion_style: CleanupTargetDeletionStyle,
128 pub estimated_bytes: u64,
129 pub status: TargetStatus,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub reason: Option<String>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub reason_code: Option<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub attempted_paths: Vec<PathBuf>,
136 pub confirmed_reclaimed_bytes: u64,
137 pub pending_reclaim_bytes: u64,
138}
139
140impl ExecutionActionReport {
141 pub fn from_target(target_index: usize, target: &CleanupTarget) -> Self {
142 let attempted_paths = match target.status {
143 TargetStatus::Completed | TargetStatus::Failed => vec![target.path.clone()],
144 TargetStatus::Allowed | TargetStatus::Skipped | TargetStatus::Blocked => Vec::new(),
145 };
146
147 Self {
148 target_index,
149 rule_id: target.rule_id.clone(),
150 path: target.path.clone(),
151 deletion_style: target.deletion_style,
152 estimated_bytes: target.estimated_bytes,
153 status: target.status,
154 reason: target.reason.clone(),
155 reason_code: target.reason_code.map(|reason| reason.label().to_string()),
156 attempted_paths,
157 confirmed_reclaimed_bytes: target.freed_bytes,
158 pending_reclaim_bytes: target.pending_reclaim_bytes,
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct ExecutionWarning {
165 pub kind: ExecutionWarningKind,
166 pub message: String,
167}
168
169impl ExecutionWarning {
170 pub fn history_write_failed(message: impl Into<String>) -> Self {
171 Self {
172 kind: ExecutionWarningKind::HistoryWriteFailed,
173 message: message.into(),
174 }
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "kebab-case")]
180pub enum ExecutionWarningKind {
181 HistoryWriteFailed,
182}