docker_wrapper/command/builder/
prune.rs1use crate::command::{CommandExecutor, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
30pub struct BuilderPruneCommand {
31 all: bool,
33 filters: HashMap<String, String>,
35 force: bool,
37 keep_storage: Option<String>,
39 pub executor: CommandExecutor,
41}
42
43#[derive(Debug)]
45pub struct BuilderPruneResult {
46 pub deleted_cache_ids: Vec<String>,
48 pub space_reclaimed: Option<u64>,
50 pub space_reclaimed_str: Option<String>,
52 pub stdout: String,
54 pub stderr: String,
56 pub exit_code: i32,
58}
59
60impl BuilderPruneCommand {
61 #[must_use]
63 pub fn new() -> Self {
64 Self {
65 all: false,
66 filters: HashMap::new(),
67 force: false,
68 keep_storage: None,
69 executor: CommandExecutor::new(),
70 }
71 }
72
73 #[must_use]
75 pub fn all(mut self) -> Self {
76 self.all = true;
77 self
78 }
79
80 #[must_use]
86 pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
87 self.filters.insert(key.into(), value.into());
88 self
89 }
90
91 #[must_use]
93 pub fn force(mut self) -> Self {
94 self.force = true;
95 self
96 }
97
98 #[must_use]
113 pub fn keep_storage(mut self, size: impl Into<String>) -> Self {
114 self.keep_storage = Some(size.into());
115 self
116 }
117
118 fn parse_output(output: &str) -> (Vec<String>, Option<u64>, Option<String>) {
120 let mut cache_ids = Vec::new();
121 let mut space_reclaimed = None;
122 let mut space_reclaimed_str = None;
123
124 for line in output.lines() {
125 if line.starts_with("Deleted:") || line.starts_with("deleted:") {
127 if let Some(id) = line.split_whitespace().nth(1) {
128 cache_ids.push(id.to_string());
129 }
130 }
131
132 if line.contains("Total reclaimed space:") || line.contains("total reclaimed space:") {
134 space_reclaimed_str = line.split(':').nth(1).map(|s| s.trim().to_string());
135
136 if let Some(size_str) = &space_reclaimed_str {
138 space_reclaimed = parse_size(size_str);
139 }
140 }
141 }
142
143 (cache_ids, space_reclaimed, space_reclaimed_str)
144 }
145}
146
147impl Default for BuilderPruneCommand {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153#[async_trait]
154impl DockerCommand for BuilderPruneCommand {
155 type Output = BuilderPruneResult;
156
157 fn get_executor(&self) -> &CommandExecutor {
158 &self.executor
159 }
160
161 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
162 &mut self.executor
163 }
164
165 fn build_command_args(&self) -> Vec<String> {
166 let mut args = vec!["builder".to_string(), "prune".to_string()];
167
168 if self.all {
169 args.push("--all".to_string());
170 }
171
172 for (key, value) in &self.filters {
173 args.push("--filter".to_string());
174 args.push(format!("{key}={value}"));
175 }
176
177 if self.force {
178 args.push("--force".to_string());
179 }
180
181 if let Some(storage) = &self.keep_storage {
182 args.push("--keep-storage".to_string());
183 args.push(storage.clone());
184 }
185
186 args.extend(self.executor.raw_args.clone());
188
189 args
190 }
191
192 async fn execute(&self) -> Result<Self::Output> {
193 let args = self.build_command_args();
194 let output = self.executor.execute_command("docker", args).await?;
195
196 let (deleted_cache_ids, space_reclaimed, space_reclaimed_str) =
197 Self::parse_output(&output.stdout);
198
199 Ok(BuilderPruneResult {
200 deleted_cache_ids,
201 space_reclaimed,
202 space_reclaimed_str,
203 stdout: output.stdout,
204 stderr: output.stderr,
205 exit_code: output.exit_code,
206 })
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) -> Option<u64> {
215 let size_str = size_str.trim();
216
217 let (num_str, unit) = if let Some(pos) = size_str.find(|c: char| c.is_alphabetic()) {
219 (&size_str[..pos], &size_str[pos..])
220 } else {
221 return size_str.parse().ok();
222 };
223
224 let number: f64 = num_str.trim().parse().ok()?;
225
226 let multiplier = match unit.to_uppercase().as_str() {
227 "B" | "" => 1.0,
228 "KB" | "K" => 1024.0,
229 "MB" | "M" => 1024.0 * 1024.0,
230 "GB" | "G" => 1024.0 * 1024.0 * 1024.0,
231 "TB" | "T" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
232 _ => return None,
233 };
234
235 if number.is_sign_negative() || !number.is_finite() {
236 return None;
237 }
238 let result = (number * multiplier).round();
239 if result > u64::MAX as f64 {
240 return None;
241 }
242 Some(result as u64)
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_builder_prune_basic() {
251 let cmd = BuilderPruneCommand::new();
252 let args = cmd.build_command_args();
253 assert_eq!(args, vec!["builder", "prune"]);
254 }
255
256 #[test]
257 fn test_builder_prune_all_options() {
258 let cmd = BuilderPruneCommand::new()
259 .all()
260 .filter("until", "24h")
261 .force()
262 .keep_storage("5GB");
263
264 let args = cmd.build_command_args();
265 assert!(args.contains(&"--all".to_string()));
266 assert!(args.contains(&"--filter".to_string()));
267 assert!(args.contains(&"until=24h".to_string()));
268 assert!(args.contains(&"--force".to_string()));
269 assert!(args.contains(&"--keep-storage".to_string()));
270 assert!(args.contains(&"5GB".to_string()));
271 }
272
273 #[test]
274 fn test_parse_size() {
275 assert_eq!(parse_size("100"), Some(100));
276 assert_eq!(parse_size("1KB"), Some(1024));
277 assert_eq!(parse_size("1.5KB"), Some(1536));
278 assert_eq!(parse_size("2MB"), Some(2 * 1024 * 1024));
279 assert_eq!(parse_size("1GB"), Some(1024 * 1024 * 1024));
280 assert_eq!(parse_size("2.5GB"), Some(2_684_354_560));
281 assert_eq!(parse_size("1TB"), Some(1_099_511_627_776));
282 }
283
284 #[test]
285 fn test_parse_output() {
286 let output = r"Deleted: sha256:abc123
287Deleted: sha256:def456
288Total reclaimed space: 2.5GB";
289
290 let (ids, bytes, str_val) = BuilderPruneCommand::parse_output(output);
291 assert_eq!(ids.len(), 2);
292 assert!(ids.contains(&"sha256:abc123".to_string()));
293 assert!(ids.contains(&"sha256:def456".to_string()));
294 assert_eq!(bytes, Some(2_684_354_560));
295 assert_eq!(str_val, Some("2.5GB".to_string()));
296 }
297
298 #[test]
299 fn test_builder_prune_extensibility() {
300 let mut cmd = BuilderPruneCommand::new();
301 cmd.get_executor_mut()
302 .raw_args
303 .push("--custom-flag".to_string());
304
305 let args = cmd.build_command_args();
306 assert!(args.contains(&"--custom-flag".to_string()));
307 }
308}