Skip to main content

harmont_cli/commands/cache/
save.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use tracing::info;
5
6use super::manifest::{self, Manifest};
7use crate::orchestrator::docker_client::DockerClient;
8
9/// Save all `harmont-local/*` images to a cache directory as tar files,
10/// write a manifest, and prune stale tars that no longer correspond to
11/// any known image.
12///
13/// Prints the manifest's content hash to stdout so CI runners (e.g.
14/// GitHub Actions) can capture it for use as a cache key.
15///
16/// # Errors
17///
18/// Returns an error if the Docker daemon is unreachable, an image
19/// export fails, or any filesystem operation on `dir` fails.
20#[allow(clippy::print_stdout)]
21pub async fn handle_save(dir: &Path) -> Result<i32> {
22    let docker = DockerClient::connect()?;
23    docker.ping().await?;
24
25    tokio::fs::create_dir_all(dir)
26        .await
27        .with_context(|| format!("create cache dir {}", dir.display()))?;
28
29    let tags = docker.list_images_by_prefix("harmont-local/").await?;
30
31    let mut manifest = Manifest::new();
32
33    for tag in &tags {
34        let filename = manifest::tar_name_for_tag(tag);
35        let tar_path = dir.join(&filename);
36
37        if tar_path.exists() {
38            info!("skip (exists): {filename}");
39        } else {
40            info!("save: {tag} → {filename}");
41            docker.export_image(tag, &tar_path).await?;
42        }
43
44        manifest.images.insert(filename, tag.clone());
45    }
46
47    let manifest_json = serde_json::to_string_pretty(&manifest)?;
48    tokio::fs::write(dir.join("manifest.json"), &manifest_json)
49        .await
50        .context("write manifest.json")?;
51
52    let mut entries = tokio::fs::read_dir(dir).await?;
53    while let Some(entry) = entries.next_entry().await? {
54        let name = entry.file_name();
55        let name_str = name.to_string_lossy();
56        if name_str.ends_with(".tar") && !manifest.images.contains_key(name_str.as_ref()) {
57            info!("prune stale: {name_str}");
58            tokio::fs::remove_file(entry.path()).await.ok();
59        }
60    }
61
62    let hash = manifest.content_hash();
63    println!("{hash}");
64
65    Ok(0)
66}