use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::broker::protocol::{CacheManifest, CacheRoot, CacheRootKind, StorageDisposition};
pub mod instances;
pub mod list;
pub mod prune;
pub mod uninstall;
pub mod verify_artifacts;
pub mod verify_basic;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CleanupAction {
pub service_name: String,
pub service_version: String,
pub path: PathBuf,
pub reason: String,
pub deleted: bool,
pub skipped: bool,
pub skip_reason: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum CleanupError {
#[error(transparent)]
Manifest(#[from] crate::broker::manifest::ManifestError),
#[error("cleanup I/O failed: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
User(String),
}
pub fn now_unix_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub fn parse_duration_secs(input: &str) -> Result<u64, CleanupError> {
if input.is_empty() {
return Err(CleanupError::User("duration must not be empty".into()));
}
let (digits, suffix) = input.split_at(input.len() - 1);
let value: u64 = digits
.parse()
.map_err(|_| CleanupError::User(format!("invalid duration: {input}")))?;
match suffix {
"d" => Ok(value * 24 * 60 * 60),
"h" => Ok(value * 60 * 60),
"m" => Ok(value * 60),
"s" => Ok(value),
_ => Err(CleanupError::User(format!(
"duration must end with d, h, m, or s: {input}"
))),
}
}
pub(crate) fn root_disposition(root: &CacheRoot) -> i32 {
root.disposition
}
pub(crate) fn root_kind(root: &CacheRoot) -> i32 {
root.kind
}
pub(crate) fn root_is_config(root: &CacheRoot) -> bool {
root_kind(root) == CacheRootKind::CacheConfig as i32
}
pub(crate) fn root_is_prunable(root: &CacheRoot) -> bool {
!matches!(
root_disposition(root),
x if x == StorageDisposition::NeverPrune as i32
|| x == StorageDisposition::PreserveAcrossUninstall as i32
)
}
pub(crate) fn delete_path(path: &Path) -> Result<(), CleanupError> {
match std::fs::symlink_metadata(path) {
Ok(meta) if meta.is_dir() => std::fs::remove_dir_all(path)?,
Ok(_) => std::fs::remove_file(path)?,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(CleanupError::Io(err)),
}
Ok(())
}
pub(crate) fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
pub(crate) fn manifest_json(manifest: &CacheManifest) -> String {
let roots = manifest
.roots
.iter()
.map(|root| {
format!(
"{{\"path\":\"{}\",\"kind\":{},\"disposition\":{},\"estimated_size_bytes\":{}}}",
json_escape(&root.path),
root.kind,
root.disposition,
root.estimated_size_bytes
)
})
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"service_name\":\"{}\",\"service_version\":\"{}\",\"broker_instance\":\"{}\",\"last_active_unix_ms\":{},\"roots\":[{}]}}",
json_escape(&manifest.service_name),
json_escape(&manifest.service_version),
json_escape(&manifest.broker_instance),
manifest.last_active_unix_ms,
roots
)
}
pub fn actions_json(schema_version: u32, actions: &[CleanupAction]) -> String {
let actions = actions
.iter()
.map(|action| {
format!(
"{{\"service_name\":\"{}\",\"service_version\":\"{}\",\"path\":\"{}\",\"reason\":\"{}\",\"deleted\":{},\"skipped\":{},\"skip_reason\":{}}}",
json_escape(&action.service_name),
json_escape(&action.service_version),
json_escape(&action.path.to_string_lossy()),
json_escape(&action.reason),
action.deleted,
action.skipped,
match &action.skip_reason {
Some(reason) => format!("\"{}\"", json_escape(reason)),
None => "null".to_string(),
}
)
})
.collect::<Vec<_>>()
.join(",");
format!("{{\"schema_version\":{schema_version},\"actions\":[{actions}]}}")
}