Skip to main content

harmont_cli/commands/cache/
clean.rs

1use anyhow::Result;
2use hm_vm::VmBackend as _;
3
4/// # Errors
5/// Returns an error if workspace cache removal fails.
6pub async fn handle_clean() -> Result<i32> {
7    let ws_cleaned = if let Some(ws_cache) = hm_util::dirs::hm_workspace_cache_dir()
8        && ws_cache.exists()
9    {
10        let size = dir_size(&ws_cache);
11        std::fs::remove_dir_all(&ws_cache)?;
12        tracing::info!(
13            path = %ws_cache.display(),
14            "removed workspace cache ({})",
15            human_bytes(size),
16        );
17        true
18    } else {
19        false
20    };
21
22    let db_cleaned = if let Some(cache_dir) = hm_util::dirs::hm_cache_dir() {
23        let db_path = cache_dir.join("registry.db");
24        if db_path.exists() {
25            // Remove the backing Docker images BEFORE deleting registry.db.
26            // The registry is the only index from a cache key to its tagged
27            // image (`forever-*`, etc.); once the DB is gone the images can't
28            // be located by key, and `docker image prune` only reclaims
29            // *dangling* images, so a tagged snapshot survives it. So we
30            // enumerate the registry, remove each image via the Docker
31            // backend (best-effort), then drop the DB.
32            remove_registered_images(&db_path).await;
33
34            std::fs::remove_file(&db_path)?;
35            tracing::info!(path = %db_path.display(), "removed VM image registry");
36            true
37        } else {
38            false
39        }
40    } else {
41        false
42    };
43
44    if !ws_cleaned && !db_cleaned {
45        tracing::info!("nothing to clean");
46    }
47
48    Ok(0)
49}
50
51/// Remove every Docker image tracked by the registry at `db_path`.
52///
53/// Best-effort: a missing Docker daemon or an already-deleted image is logged
54/// and skipped, never fatal — `clean` must still delete the registry DB so the
55/// cache index is reset.
56async fn remove_registered_images(db_path: &std::path::Path) {
57    // Capacity here is irrelevant — we only read existing rows, never insert.
58    let registry = match hm_vm::ImageRegistry::open(db_path, std::num::NonZeroU64::MAX) {
59        Ok(r) => r,
60        Err(e) => {
61            tracing::warn!(error = %e, "could not open image registry; skipping image removal");
62            return;
63        }
64    };
65
66    let snapshots = registry.all_snapshot_ids();
67    if snapshots.is_empty() {
68        return;
69    }
70
71    let backend = match hm_vm::docker::DockerBackend::connect() {
72        Ok(b) => b,
73        Err(e) => {
74            tracing::warn!(
75                error = %e,
76                "could not connect to Docker; {} cached image(s) may remain — remove them with `docker image rm`",
77                snapshots.len(),
78            );
79            return;
80        }
81    };
82
83    let mut removed = 0usize;
84    for snap in &snapshots {
85        match backend.remove_snapshot(snap).await {
86            Ok(()) => removed += 1,
87            Err(e) => {
88                tracing::warn!(image = %snap, error = %e, "failed to remove cached image");
89            }
90        }
91    }
92    tracing::info!(
93        "removed {removed} of {} cached Docker image(s)",
94        snapshots.len()
95    );
96}
97
98fn dir_size(path: &std::path::Path) -> u64 {
99    fn walk(p: &std::path::Path) -> u64 {
100        std::fs::read_dir(p)
101            .into_iter()
102            .flatten()
103            .filter_map(std::result::Result::ok)
104            .map(|e| {
105                let path = e.path();
106                if path.is_dir() {
107                    walk(&path)
108                } else {
109                    e.metadata().map_or(0, |m| m.len())
110                }
111            })
112            .sum()
113    }
114    walk(path)
115}
116
117#[allow(
118    clippy::cast_precision_loss,
119    reason = "human-readable display; sub-byte precision irrelevant"
120)]
121fn human_bytes(bytes: u64) -> String {
122    let b = bytes as f64;
123    if bytes < 1024 {
124        format!("{bytes}B")
125    } else if bytes < 1024 * 1024 {
126        format!("{:.1}KB", b / 1024.0)
127    } else if bytes < 1024 * 1024 * 1024 {
128        format!("{:.1}MB", b / (1024.0 * 1024.0))
129    } else {
130        format!("{:.1}GB", b / (1024.0 * 1024.0 * 1024.0))
131    }
132}