use crate::command::{CommandExecutor, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct BuilderPruneCommand {
all: bool,
filters: HashMap<String, String>,
force: bool,
keep_storage: Option<String>,
pub executor: CommandExecutor,
}
#[derive(Debug)]
pub struct BuilderPruneResult {
pub deleted_cache_ids: Vec<String>,
pub space_reclaimed: Option<u64>,
pub space_reclaimed_str: Option<String>,
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
impl BuilderPruneCommand {
#[must_use]
pub fn new() -> Self {
Self {
all: false,
filters: HashMap::new(),
force: false,
keep_storage: None,
executor: CommandExecutor::new(),
}
}
#[must_use]
pub fn all(mut self) -> Self {
self.all = true;
self
}
#[must_use]
pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.filters.insert(key.into(), value.into());
self
}
#[must_use]
pub fn force(mut self) -> Self {
self.force = true;
self
}
#[must_use]
pub fn keep_storage(mut self, size: impl Into<String>) -> Self {
self.keep_storage = Some(size.into());
self
}
fn parse_output(output: &str) -> (Vec<String>, Option<u64>, Option<String>) {
let mut cache_ids = Vec::new();
let mut space_reclaimed = None;
let mut space_reclaimed_str = None;
for line in output.lines() {
if line.starts_with("Deleted:") || line.starts_with("deleted:") {
if let Some(id) = line.split_whitespace().nth(1) {
cache_ids.push(id.to_string());
}
}
if line.contains("Total reclaimed space:") || line.contains("total reclaimed space:") {
space_reclaimed_str = line.split(':').nth(1).map(|s| s.trim().to_string());
if let Some(size_str) = &space_reclaimed_str {
space_reclaimed = parse_size(size_str);
}
}
}
(cache_ids, space_reclaimed, space_reclaimed_str)
}
}
impl Default for BuilderPruneCommand {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl DockerCommand for BuilderPruneCommand {
type Output = BuilderPruneResult;
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
fn build_command_args(&self) -> Vec<String> {
let mut args = vec!["builder".to_string(), "prune".to_string()];
if self.all {
args.push("--all".to_string());
}
for (key, value) in &self.filters {
args.push("--filter".to_string());
args.push(format!("{key}={value}"));
}
if self.force {
args.push("--force".to_string());
}
if let Some(storage) = &self.keep_storage {
args.push("--keep-storage".to_string());
args.push(storage.clone());
}
args.extend(self.executor.raw_args.clone());
args
}
async fn execute(&self) -> Result<Self::Output> {
let args = self.build_command_args();
let output = self.execute_command(args).await?;
let (deleted_cache_ids, space_reclaimed, space_reclaimed_str) =
Self::parse_output(&output.stdout);
Ok(BuilderPruneResult {
deleted_cache_ids,
space_reclaimed,
space_reclaimed_str,
stdout: output.stdout,
stderr: output.stderr,
exit_code: output.exit_code,
})
}
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
fn parse_size(size_str: &str) -> Option<u64> {
let size_str = size_str.trim();
let (num_str, unit) = if let Some(pos) = size_str.find(|c: char| c.is_alphabetic()) {
(&size_str[..pos], &size_str[pos..])
} else {
return size_str.parse().ok();
};
let number: f64 = num_str.trim().parse().ok()?;
let multiplier = match unit.to_uppercase().as_str() {
"B" | "" => 1.0,
"KB" | "K" => 1024.0,
"MB" | "M" => 1024.0 * 1024.0,
"GB" | "G" => 1024.0 * 1024.0 * 1024.0,
"TB" | "T" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
_ => return None,
};
if number.is_sign_negative() || !number.is_finite() {
return None;
}
let result = (number * multiplier).round();
if result > u64::MAX as f64 {
return None;
}
Some(result as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_prune_basic() {
let cmd = BuilderPruneCommand::new();
let args = cmd.build_command_args();
assert_eq!(args, vec!["builder", "prune"]);
}
#[test]
fn test_builder_prune_all_options() {
let cmd = BuilderPruneCommand::new()
.all()
.filter("until", "24h")
.force()
.keep_storage("5GB");
let args = cmd.build_command_args();
assert!(args.contains(&"--all".to_string()));
assert!(args.contains(&"--filter".to_string()));
assert!(args.contains(&"until=24h".to_string()));
assert!(args.contains(&"--force".to_string()));
assert!(args.contains(&"--keep-storage".to_string()));
assert!(args.contains(&"5GB".to_string()));
}
#[test]
fn test_parse_size() {
assert_eq!(parse_size("100"), Some(100));
assert_eq!(parse_size("1KB"), Some(1024));
assert_eq!(parse_size("1.5KB"), Some(1536));
assert_eq!(parse_size("2MB"), Some(2 * 1024 * 1024));
assert_eq!(parse_size("1GB"), Some(1024 * 1024 * 1024));
assert_eq!(parse_size("2.5GB"), Some(2_684_354_560));
assert_eq!(parse_size("1TB"), Some(1_099_511_627_776));
}
#[test]
fn test_parse_output() {
let output = r"Deleted: sha256:abc123
Deleted: sha256:def456
Total reclaimed space: 2.5GB";
let (ids, bytes, str_val) = BuilderPruneCommand::parse_output(output);
assert_eq!(ids.len(), 2);
assert!(ids.contains(&"sha256:abc123".to_string()));
assert!(ids.contains(&"sha256:def456".to_string()));
assert_eq!(bytes, Some(2_684_354_560));
assert_eq!(str_val, Some("2.5GB".to_string()));
}
#[test]
fn test_builder_prune_extensibility() {
let mut cmd = BuilderPruneCommand::new();
cmd.get_executor_mut()
.raw_args
.push("--custom-flag".to_string());
let args = cmd.build_command_args();
assert!(args.contains(&"--custom-flag".to_string()));
}
}