use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct NetworkPruneCommand {
until: Option<String>,
filters: HashMap<String, String>,
force: bool,
pub executor: CommandExecutor,
}
impl NetworkPruneCommand {
#[must_use]
pub fn new() -> Self {
Self {
until: None,
filters: HashMap::new(),
force: false,
executor: CommandExecutor::new(),
}
}
#[must_use]
pub fn until(mut self, timestamp: impl Into<String>) -> Self {
self.until = Some(timestamp.into());
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 label_filter(self, label: impl Into<String>) -> Self {
self.filter("label", label)
}
#[must_use]
pub fn force(mut self) -> Self {
self.force = true;
self
}
pub async fn run(&self) -> Result<NetworkPruneResult> {
self.execute().await.map(NetworkPruneResult::from)
}
}
impl Default for NetworkPruneCommand {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl DockerCommand for NetworkPruneCommand {
type Output = CommandOutput;
fn build_command_args(&self) -> Vec<String> {
let mut args = vec!["network".to_string(), "prune".to_string()];
if let Some(ref until) = self.until {
args.push("--filter".to_string());
args.push(format!("until={until}"));
}
for (key, value) in &self.filters {
args.push("--filter".to_string());
args.push(format!("{key}={value}"));
}
if self.force {
args.push("--force".to_string());
}
args.extend(self.executor.raw_args.clone());
args
}
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
async fn execute(&self) -> Result<Self::Output> {
let args = self.build_command_args();
let command_name = args[0].clone();
let command_args = args[1..].to_vec();
self.executor
.execute_command(&command_name, command_args)
.await
}
}
#[derive(Debug, Clone)]
pub struct NetworkPruneResult {
pub deleted_networks: Vec<String>,
pub space_reclaimed: Option<u64>,
pub raw_output: CommandOutput,
}
impl From<CommandOutput> for NetworkPruneResult {
fn from(output: CommandOutput) -> Self {
let mut deleted_networks = Vec::new();
let mut space_reclaimed = None;
for line in output.stdout.lines() {
if line.starts_with("Deleted Networks:") {
continue;
}
if line.contains("Total reclaimed space:") {
if let Some(space_str) = line.split(':').nth(1) {
space_reclaimed = parse_size(space_str.trim());
}
} else if !line.trim().is_empty() && !line.contains("WARNING") {
deleted_networks.push(line.trim().to_string());
}
}
Self {
deleted_networks,
space_reclaimed,
raw_output: output,
}
}
}
impl NetworkPruneResult {
#[must_use]
pub fn is_success(&self) -> bool {
self.raw_output.success
}
#[must_use]
pub fn count(&self) -> usize {
self.deleted_networks.len()
}
}
fn parse_size(size_str: &str) -> Option<u64> {
let size_str = size_str.trim();
if size_str == "0B" {
return Some(0);
}
let (num_part, unit_part) = size_str.split_at(
size_str
.rfind(|c: char| c.is_ascii_digit() || c == '.')
.map_or(0, |i| i + 1),
);
let number: f64 = num_part.parse().ok()?;
let multiplier = match unit_part.to_uppercase().as_str() {
"B" => 1,
"KB" => 1_000,
"MB" => 1_000_000,
"GB" => 1_000_000_000,
_ => return None,
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Some((number * f64::from(multiplier)) as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_prune_basic() {
let cmd = NetworkPruneCommand::new();
let args = cmd.build_command_args();
assert_eq!(args, vec!["network", "prune"]);
}
#[test]
fn test_network_prune_force() {
let cmd = NetworkPruneCommand::new().force();
let args = cmd.build_command_args();
assert_eq!(args, vec!["network", "prune", "--force"]);
}
#[test]
fn test_network_prune_with_filters() {
let cmd = NetworkPruneCommand::new()
.until("24h")
.label_filter("env=test");
let args = cmd.build_command_args();
assert!(args.contains(&"--filter".to_string()));
assert!(args.iter().any(|a| a.contains("until=24h")));
assert!(args.iter().any(|a| a.contains("label=env=test")));
}
#[test]
fn test_parse_size() {
assert_eq!(parse_size("0B"), Some(0));
assert_eq!(parse_size("100B"), Some(100));
assert_eq!(parse_size("1.5KB"), Some(1_500));
assert_eq!(parse_size("2MB"), Some(2_000_000));
assert_eq!(parse_size("1.234GB"), Some(1_234_000_000));
}
}