running_process/cleanup/
mod.rs1use std::path::{Path, PathBuf};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use crate::broker::protocol::{CacheManifest, CacheRoot, CacheRootKind, StorageDisposition};
11
12pub mod instances;
14pub mod list;
16pub mod prune;
18pub mod uninstall;
20pub mod verify_basic;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct CleanupAction {
26 pub service_name: String,
28 pub service_version: String,
30 pub path: PathBuf,
32 pub reason: String,
34 pub deleted: bool,
36 pub skipped: bool,
38 pub skip_reason: Option<String>,
40}
41
42#[derive(Debug, thiserror::Error)]
44pub enum CleanupError {
45 #[error(transparent)]
47 Manifest(#[from] crate::broker::manifest::ManifestError),
48 #[error("cleanup I/O failed: {0}")]
50 Io(#[from] std::io::Error),
51 #[error("{0}")]
53 User(String),
54}
55
56pub fn now_unix_ms() -> u64 {
58 SystemTime::now()
59 .duration_since(UNIX_EPOCH)
60 .map(|d| d.as_millis() as u64)
61 .unwrap_or(0)
62}
63
64pub fn parse_duration_secs(input: &str) -> Result<u64, CleanupError> {
66 if input.is_empty() {
67 return Err(CleanupError::User("duration must not be empty".into()));
68 }
69 let (digits, suffix) = input.split_at(input.len() - 1);
70 let value: u64 = digits
71 .parse()
72 .map_err(|_| CleanupError::User(format!("invalid duration: {input}")))?;
73 match suffix {
74 "d" => Ok(value * 24 * 60 * 60),
75 "h" => Ok(value * 60 * 60),
76 "m" => Ok(value * 60),
77 "s" => Ok(value),
78 _ => Err(CleanupError::User(format!(
79 "duration must end with d, h, m, or s: {input}"
80 ))),
81 }
82}
83
84pub(crate) fn root_disposition(root: &CacheRoot) -> i32 {
85 root.disposition
86}
87
88pub(crate) fn root_kind(root: &CacheRoot) -> i32 {
89 root.kind
90}
91
92pub(crate) fn root_is_config(root: &CacheRoot) -> bool {
93 root_kind(root) == CacheRootKind::CacheConfig as i32
94}
95
96pub(crate) fn root_is_prunable(root: &CacheRoot) -> bool {
97 !matches!(
98 root_disposition(root),
99 x if x == StorageDisposition::NeverPrune as i32
100 || x == StorageDisposition::PreserveAcrossUninstall as i32
101 )
102}
103
104pub(crate) fn delete_path(path: &Path) -> Result<(), CleanupError> {
105 match std::fs::symlink_metadata(path) {
106 Ok(meta) if meta.is_dir() => std::fs::remove_dir_all(path)?,
107 Ok(_) => std::fs::remove_file(path)?,
108 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
109 Err(err) => return Err(CleanupError::Io(err)),
110 }
111 Ok(())
112}
113
114pub(crate) fn json_escape(s: &str) -> String {
115 let mut out = String::with_capacity(s.len());
116 for c in s.chars() {
117 match c {
118 '"' => out.push_str("\\\""),
119 '\\' => out.push_str("\\\\"),
120 '\n' => out.push_str("\\n"),
121 '\r' => out.push_str("\\r"),
122 '\t' => out.push_str("\\t"),
123 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
124 c => out.push(c),
125 }
126 }
127 out
128}
129
130pub(crate) fn manifest_json(manifest: &CacheManifest) -> String {
131 let roots = manifest
132 .roots
133 .iter()
134 .map(|root| {
135 format!(
136 "{{\"path\":\"{}\",\"kind\":{},\"disposition\":{},\"estimated_size_bytes\":{}}}",
137 json_escape(&root.path),
138 root.kind,
139 root.disposition,
140 root.estimated_size_bytes
141 )
142 })
143 .collect::<Vec<_>>()
144 .join(",");
145 format!(
146 "{{\"service_name\":\"{}\",\"service_version\":\"{}\",\"broker_instance\":\"{}\",\"last_active_unix_ms\":{},\"roots\":[{}]}}",
147 json_escape(&manifest.service_name),
148 json_escape(&manifest.service_version),
149 json_escape(&manifest.broker_instance),
150 manifest.last_active_unix_ms,
151 roots
152 )
153}
154
155pub fn actions_json(schema_version: u32, actions: &[CleanupAction]) -> String {
157 let actions = actions
158 .iter()
159 .map(|action| {
160 format!(
161 "{{\"service_name\":\"{}\",\"service_version\":\"{}\",\"path\":\"{}\",\"reason\":\"{}\",\"deleted\":{},\"skipped\":{},\"skip_reason\":{}}}",
162 json_escape(&action.service_name),
163 json_escape(&action.service_version),
164 json_escape(&action.path.to_string_lossy()),
165 json_escape(&action.reason),
166 action.deleted,
167 action.skipped,
168 match &action.skip_reason {
169 Some(reason) => format!("\"{}\"", json_escape(reason)),
170 None => "null".to_string(),
171 }
172 )
173 })
174 .collect::<Vec<_>>()
175 .join(",");
176 format!("{{\"schema_version\":{schema_version},\"actions\":[{actions}]}}")
177}