docker_wrapper/command/system/
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 PruneResult {
13 #[serde(default)]
15 pub containers_deleted: Vec<String>,
16
17 #[serde(default)]
19 pub containers_space_reclaimed: u64,
20
21 #[serde(default)]
23 pub images_deleted: Vec<String>,
24
25 #[serde(default)]
27 pub images_space_reclaimed: u64,
28
29 #[serde(default)]
31 pub networks_deleted: Vec<String>,
32
33 #[serde(default)]
35 pub volumes_deleted: Vec<String>,
36
37 #[serde(default)]
39 pub volumes_space_reclaimed: u64,
40
41 #[serde(default)]
43 pub build_cache_deleted: Vec<String>,
44
45 #[serde(default)]
47 pub space_reclaimed: u64,
48}
49
50#[derive(Debug, Clone)]
59pub struct SystemPruneCommand {
60 all: bool,
62
63 volumes: bool,
65
66 force: bool,
68
69 filter: HashMap<String, String>,
71
72 pub executor: CommandExecutor,
74}
75
76impl SystemPruneCommand {
77 #[must_use]
79 pub fn new() -> Self {
80 Self {
81 all: false,
82 volumes: false,
83 force: false,
84 filter: HashMap::new(),
85 executor: CommandExecutor::new(),
86 }
87 }
88
89 #[must_use]
91 pub fn all(mut self) -> Self {
92 self.all = true;
93 self
94 }
95
96 #[must_use]
98 pub fn volumes(mut self) -> Self {
99 self.volumes = true;
100 self
101 }
102
103 #[must_use]
105 pub fn force(mut self) -> Self {
106 self.force = true;
107 self
108 }
109
110 #[must_use]
112 pub fn filter(mut self, key: &str, value: &str) -> Self {
113 self.filter.insert(key.to_string(), value.to_string());
114 self
115 }
116
117 pub async fn run(&self) -> Result<PruneResult> {
123 self.execute().await
124 }
125}
126
127impl Default for SystemPruneCommand {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133#[async_trait]
134impl DockerCommand for SystemPruneCommand {
135 type Output = PruneResult;
136
137 fn build_command_args(&self) -> Vec<String> {
138 let mut args = vec!["system".to_string(), "prune".to_string()];
139
140 if self.all {
141 args.push("--all".to_string());
142 }
143
144 if self.volumes {
145 args.push("--volumes".to_string());
146 }
147
148 if self.force {
149 args.push("--force".to_string());
150 }
151
152 for (key, value) in &self.filter {
153 args.push("--filter".to_string());
154 args.push(format!("{key}={value}"));
155 }
156
157 args.extend(self.executor.raw_args.clone());
158 args
159 }
160
161 fn get_executor(&self) -> &CommandExecutor {
162 &self.executor
163 }
164
165 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
166 &mut self.executor
167 }
168
169 async fn execute(&self) -> Result<Self::Output> {
170 let args = self.build_command_args();
171 let command_name = args[0].clone();
172 let command_args = args[1..].to_vec();
173 let output = self
174 .executor
175 .execute_command(&command_name, command_args)
176 .await?;
177
178 let stdout = &output.stdout;
181
182 let mut result = PruneResult {
183 containers_deleted: Vec::new(),
184 containers_space_reclaimed: 0,
185 images_deleted: Vec::new(),
186 images_space_reclaimed: 0,
187 networks_deleted: Vec::new(),
188 volumes_deleted: Vec::new(),
189 volumes_space_reclaimed: 0,
190 build_cache_deleted: Vec::new(),
191 space_reclaimed: 0,
192 };
193
194 for line in stdout.lines() {
197 if line.contains("Total reclaimed space:") {
198 if let Some(space_str) = line.split(':').nth(1) {
200 result.space_reclaimed = parse_size(space_str.trim());
201 }
202 }
203 }
205
206 Ok(result)
207 }
208}
209
210#[allow(clippy::cast_possible_truncation)]
212#[allow(clippy::cast_sign_loss)]
213#[allow(clippy::cast_precision_loss)]
214fn parse_size(size_str: &str) -> u64 {
215 let size_str = size_str.trim();
217
218 let mut numeric_part = String::new();
220 let mut unit_part = String::new();
221 let mut found_dot = false;
222
223 for ch in size_str.chars() {
224 if ch.is_ascii_digit() || (ch == '.' && !found_dot) {
225 numeric_part.push(ch);
226 if ch == '.' {
227 found_dot = true;
228 }
229 } else if ch.is_ascii_alphabetic() {
230 unit_part.push(ch);
231 }
232 }
233
234 let value: f64 = numeric_part.parse().unwrap_or(0.0);
236
237 let multiplier = match unit_part.to_uppercase().as_str() {
239 "KB" | "K" => 1_024,
240 "MB" | "M" => 1_024 * 1_024,
241 "GB" | "G" => 1_024 * 1_024 * 1_024,
242 "TB" | "T" => 1_024_u64.pow(4),
243 _ => 1, };
245
246 (value * multiplier as f64) as u64
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_parse_size() {
255 assert_eq!(parse_size("100B"), 100);
256 assert_eq!(parse_size("1KB"), 1024);
257 assert_eq!(parse_size("1.5KB"), 1536);
258 assert_eq!(parse_size("2MB"), 2 * 1024 * 1024);
259 assert_eq!(parse_size("1GB"), 1024 * 1024 * 1024);
260 #[allow(clippy::cast_possible_truncation)]
261 #[allow(clippy::cast_sign_loss)]
262 {
263 assert_eq!(parse_size("1.5GB"), (1.5 * 1024.0 * 1024.0 * 1024.0) as u64);
264 }
265 }
266
267 #[test]
268 fn test_system_prune_builder() {
269 let cmd = SystemPruneCommand::new()
270 .all()
271 .volumes()
272 .force()
273 .filter("until", "24h");
274
275 let args = cmd.build_command_args();
276 assert_eq!(args[0], "system");
277 assert!(args.contains(&"prune".to_string()));
278 assert!(args.contains(&"--all".to_string()));
279 assert!(args.contains(&"--volumes".to_string()));
280 assert!(args.contains(&"--force".to_string()));
281 assert!(args.contains(&"--filter".to_string()));
282 assert!(args.contains(&"until=24h".to_string()));
283 }
284}