containerd_store/
export.rs

1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use sha2::{Digest, Sha256};
6
7use crate::ContainerdStore;
8use crate::types::{DigestRef, ManifestInfo, PortableImageExport, Result, StoreError};
9
10pub fn export_image_to_dir(
11    store: &ContainerdStore,
12    image: &str,
13    out: impl AsRef<Path>,
14) -> Result<PortableImageExport> {
15    let mut all = export_images_to_dir(store, &[image], out)?;
16    Ok(all.remove(0))
17}
18
19pub fn export_images_to_dir(
20    store: &ContainerdStore,
21    images: &[&str],
22    out: impl AsRef<Path>,
23) -> Result<Vec<PortableImageExport>> {
24    let out_root = out.as_ref().to_path_buf();
25    let mirror_root = out_root
26        .join("io.containerd.content.v1.content")
27        .join("blobs");
28    fs::create_dir_all(&mirror_root)?;
29
30    // copy meta.db so the bundle is self-contained
31    let meta_src = store.meta_db_path();
32    let meta_dst = out_root
33        .join("io.containerd.metadata.v1.bolt")
34        .join("meta.db");
35    if let Some(parent) = meta_dst.parent() {
36        fs::create_dir_all(parent)?;
37    }
38    if meta_src.exists() {
39        fs::copy(&meta_src, &meta_dst)?;
40    }
41
42    let content_root = store.content_root();
43    let mut copied = HashSet::new();
44    let mut results = Vec::new();
45
46    for image in images {
47        let resolved = store.resolve_image(image)?;
48        let manifest_digest = resolved.entry.target.digest.clone();
49        let mut this_copied = Vec::new();
50        copy_manifest_tree(
51            &content_root,
52            &mirror_root,
53            &manifest_digest,
54            &mut copied,
55            &mut this_copied,
56        )?;
57        results.push(PortableImageExport {
58            manifest_digest,
59            blobs_root: mirror_root.clone(),
60            copied: this_copied,
61        });
62    }
63
64    Ok(results)
65}
66
67fn copy_if_needed(src: &Path, dst: &Path) -> Result<()> {
68    if !src.exists() {
69        return Err(StoreError::Io(std::io::Error::new(
70            std::io::ErrorKind::NotFound,
71            format!("missing blob: {}", src.display()),
72        )));
73    }
74    if let Some(parent) = dst.parent() {
75        fs::create_dir_all(parent)?;
76    }
77    if !dst.exists() {
78        fs::copy(src, dst)?;
79    }
80    Ok(())
81}
82
83fn load_manifest(path: &Path) -> Result<ManifestInfo> {
84    let bytes = fs::read(path)?;
85    let manifest: serde_json::Value = serde_json::from_slice(&bytes)?;
86    let config_digest = manifest["config"]["digest"].as_str().map(|s| s.to_string());
87    let mut layers = Vec::new();
88    if let Some(arr) = manifest["layers"].as_array() {
89        for layer in arr {
90            if let Some(d) = layer["digest"].as_str() {
91                layers.push(d.to_string());
92            }
93        }
94    }
95    Ok(ManifestInfo {
96        config_digest,
97        layer_digests: layers,
98        raw: bytes,
99    })
100}
101
102enum ManifestKind {
103    Image(ManifestInfo),
104    Index(String), // selected manifest digest
105}
106
107fn parse_manifest_or_index(path: &Path) -> Result<ManifestKind> {
108    let bytes = fs::read(path)?;
109    let json: serde_json::Value = serde_json::from_slice(&bytes)?;
110    if json.get("manifests").is_some() {
111        // Treat as OCI index / Docker manifest list
112        let manifests = json["manifests"].as_array().cloned().unwrap_or_default();
113        let pick = select_platform_manifest(&manifests)
114            .ok_or_else(|| StoreError::ImageNotFound("no manifests in index".into()))?;
115        let digest = pick
116            .get("digest")
117            .and_then(|v| v.as_str())
118            .ok_or_else(|| StoreError::ImageNotFound("manifest digest missing in index".into()))?;
119        return Ok(ManifestKind::Index(digest.to_string()));
120    }
121    Ok(ManifestKind::Image(load_manifest(path)?))
122}
123
124fn select_platform_manifest(manifests: &[serde_json::Value]) -> Option<serde_json::Value> {
125    let mut fallback: Option<serde_json::Value> = None;
126    for m in manifests {
127        if let Some(p) = m.get("platform") {
128            let arch = p.get("architecture").and_then(|v| v.as_str()).unwrap_or("");
129            let os = p.get("os").and_then(|v| v.as_str()).unwrap_or("");
130            if os == "linux" && arch == "amd64" {
131                return Some(m.clone());
132            }
133            if fallback.is_none() {
134                fallback = Some(m.clone());
135            }
136        } else if fallback.is_none() {
137            fallback = Some(m.clone());
138        }
139    }
140    fallback
141}
142
143fn copy_manifest_tree(
144    content_root: &PathBuf,
145    mirror_root: &PathBuf,
146    manifest_digest: &str,
147    copied: &mut HashSet<String>,
148    record: &mut Vec<String>,
149) -> Result<()> {
150    let manifest_ref = DigestRef::parse(manifest_digest)?;
151    let manifest_src = manifest_ref.path_under(content_root);
152    if !manifest_src.exists() {
153        return Err(StoreError::ImageNotFound(format!(
154            "manifest blob missing at {}",
155            manifest_src.display()
156        )));
157    }
158    let manifest_dst = manifest_ref.path_under(mirror_root);
159    copy_if_needed(&manifest_src, &manifest_dst)?;
160    if copied.insert(manifest_digest.to_string()) {
161        record.push(manifest_digest.to_string());
162    }
163
164    let parsed = parse_manifest_or_index(&manifest_dst)?;
165    match parsed {
166        ManifestKind::Image(manifest) => {
167            if let Some(cfg) = manifest.config_digest.as_ref() {
168                let cfg_ref = DigestRef::parse(cfg)?;
169                let cfg_src = cfg_ref.path_under(content_root);
170                let cfg_dst = cfg_ref.path_under(mirror_root);
171                copy_if_needed(&cfg_src, &cfg_dst)?;
172                if copied.insert(cfg.to_string()) {
173                    record.push(cfg.to_string());
174                }
175            }
176            for layer in &manifest.layer_digests {
177                let lr = DigestRef::parse(layer)?;
178                let ls = lr.path_under(content_root);
179                let ld = lr.path_under(mirror_root);
180                copy_if_needed(&ls, &ld)?;
181                if copied.insert(layer.clone()) {
182                    record.push(layer.clone());
183                }
184            }
185            Ok(())
186        }
187        ManifestKind::Index(next_digest) => {
188            // Ensure nested manifest is also copied.
189            copy_manifest_tree(content_root, mirror_root, &next_digest, copied, record)
190        }
191    }
192}
193
194// Utility to compute digest of a file (sha256) if needed later
195#[allow(dead_code)]
196fn digest_file(path: &Path) -> Result<String> {
197    let mut file = fs::File::open(path)?;
198    let mut hasher = Sha256::new();
199    std::io::copy(&mut file, &mut hasher)?;
200    let hex = format!("{:x}", hasher.finalize());
201    Ok(format!("sha256:{}", hex))
202}