containerd_store/
export.rs1use 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 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), }
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 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 copy_manifest_tree(content_root, mirror_root, &next_digest, copied, record)
190 }
191 }
192}
193
194#[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}