docker_wrapper/command/network/
prune.rs

1//! Docker network prune command implementation.
2
3use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8/// Docker network prune command builder
9#[derive(Debug, Clone)]
10pub struct NetworkPruneCommand {
11    /// Remove networks created before given timestamp
12    until: Option<String>,
13    /// Filter values
14    filters: HashMap<String, String>,
15    /// Do not prompt for confirmation
16    force: bool,
17    /// Command executor
18    pub executor: CommandExecutor,
19}
20
21impl NetworkPruneCommand {
22    /// Create a new network prune command
23    #[must_use]
24    pub fn new() -> Self {
25        Self {
26            until: None,
27            filters: HashMap::new(),
28            force: false,
29            executor: CommandExecutor::new(),
30        }
31    }
32
33    /// Remove networks created before given timestamp
34    #[must_use]
35    pub fn until(mut self, timestamp: impl Into<String>) -> Self {
36        self.until = Some(timestamp.into());
37        self
38    }
39
40    /// Add a filter
41    #[must_use]
42    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
43        self.filters.insert(key.into(), value.into());
44        self
45    }
46
47    /// Filter by label
48    #[must_use]
49    pub fn label_filter(self, label: impl Into<String>) -> Self {
50        self.filter("label", label)
51    }
52
53    /// Do not prompt for confirmation
54    #[must_use]
55    pub fn force(mut self) -> Self {
56        self.force = true;
57        self
58    }
59
60    /// Execute the command
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the Docker daemon is not running or the command fails
65    pub async fn run(&self) -> Result<NetworkPruneResult> {
66        self.execute().await.map(NetworkPruneResult::from)
67    }
68}
69
70impl Default for NetworkPruneCommand {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76#[async_trait]
77impl DockerCommand for NetworkPruneCommand {
78    type Output = CommandOutput;
79
80    fn build_command_args(&self) -> Vec<String> {
81        let mut args = vec!["network".to_string(), "prune".to_string()];
82
83        if let Some(ref until) = self.until {
84            args.push("--filter".to_string());
85            args.push(format!("until={until}"));
86        }
87
88        for (key, value) in &self.filters {
89            args.push("--filter".to_string());
90            args.push(format!("{key}={value}"));
91        }
92
93        if self.force {
94            args.push("--force".to_string());
95        }
96
97        args.extend(self.executor.raw_args.clone());
98        args
99    }
100
101    fn get_executor(&self) -> &CommandExecutor {
102        &self.executor
103    }
104
105    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
106        &mut self.executor
107    }
108
109    async fn execute(&self) -> Result<Self::Output> {
110        let args = self.build_command_args();
111        let command_name = args[0].clone();
112        let command_args = args[1..].to_vec();
113        self.executor
114            .execute_command(&command_name, command_args)
115            .await
116    }
117}
118
119/// Result from network prune command
120#[derive(Debug, Clone)]
121pub struct NetworkPruneResult {
122    /// Deleted networks
123    pub deleted_networks: Vec<String>,
124    /// Space reclaimed in bytes
125    pub space_reclaimed: Option<u64>,
126    /// Raw command output
127    pub raw_output: CommandOutput,
128}
129
130impl From<CommandOutput> for NetworkPruneResult {
131    fn from(output: CommandOutput) -> Self {
132        let mut deleted_networks = Vec::new();
133        let mut space_reclaimed = None;
134
135        for line in output.stdout.lines() {
136            if line.starts_with("Deleted Networks:") {
137                continue;
138            }
139            if line.contains("Total reclaimed space:") {
140                // Parse space from line like "Total reclaimed space: 1.234MB"
141                if let Some(space_str) = line.split(':').nth(1) {
142                    space_reclaimed = parse_size(space_str.trim());
143                }
144            } else if !line.trim().is_empty() && !line.contains("WARNING") {
145                deleted_networks.push(line.trim().to_string());
146            }
147        }
148
149        Self {
150            deleted_networks,
151            space_reclaimed,
152            raw_output: output,
153        }
154    }
155}
156
157impl NetworkPruneResult {
158    /// Check if the command was successful
159    #[must_use]
160    pub fn is_success(&self) -> bool {
161        self.raw_output.success
162    }
163
164    /// Get count of deleted networks
165    #[must_use]
166    pub fn count(&self) -> usize {
167        self.deleted_networks.len()
168    }
169}
170
171fn parse_size(size_str: &str) -> Option<u64> {
172    // Simple parser for Docker size strings (e.g., "1.234MB", "567KB", "2GB")
173    let size_str = size_str.trim();
174    if size_str == "0B" {
175        return Some(0);
176    }
177
178    let (num_part, unit_part) = size_str.split_at(
179        size_str
180            .rfind(|c: char| c.is_ascii_digit() || c == '.')
181            .map_or(0, |i| i + 1),
182    );
183
184    let number: f64 = num_part.parse().ok()?;
185    let multiplier = match unit_part.to_uppercase().as_str() {
186        "B" => 1,
187        "KB" => 1_000,
188        "MB" => 1_000_000,
189        "GB" => 1_000_000_000,
190        _ => return None,
191    };
192
193    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
194    Some((number * f64::from(multiplier)) as u64)
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_network_prune_basic() {
203        let cmd = NetworkPruneCommand::new();
204        let args = cmd.build_command_args();
205        assert_eq!(args, vec!["network", "prune"]);
206    }
207
208    #[test]
209    fn test_network_prune_force() {
210        let cmd = NetworkPruneCommand::new().force();
211        let args = cmd.build_command_args();
212        assert_eq!(args, vec!["network", "prune", "--force"]);
213    }
214
215    #[test]
216    fn test_network_prune_with_filters() {
217        let cmd = NetworkPruneCommand::new()
218            .until("24h")
219            .label_filter("env=test");
220        let args = cmd.build_command_args();
221        assert!(args.contains(&"--filter".to_string()));
222        assert!(args.iter().any(|a| a.contains("until=24h")));
223        assert!(args.iter().any(|a| a.contains("label=env=test")));
224    }
225
226    #[test]
227    fn test_parse_size() {
228        assert_eq!(parse_size("0B"), Some(0));
229        assert_eq!(parse_size("100B"), Some(100));
230        assert_eq!(parse_size("1.5KB"), Some(1_500));
231        assert_eq!(parse_size("2MB"), Some(2_000_000));
232        assert_eq!(parse_size("1.234GB"), Some(1_234_000_000));
233    }
234}