1use std::path::{Path, PathBuf};
4
5use oci_client::Reference;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest as Sha2Digest, Sha256};
8
9use crate::{
10 config::ImageConfig,
11 digest::Digest,
12 error::{ImageError, ImageResult},
13};
14
15const LAYERS_DIR: &str = "layers";
21
22const FSMETA_DIR: &str = "fsmeta";
24
25const VMDK_DIR: &str = "vmdk";
27
28const MANIFESTS_DIR: &str = "manifests";
30
31const TMP_DIR: &str = "tmp";
33
34const EROFS_ALIGNMENT_BYTES: u64 = 4096;
36
37pub struct GlobalCache {
57 layers_dir: PathBuf,
59
60 fsmeta_dir: PathBuf,
62
63 vmdk_dir: PathBuf,
65
66 manifests_dir: PathBuf,
68
69 tmp_dir: PathBuf,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CachedImageMetadata {
76 pub manifest_digest: String,
78 pub config_digest: String,
80 pub config: ImageConfig,
82 pub layers: Vec<CachedLayerMetadata>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct CachedLayerMetadata {
89 pub digest: String,
91 pub media_type: Option<String>,
93 pub size_bytes: Option<u64>,
95 pub diff_id: String,
97}
98
99impl GlobalCache {
104 pub fn new(cache_dir: &Path) -> ImageResult<Self> {
108 let layers_dir = cache_dir.join(LAYERS_DIR);
109 let fsmeta_dir = cache_dir.join(FSMETA_DIR);
110 let vmdk_dir = cache_dir.join(VMDK_DIR);
111 let manifests_dir = cache_dir.join(MANIFESTS_DIR);
112 let tmp_dir = cache_dir.join(TMP_DIR);
113
114 for dir in [
115 &layers_dir,
116 &fsmeta_dir,
117 &vmdk_dir,
118 &manifests_dir,
119 &tmp_dir,
120 ] {
121 std::fs::create_dir_all(dir).map_err(|e| ImageError::Cache {
122 path: dir.clone(),
123 source: e,
124 })?;
125 }
126
127 Ok(Self {
128 layers_dir,
129 fsmeta_dir,
130 vmdk_dir,
131 manifests_dir,
132 tmp_dir,
133 })
134 }
135
136 pub async fn new_async(cache_dir: &Path) -> ImageResult<Self> {
138 let layers_dir = cache_dir.join(LAYERS_DIR);
139 let fsmeta_dir = cache_dir.join(FSMETA_DIR);
140 let vmdk_dir = cache_dir.join(VMDK_DIR);
141 let manifests_dir = cache_dir.join(MANIFESTS_DIR);
142 let tmp_dir = cache_dir.join(TMP_DIR);
143
144 for dir in [
145 &layers_dir,
146 &fsmeta_dir,
147 &vmdk_dir,
148 &manifests_dir,
149 &tmp_dir,
150 ] {
151 tokio::fs::create_dir_all(dir)
152 .await
153 .map_err(|e| ImageError::Cache {
154 path: dir.clone(),
155 source: e,
156 })?;
157 }
158
159 Ok(Self {
160 layers_dir,
161 fsmeta_dir,
162 vmdk_dir,
163 manifests_dir,
164 tmp_dir,
165 })
166 }
167
168 pub fn layers_dir(&self) -> &Path {
172 &self.layers_dir
173 }
174
175 pub fn layer_erofs_path(&self, diff_id: &Digest) -> PathBuf {
177 self.layers_dir
178 .join(format!("{}.erofs", diff_id.to_path_safe()))
179 }
180
181 pub fn layer_erofs_lock_path(&self, diff_id: &Digest) -> PathBuf {
183 self.layers_dir
184 .join(format!("{}.erofs.lock", diff_id.to_path_safe()))
185 }
186
187 pub fn is_layer_materialized(&self, diff_id: &Digest) -> bool {
189 is_valid_erofs_artifact(&self.layer_erofs_path(diff_id))
190 }
191
192 pub fn all_layers_materialized(&self, diff_ids: &[Digest]) -> bool {
194 diff_ids.iter().all(|d| self.is_layer_materialized(d))
195 }
196
197 pub fn fsmeta_dir(&self) -> &Path {
201 &self.fsmeta_dir
202 }
203
204 pub fn fsmeta_erofs_path(&self, manifest_digest: &Digest) -> PathBuf {
206 self.fsmeta_dir
207 .join(format!("{}.erofs", manifest_digest.to_path_safe()))
208 }
209
210 pub fn fsmeta_erofs_lock_path(&self, manifest_digest: &Digest) -> PathBuf {
212 self.fsmeta_dir
213 .join(format!("{}.erofs.lock", manifest_digest.to_path_safe()))
214 }
215
216 pub fn is_fsmeta_materialized(&self, manifest_digest: &Digest) -> bool {
218 is_valid_erofs_artifact(&self.fsmeta_erofs_path(manifest_digest))
219 }
220
221 pub fn vmdk_dir(&self) -> &Path {
225 &self.vmdk_dir
226 }
227
228 pub fn vmdk_path(&self, manifest_digest: &Digest) -> PathBuf {
230 self.vmdk_dir
231 .join(format!("{}.vmdk", manifest_digest.to_path_safe()))
232 }
233
234 pub fn vmdk_lock_path(&self, manifest_digest: &Digest) -> PathBuf {
236 self.vmdk_dir
237 .join(format!("{}.vmdk.lock", manifest_digest.to_path_safe()))
238 }
239
240 pub fn is_vmdk_materialized(&self, manifest_digest: &Digest) -> bool {
242 self.vmdk_path(manifest_digest).exists()
243 }
244
245 pub fn tmp_dir(&self) -> &Path {
249 &self.tmp_dir
250 }
251
252 pub fn part_path(&self, blob_digest: &Digest) -> PathBuf {
254 self.tmp_dir
255 .join(format!("{}.part", blob_digest.to_path_safe()))
256 }
257
258 pub fn download_lock_path(&self, blob_digest: &Digest) -> PathBuf {
260 self.tmp_dir
261 .join(format!("{}.download.lock", blob_digest.to_path_safe()))
262 }
263
264 pub fn work_dir(&self, key: &Digest) -> PathBuf {
266 self.tmp_dir.join(format!("{}.work", key.to_path_safe()))
267 }
268
269 pub fn manifests_dir(&self) -> &Path {
273 &self.manifests_dir
274 }
275
276 pub fn image_lock_path(&self, reference: &Reference) -> PathBuf {
278 self.manifests_dir
279 .join(format!("{}.lock", image_cache_key(reference)))
280 }
281
282 pub fn read_image_metadata(
284 &self,
285 reference: &Reference,
286 ) -> ImageResult<Option<CachedImageMetadata>> {
287 let path = self.image_metadata_path(reference);
288
289 let data = match std::fs::read_to_string(&path) {
290 Ok(data) => data,
291 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
292 Err(e) => return Err(ImageError::Cache { path, source: e }),
293 };
294
295 parse_cached_image_metadata(&path, &data)
296 }
297
298 pub async fn read_image_metadata_async(
300 &self,
301 reference: &Reference,
302 ) -> ImageResult<Option<CachedImageMetadata>> {
303 let path = self.image_metadata_path(reference);
304
305 let data = match tokio::fs::read_to_string(&path).await {
306 Ok(data) => data,
307 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
308 Err(e) => return Err(ImageError::Cache { path, source: e }),
309 };
310
311 parse_cached_image_metadata(&path, &data)
312 }
313
314 #[cfg_attr(not(test), allow(dead_code))]
316 pub(crate) fn write_image_metadata(
317 &self,
318 reference: &Reference,
319 metadata: &CachedImageMetadata,
320 ) -> ImageResult<()> {
321 let path = self.image_metadata_path(reference);
322 let temp_path = path.with_extension("json.part");
323 let payload = serde_json::to_vec(metadata).map_err(|e| {
324 ImageError::ConfigParse(format!("failed to serialize cached image metadata: {e}"))
325 })?;
326
327 std::fs::write(&temp_path, payload).map_err(|e| ImageError::Cache {
328 path: temp_path.clone(),
329 source: e,
330 })?;
331 std::fs::rename(&temp_path, &path).map_err(|e| ImageError::Cache { path, source: e })?;
332
333 Ok(())
334 }
335
336 pub(crate) async fn write_image_metadata_async(
338 &self,
339 reference: &Reference,
340 metadata: &CachedImageMetadata,
341 ) -> ImageResult<()> {
342 let path = self.image_metadata_path(reference);
343 let temp_path = path.with_extension("json.part");
344 let payload = serde_json::to_vec(metadata).map_err(|e| {
345 ImageError::ConfigParse(format!("failed to serialize cached image metadata: {e}"))
346 })?;
347
348 tokio::fs::write(&temp_path, payload)
349 .await
350 .map_err(|e| ImageError::Cache {
351 path: temp_path.clone(),
352 source: e,
353 })?;
354 tokio::fs::rename(&temp_path, &path)
355 .await
356 .map_err(|e| ImageError::Cache { path, source: e })?;
357
358 Ok(())
359 }
360
361 pub fn delete_image_metadata(&self, reference: &Reference) -> ImageResult<()> {
363 let path = self.image_metadata_path(reference);
364 match std::fs::remove_file(&path) {
365 Ok(()) => Ok(()),
366 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
367 Err(e) => Err(ImageError::Cache { path, source: e }),
368 }
369 }
370
371 pub async fn delete_image_metadata_async(&self, reference: &Reference) -> ImageResult<()> {
373 let path = self.image_metadata_path(reference);
374 match tokio::fs::remove_file(&path).await {
375 Ok(()) => Ok(()),
376 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
377 Err(e) => Err(ImageError::Cache { path, source: e }),
378 }
379 }
380
381 fn image_metadata_path(&self, reference: &Reference) -> PathBuf {
383 self.manifests_dir
384 .join(format!("{}.json", image_cache_key(reference)))
385 }
386
387 pub fn tar_path(&self, digest: &Digest) -> PathBuf {
391 self.layers_dir
392 .join(format!("{}.tar.gz", digest.to_path_safe()))
393 }
394}
395
396fn image_cache_key(reference: &Reference) -> String {
401 let mut hasher = Sha256::new();
402 hasher.update(reference.to_string().as_bytes());
403 hex::encode(hasher.finalize())
404}
405
406fn parse_cached_image_metadata(
407 path: &Path,
408 data: &str,
409) -> ImageResult<Option<CachedImageMetadata>> {
410 match serde_json::from_str::<CachedImageMetadata>(data) {
411 Ok(metadata) => Ok(Some(metadata)),
412 Err(e) => {
413 tracing::warn!(
414 path = %path.display(),
415 error = %e,
416 "corrupt image metadata cache, ignoring"
417 );
418 Ok(None)
419 }
420 }
421}
422
423pub(crate) fn is_valid_erofs_artifact(path: &Path) -> bool {
424 match std::fs::metadata(path) {
425 Ok(meta) => {
426 let len = meta.len();
427 len > 0 && len % EROFS_ALIGNMENT_BYTES == 0
428 }
429 Err(_) => false,
430 }
431}
432
433pub(crate) async fn is_valid_erofs_artifact_async(path: &Path) -> bool {
434 match tokio::fs::metadata(path).await {
435 Ok(meta) => {
436 let len = meta.len();
437 len > 0 && len % EROFS_ALIGNMENT_BYTES == 0
438 }
439 Err(_) => false,
440 }
441}