docker_wrapper/command/
container_prune.rs1use crate::command::{CommandExecutor, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6use serde::Deserialize;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Deserialize)]
11#[serde(rename_all = "PascalCase")]
12pub struct ContainerPruneResult {
13 #[serde(default)]
15 pub containers_deleted: Vec<String>,
16
17 #[serde(default)]
19 pub space_reclaimed: u64,
20}
21
22#[derive(Debug, Clone)]
26pub struct ContainerPruneCommand {
27 force: bool,
29
30 filter: HashMap<String, String>,
32
33 pub executor: CommandExecutor,
35}
36
37impl ContainerPruneCommand {
38 #[must_use]
40 pub fn new() -> Self {
41 Self {
42 force: false,
43 filter: HashMap::new(),
44 executor: CommandExecutor::new(),
45 }
46 }
47
48 #[must_use]
50 pub fn force(mut self) -> Self {
51 self.force = true;
52 self
53 }
54
55 #[must_use]
57 pub fn filter(mut self, key: &str, value: &str) -> Self {
58 self.filter.insert(key.to_string(), value.to_string());
59 self
60 }
61
62 #[must_use]
64 pub fn until(mut self, duration: &str) -> Self {
65 self.filter
66 .insert("until".to_string(), duration.to_string());
67 self
68 }
69
70 #[must_use]
72 pub fn with_label(mut self, key: &str, value: Option<&str>) -> Self {
73 let label_filter = if let Some(val) = value {
74 format!("{key}={val}")
75 } else {
76 key.to_string()
77 };
78 self.filter.insert("label".to_string(), label_filter);
79 self
80 }
81
82 pub async fn run(&self) -> Result<ContainerPruneResult> {
88 self.execute().await
89 }
90}
91
92impl Default for ContainerPruneCommand {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98#[async_trait]
99impl DockerCommand for ContainerPruneCommand {
100 type Output = ContainerPruneResult;
101
102 fn build_command_args(&self) -> Vec<String> {
103 let mut args = vec!["container".to_string(), "prune".to_string()];
104
105 if self.force {
106 args.push("--force".to_string());
107 }
108
109 for (key, value) in &self.filter {
110 args.push("--filter".to_string());
111 if key == "label" {
112 args.push(value.clone());
113 } else {
114 args.push(format!("{key}={value}"));
115 }
116 }
117
118 args.extend(self.executor.raw_args.clone());
119 args
120 }
121
122 fn get_executor(&self) -> &CommandExecutor {
123 &self.executor
124 }
125
126 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
127 &mut self.executor
128 }
129
130 async fn execute(&self) -> Result<Self::Output> {
131 let args = self.build_command_args();
132 let command_name = args[0].clone();
133 let command_args = args[1..].to_vec();
134 let output = self
135 .executor
136 .execute_command(&command_name, command_args)
137 .await?;
138 let stdout = &output.stdout;
139
140 let mut result = ContainerPruneResult {
142 containers_deleted: Vec::new(),
143 space_reclaimed: 0,
144 };
145
146 for line in stdout.lines() {
148 if line.starts_with("Deleted Containers:") {
149 continue;
151 }
152 if line.starts_with("Total reclaimed space:") {
153 if let Some(space_str) = line.split(':').nth(1) {
155 result.space_reclaimed = parse_size(space_str.trim());
156 }
157 } else if !line.is_empty() && !line.contains("will be removed") {
158 let id = line.trim();
160 if id.len() == 12 || id.len() == 64 {
161 result.containers_deleted.push(id.to_string());
162 }
163 }
164 }
165
166 Ok(result)
167 }
168}
169
170#[allow(clippy::cast_possible_truncation)]
172#[allow(clippy::cast_sign_loss)]
173#[allow(clippy::cast_precision_loss)]
174fn parse_size(size_str: &str) -> u64 {
175 let size_str = size_str.trim();
176 let mut numeric_part = String::new();
177 let mut unit_part = String::new();
178 let mut found_dot = false;
179
180 for ch in size_str.chars() {
181 if ch.is_ascii_digit() || (ch == '.' && !found_dot) {
182 numeric_part.push(ch);
183 if ch == '.' {
184 found_dot = true;
185 }
186 } else if ch.is_ascii_alphabetic() {
187 unit_part.push(ch);
188 }
189 }
190
191 let value: f64 = numeric_part.parse().unwrap_or(0.0);
192
193 let multiplier = match unit_part.to_uppercase().as_str() {
194 "KB" | "K" => 1_024,
195 "MB" | "M" => 1_024 * 1_024,
196 "GB" | "G" => 1_024 * 1_024 * 1_024,
197 "TB" | "T" => 1_024_u64.pow(4),
198 _ => 1, };
200
201 (value * multiplier as f64) as u64
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_container_prune_builder() {
210 let cmd = ContainerPruneCommand::new()
211 .force()
212 .until("24h")
213 .with_label("temp", Some("true"));
214
215 let args = cmd.build_command_args();
216 assert_eq!(args[0], "container");
217 assert!(args.contains(&"prune".to_string()));
218 assert!(args.contains(&"--force".to_string()));
219 assert!(args.contains(&"--filter".to_string()));
220 }
221}