docker_wrapper/command/network/
prune.rs1use crate::command::{CommandExecutor, CommandOutput, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct NetworkPruneCommand {
11 until: Option<String>,
13 filters: HashMap<String, String>,
15 force: bool,
17 pub executor: CommandExecutor,
19}
20
21impl NetworkPruneCommand {
22 #[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 #[must_use]
35 pub fn until(mut self, timestamp: impl Into<String>) -> Self {
36 self.until = Some(timestamp.into());
37 self
38 }
39
40 #[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 #[must_use]
49 pub fn label_filter(self, label: impl Into<String>) -> Self {
50 self.filter("label", label)
51 }
52
53 #[must_use]
55 pub fn force(mut self) -> Self {
56 self.force = true;
57 self
58 }
59
60 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#[derive(Debug, Clone)]
121pub struct NetworkPruneResult {
122 pub deleted_networks: Vec<String>,
124 pub space_reclaimed: Option<u64>,
126 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 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 #[must_use]
160 pub fn is_success(&self) -> bool {
161 self.raw_output.success
162 }
163
164 #[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 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}