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 FSMETA_DIR: &str = "fsmeta";
const VMDK_DIR: &str = "vmdk";
const MANIFESTS_DIR: &str = "manifests";
const TMP_DIR: &str = "tmp";
const EROFS_ALIGNMENT_BYTES: u64 = 4096;
pub struct GlobalCache {
layers_dir: PathBuf,
fsmeta_dir: PathBuf,
vmdk_dir: PathBuf,
manifests_dir: PathBuf,
tmp_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 fsmeta_dir = cache_dir.join(FSMETA_DIR);
let vmdk_dir = cache_dir.join(VMDK_DIR);
let manifests_dir = cache_dir.join(MANIFESTS_DIR);
let tmp_dir = cache_dir.join(TMP_DIR);
for dir in [
&layers_dir,
&fsmeta_dir,
&vmdk_dir,
&manifests_dir,
&tmp_dir,
] {
std::fs::create_dir_all(dir).map_err(|e| ImageError::Cache {
path: dir.clone(),
source: e,
})?;
}
Ok(Self {
layers_dir,
fsmeta_dir,
vmdk_dir,
manifests_dir,
tmp_dir,
})
}
pub async fn new_async(cache_dir: &Path) -> ImageResult<Self> {
let layers_dir = cache_dir.join(LAYERS_DIR);
let fsmeta_dir = cache_dir.join(FSMETA_DIR);
let vmdk_dir = cache_dir.join(VMDK_DIR);
let manifests_dir = cache_dir.join(MANIFESTS_DIR);
let tmp_dir = cache_dir.join(TMP_DIR);
for dir in [
&layers_dir,
&fsmeta_dir,
&vmdk_dir,
&manifests_dir,
&tmp_dir,
] {
tokio::fs::create_dir_all(dir)
.await
.map_err(|e| ImageError::Cache {
path: dir.clone(),
source: e,
})?;
}
Ok(Self {
layers_dir,
fsmeta_dir,
vmdk_dir,
manifests_dir,
tmp_dir,
})
}
pub fn layers_dir(&self) -> &Path {
&self.layers_dir
}
pub fn layer_erofs_path(&self, diff_id: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.erofs", diff_id.to_path_safe()))
}
pub fn layer_erofs_lock_path(&self, diff_id: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.erofs.lock", diff_id.to_path_safe()))
}
pub fn is_layer_materialized(&self, diff_id: &Digest) -> bool {
is_valid_erofs_artifact(&self.layer_erofs_path(diff_id))
}
pub fn all_layers_materialized(&self, diff_ids: &[Digest]) -> bool {
diff_ids.iter().all(|d| self.is_layer_materialized(d))
}
pub fn fsmeta_dir(&self) -> &Path {
&self.fsmeta_dir
}
pub fn fsmeta_erofs_path(&self, manifest_digest: &Digest) -> PathBuf {
self.fsmeta_dir
.join(format!("{}.erofs", manifest_digest.to_path_safe()))
}
pub fn fsmeta_erofs_lock_path(&self, manifest_digest: &Digest) -> PathBuf {
self.fsmeta_dir
.join(format!("{}.erofs.lock", manifest_digest.to_path_safe()))
}
pub fn is_fsmeta_materialized(&self, manifest_digest: &Digest) -> bool {
is_valid_erofs_artifact(&self.fsmeta_erofs_path(manifest_digest))
}
pub fn vmdk_dir(&self) -> &Path {
&self.vmdk_dir
}
pub fn vmdk_path(&self, manifest_digest: &Digest) -> PathBuf {
self.vmdk_dir
.join(format!("{}.vmdk", manifest_digest.to_path_safe()))
}
pub fn vmdk_lock_path(&self, manifest_digest: &Digest) -> PathBuf {
self.vmdk_dir
.join(format!("{}.vmdk.lock", manifest_digest.to_path_safe()))
}
pub fn is_vmdk_materialized(&self, manifest_digest: &Digest) -> bool {
self.vmdk_path(manifest_digest).exists()
}
pub fn tmp_dir(&self) -> &Path {
&self.tmp_dir
}
pub fn part_path(&self, blob_digest: &Digest) -> PathBuf {
self.tmp_dir
.join(format!("{}.part", blob_digest.to_path_safe()))
}
pub fn download_lock_path(&self, blob_digest: &Digest) -> PathBuf {
self.tmp_dir
.join(format!("{}.download.lock", blob_digest.to_path_safe()))
}
pub fn work_dir(&self, key: &Digest) -> PathBuf {
self.tmp_dir.join(format!("{}.work", key.to_path_safe()))
}
pub fn manifests_dir(&self) -> &Path {
&self.manifests_dir
}
pub fn image_lock_path(&self, reference: &Reference) -> PathBuf {
self.manifests_dir
.join(format!("{}.lock", image_cache_key(reference)))
}
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 }),
};
parse_cached_image_metadata(&path, &data)
}
pub async fn read_image_metadata_async(
&self,
reference: &Reference,
) -> ImageResult<Option<CachedImageMetadata>> {
let path = self.image_metadata_path(reference);
let data = match tokio::fs::read_to_string(&path).await {
Ok(data) => data,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(ImageError::Cache { path, source: e }),
};
parse_cached_image_metadata(&path, &data)
}
#[cfg_attr(not(test), allow(dead_code))]
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(crate) async fn write_image_metadata_async(
&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}"))
})?;
tokio::fs::write(&temp_path, payload)
.await
.map_err(|e| ImageError::Cache {
path: temp_path.clone(),
source: e,
})?;
tokio::fs::rename(&temp_path, &path)
.await
.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 }),
}
}
pub async fn delete_image_metadata_async(&self, reference: &Reference) -> ImageResult<()> {
let path = self.image_metadata_path(reference);
match tokio::fs::remove_file(&path).await {
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.manifests_dir
.join(format!("{}.json", image_cache_key(reference)))
}
pub fn tar_path(&self, digest: &Digest) -> PathBuf {
self.layers_dir
.join(format!("{}.tar.gz", digest.to_path_safe()))
}
}
fn image_cache_key(reference: &Reference) -> String {
let mut hasher = Sha256::new();
hasher.update(reference.to_string().as_bytes());
hex::encode(hasher.finalize())
}
fn parse_cached_image_metadata(
path: &Path,
data: &str,
) -> ImageResult<Option<CachedImageMetadata>> {
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 is_valid_erofs_artifact(path: &Path) -> bool {
match std::fs::metadata(path) {
Ok(meta) => {
let len = meta.len();
len > 0 && len % EROFS_ALIGNMENT_BYTES == 0
}
Err(_) => false,
}
}
pub(crate) async fn is_valid_erofs_artifact_async(path: &Path) -> bool {
match tokio::fs::metadata(path).await {
Ok(meta) => {
let len = meta.len();
len > 0 && len % EROFS_ALIGNMENT_BYTES == 0
}
Err(_) => false,
}
}