use std::path::{Path, PathBuf};
use oci_client::Reference;
use serde::{Deserialize, Serialize};
use sha2::{Digest as Sha2Digest, Sha256};
use crate::{
config::ImageConfig,
digest::Digest,
error::{ImageError, ImageResult},
};
const LAYERS_DIR: &str = "layers";
const IMAGES_DIR: &str = "images";
pub(crate) const COMPLETE_MARKER: &str = ".complete";
pub struct GlobalCache {
layers_dir: PathBuf,
images_dir: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedImageMetadata {
pub manifest_digest: String,
pub config_digest: String,
pub config: ImageConfig,
pub layers: Vec<CachedLayerMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedLayerMetadata {
pub digest: String,
pub media_type: Option<String>,
pub size_bytes: Option<u64>,
pub diff_id: String,
}
impl GlobalCache {
pub fn new(cache_dir: &Path) -> ImageResult<Self> {
let layers_dir = cache_dir.join(LAYERS_DIR);
let images_dir = cache_dir.join(IMAGES_DIR);
std::fs::create_dir_all(&layers_dir).map_err(|e| ImageError::Cache {
path: layers_dir.clone(),
source: e,
})?;
std::fs::create_dir_all(&images_dir).map_err(|e| ImageError::Cache {
path: images_dir.clone(),
source: e,
})?;
Ok(Self {
layers_dir,
images_dir,
})
}
pub fn layers_dir(&self) -> &Path {
&self.layers_dir
}
pub fn tar_path(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.tar.gz", digest.to_path_safe()))
}
pub fn part_path(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.tar.gz.part", digest.to_path_safe()))
}
pub fn extracted_dir(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.extracted", digest.to_path_safe()))
}
pub fn extracting_dir(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.extracting", digest.to_path_safe()))
}
pub fn index_path(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.index", digest.to_path_safe()))
}
pub fn implicit_dirs_path(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.implicit_dirs", digest.to_path_safe()))
}
pub fn lock_path(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.lock", digest.to_path_safe()))
}
pub fn download_lock_path(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.download.lock", digest.to_path_safe()))
}
pub fn image_lock_path(&self, reference: &Reference) -> PathBuf {
self.images_dir
.join(format!("{}.lock", image_cache_key(reference)))
}
pub fn is_extracted(&self, digest: &Digest) -> bool {
self.extracted_dir(digest).join(COMPLETE_MARKER).exists()
}
pub fn all_layers_extracted(&self, digests: &[Digest]) -> bool {
digests.iter().all(|d| self.is_extracted(d))
}
pub fn read_image_metadata(
&self,
reference: &Reference,
) -> ImageResult<Option<CachedImageMetadata>> {
let path = self.image_metadata_path(reference);
let data = match std::fs::read_to_string(&path) {
Ok(data) => data,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(ImageError::Cache { path, source: e }),
};
match serde_json::from_str::<CachedImageMetadata>(&data) {
Ok(metadata) => Ok(Some(metadata)),
Err(e) => {
tracing::warn!(path = %path.display(), error = %e, "corrupt image metadata cache, ignoring");
Ok(None)
}
}
}
pub(crate) fn write_image_metadata(
&self,
reference: &Reference,
metadata: &CachedImageMetadata,
) -> ImageResult<()> {
let path = self.image_metadata_path(reference);
let temp_path = path.with_extension("json.part");
let payload = serde_json::to_vec(metadata).map_err(|e| {
ImageError::ConfigParse(format!("failed to serialize cached image metadata: {e}"))
})?;
std::fs::write(&temp_path, payload).map_err(|e| ImageError::Cache {
path: temp_path.clone(),
source: e,
})?;
std::fs::rename(&temp_path, &path).map_err(|e| ImageError::Cache { path, source: e })?;
Ok(())
}
pub fn delete_image_metadata(&self, reference: &Reference) -> ImageResult<()> {
let path = self.image_metadata_path(reference);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(ImageError::Cache { path, source: e }),
}
}
fn image_metadata_path(&self, reference: &Reference) -> PathBuf {
self.images_dir
.join(format!("{}.json", image_cache_key(reference)))
}
}
fn image_cache_key(reference: &Reference) -> String {
let mut hasher = Sha256::new();
hasher.update(reference.to_string().as_bytes());
hex::encode(hasher.finalize())
}