Skip to main content

devclean/
docker.rs

1use std::process::{Command, Stdio};
2
3use anyhow::{Context, Result, bail};
4use serde_json::Value;
5
6#[derive(Debug)]
7struct CommandOutput {
8    success: bool,
9    stdout: Vec<u8>,
10    stderr: Vec<u8>,
11}
12
13/// Returns Docker's detailed, read-only disk usage summary.
14///
15/// # Errors
16///
17/// Returns an error when Docker is missing, unavailable, or exits unsuccessfully.
18pub fn system_df() -> Result<String> {
19    run_docker(&["system", "df", "-v"])
20}
21
22/// Returns Docker disk-usage rows as structured JSON values.
23///
24/// # Errors
25///
26/// Returns an error when Docker is unavailable or emits an invalid JSON row.
27pub fn system_df_structured() -> Result<Vec<Value>> {
28    let output = run_docker(&["system", "df", "--format", "{{json .}}"])?;
29    parse_system_df(&output)
30}
31
32fn parse_system_df(output: &str) -> Result<Vec<Value>> {
33    output
34        .lines()
35        .filter(|line| !line.trim().is_empty())
36        .map(|line| serde_json::from_str(line).context("Docker emitted an invalid JSON row"))
37        .collect()
38}
39
40/// Removes only unused Docker build cache.
41///
42/// # Errors
43///
44/// Returns an error when Docker is missing, unavailable, or exits unsuccessfully.
45pub fn prune_build_cache(older_than: Option<&str>) -> Result<String> {
46    let mut arguments = vec!["builder", "prune", "-af"];
47    let filter;
48    if let Some(age) = older_than {
49        filter = format!("until={age}");
50        arguments.extend(["--filter", &filter]);
51    }
52    run_docker(&arguments)
53}
54
55/// Removes stopped containers, unused networks/images, and build cache.
56///
57/// Docker volumes are intentionally never passed to prune.
58///
59/// # Errors
60///
61/// Returns an error when Docker is missing, unavailable, or exits unsuccessfully.
62pub fn prune_system(older_than: Option<&str>) -> Result<String> {
63    let mut arguments = vec!["system", "prune", "-af"];
64    let filter;
65    if let Some(age) = older_than {
66        filter = format!("until={age}");
67        arguments.extend(["--filter", &filter]);
68    }
69    run_docker(&arguments)
70}
71
72fn run_docker(arguments: &[&str]) -> Result<String> {
73    run_docker_with(arguments, |arguments| {
74        let output = Command::new("docker")
75            .args(arguments)
76            .stdin(Stdio::null())
77            .output()
78            .context("failed to run docker")?;
79        Ok(CommandOutput {
80            success: output.status.success(),
81            stdout: output.stdout,
82            stderr: output.stderr,
83        })
84    })
85}
86
87fn run_docker_with(
88    arguments: &[&str],
89    runner: impl FnOnce(&[&str]) -> Result<CommandOutput>,
90) -> Result<String> {
91    let output = runner(arguments)?;
92    if !output.success {
93        let stderr = String::from_utf8_lossy(&output.stderr);
94        bail!("docker failed: {}", stderr.trim());
95    }
96    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn docker_runner_should_forward_arguments_and_surface_stderr() {
105        let result = run_docker_with(&["builder", "prune", "-af"], |arguments| {
106            assert_eq!(arguments, ["builder", "prune", "-af"]);
107            Ok(CommandOutput {
108                success: false,
109                stdout: Vec::new(),
110                stderr: b"daemon unavailable".to_vec(),
111            })
112        });
113
114        assert!(
115            result
116                .expect_err("failure status must be rejected")
117                .to_string()
118                .contains("daemon unavailable")
119        );
120    }
121
122    #[test]
123    fn structured_rows_should_parse_json_lines() -> Result<()> {
124        let output =
125            "{\"Type\":\"Images\",\"Size\":\"1GB\"}\n{\"Type\":\"Build Cache\",\"Size\":\"2GB\"}\n";
126        let rows = parse_system_df(output)?;
127
128        assert_eq!(rows.len(), 2);
129        assert_eq!(rows[1]["Type"], "Build Cache");
130        Ok(())
131    }
132}