Skip to main content

microsandbox_image/
store.rs

1//! Global on-disk image and layer cache.
2
3use 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
15//--------------------------------------------------------------------------------------------------
16// Constants
17//--------------------------------------------------------------------------------------------------
18
19/// Subdirectory under the cache root for layer storage.
20const LAYERS_DIR: &str = "layers";
21
22/// Subdirectory under the cache root for image metadata.
23const IMAGES_DIR: &str = "images";
24
25/// Marker file written as the last step of extraction.
26pub(crate) const COMPLETE_MARKER: &str = ".complete";
27
28//--------------------------------------------------------------------------------------------------
29// Types
30//--------------------------------------------------------------------------------------------------
31
32/// On-disk global cache for OCI layers.
33///
34/// Layout (all flat in `cache/layers/`, content-addressable by digest):
35/// ```text
36/// ~/.microsandbox/cache/layers/<digest_safe>.tar.gz            # compressed downloads
37/// ~/.microsandbox/cache/layers/<digest_safe>.extracted/        # extracted layer trees
38/// ~/.microsandbox/cache/layers/<digest_safe>.index             # binary sidecar indexes
39/// ~/.microsandbox/cache/layers/<digest_safe>.implicit_dirs     # pending implicit-dir fixups
40/// ~/.microsandbox/cache/layers/<digest_safe>.lock              # extraction flock files
41/// ~/.microsandbox/cache/layers/<digest_safe>.download.lock     # download flock files
42/// ```
43pub struct GlobalCache {
44    /// Root of the layer cache directory (`~/.microsandbox/cache/layers/`).
45    layers_dir: PathBuf,
46
47    /// Root of the image metadata directory (`~/.microsandbox/cache/images/`).
48    images_dir: PathBuf,
49}
50
51/// Cached metadata for a pulled image reference.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct CachedImageMetadata {
54    /// Content-addressable digest of the resolved manifest.
55    pub manifest_digest: String,
56    /// Content-addressable digest of the config blob.
57    pub config_digest: String,
58    /// Parsed OCI image configuration.
59    pub config: ImageConfig,
60    /// Layer metadata in bottom-to-top order.
61    pub layers: Vec<CachedLayerMetadata>,
62}
63
64/// Cached metadata for a single layer descriptor.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct CachedLayerMetadata {
67    /// Compressed layer digest from the manifest.
68    pub digest: String,
69    /// OCI media type of the layer blob.
70    pub media_type: Option<String>,
71    /// Compressed blob size in bytes.
72    pub size_bytes: Option<u64>,
73    /// Uncompressed diff ID from the image config.
74    pub diff_id: String,
75}
76
77//--------------------------------------------------------------------------------------------------
78// Methods
79//--------------------------------------------------------------------------------------------------
80
81impl GlobalCache {
82    /// Create a new GlobalCache using the provided cache directory.
83    ///
84    /// Creates `<cache_dir>/layers/` if it doesn't exist.
85    pub fn new(cache_dir: &Path) -> ImageResult<Self> {
86        let layers_dir = cache_dir.join(LAYERS_DIR);
87        let images_dir = cache_dir.join(IMAGES_DIR);
88        std::fs::create_dir_all(&layers_dir).map_err(|e| ImageError::Cache {
89            path: layers_dir.clone(),
90            source: e,
91        })?;
92        std::fs::create_dir_all(&images_dir).map_err(|e| ImageError::Cache {
93            path: images_dir.clone(),
94            source: e,
95        })?;
96        Ok(Self {
97            layers_dir,
98            images_dir,
99        })
100    }
101
102    /// Root layer cache directory.
103    pub fn layers_dir(&self) -> &Path {
104        &self.layers_dir
105    }
106
107    /// Path to the compressed tarball for a layer.
108    pub fn tar_path(&self, digest: &Digest) -> PathBuf {
109        self.layers_dir
110            .join(format!("{}.tar.gz", digest.to_path_safe()))
111    }
112
113    /// Path to the partial download file for a layer.
114    pub fn part_path(&self, digest: &Digest) -> PathBuf {
115        self.layers_dir
116            .join(format!("{}.tar.gz.part", digest.to_path_safe()))
117    }
118
119    /// Path to the extracted layer directory.
120    pub fn extracted_dir(&self, digest: &Digest) -> PathBuf {
121        self.layers_dir
122            .join(format!("{}.extracted", digest.to_path_safe()))
123    }
124
125    /// Path to the in-progress extraction temp directory.
126    pub fn extracting_dir(&self, digest: &Digest) -> PathBuf {
127        self.layers_dir
128            .join(format!("{}.extracting", digest.to_path_safe()))
129    }
130
131    /// Path to the binary sidecar index for a layer.
132    pub fn index_path(&self, digest: &Digest) -> PathBuf {
133        self.layers_dir
134            .join(format!("{}.index", digest.to_path_safe()))
135    }
136
137    /// Path to the pending implicit-dir fixup sidecar for a layer.
138    pub fn implicit_dirs_path(&self, digest: &Digest) -> PathBuf {
139        self.layers_dir
140            .join(format!("{}.implicit_dirs", digest.to_path_safe()))
141    }
142
143    /// Path to the extraction lock file for a layer.
144    pub fn lock_path(&self, digest: &Digest) -> PathBuf {
145        self.layers_dir
146            .join(format!("{}.lock", digest.to_path_safe()))
147    }
148
149    /// Path to the download lock file for a layer.
150    pub fn download_lock_path(&self, digest: &Digest) -> PathBuf {
151        self.layers_dir
152            .join(format!("{}.download.lock", digest.to_path_safe()))
153    }
154
155    /// Path to the pull lock file for an image reference.
156    pub fn image_lock_path(&self, reference: &Reference) -> PathBuf {
157        self.images_dir
158            .join(format!("{}.lock", image_cache_key(reference)))
159    }
160
161    /// Check if a layer is fully extracted (`.complete` marker present).
162    pub fn is_extracted(&self, digest: &Digest) -> bool {
163        self.extracted_dir(digest).join(COMPLETE_MARKER).exists()
164    }
165
166    /// Check if all given layer digests are fully extracted.
167    pub fn all_layers_extracted(&self, digests: &[Digest]) -> bool {
168        digests.iter().all(|d| self.is_extracted(d))
169    }
170
171    /// Read cached metadata for an image reference.
172    pub fn read_image_metadata(
173        &self,
174        reference: &Reference,
175    ) -> ImageResult<Option<CachedImageMetadata>> {
176        let path = self.image_metadata_path(reference);
177
178        let data = match std::fs::read_to_string(&path) {
179            Ok(data) => data,
180            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
181            Err(e) => return Err(ImageError::Cache { path, source: e }),
182        };
183
184        match serde_json::from_str::<CachedImageMetadata>(&data) {
185            Ok(metadata) => Ok(Some(metadata)),
186            Err(e) => {
187                tracing::warn!(path = %path.display(), error = %e, "corrupt image metadata cache, ignoring");
188                Ok(None)
189            }
190        }
191    }
192
193    /// Write cached metadata for an image reference.
194    pub(crate) fn write_image_metadata(
195        &self,
196        reference: &Reference,
197        metadata: &CachedImageMetadata,
198    ) -> ImageResult<()> {
199        let path = self.image_metadata_path(reference);
200        let temp_path = path.with_extension("json.part");
201        let payload = serde_json::to_vec(metadata).map_err(|e| {
202            ImageError::ConfigParse(format!("failed to serialize cached image metadata: {e}"))
203        })?;
204
205        std::fs::write(&temp_path, payload).map_err(|e| ImageError::Cache {
206            path: temp_path.clone(),
207            source: e,
208        })?;
209        std::fs::rename(&temp_path, &path).map_err(|e| ImageError::Cache { path, source: e })?;
210
211        Ok(())
212    }
213
214    /// Delete cached metadata for an image reference.
215    pub fn delete_image_metadata(&self, reference: &Reference) -> ImageResult<()> {
216        let path = self.image_metadata_path(reference);
217        match std::fs::remove_file(&path) {
218            Ok(()) => Ok(()),
219            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
220            Err(e) => Err(ImageError::Cache { path, source: e }),
221        }
222    }
223
224    /// Path to the cached metadata file for an image reference.
225    fn image_metadata_path(&self, reference: &Reference) -> PathBuf {
226        self.images_dir
227            .join(format!("{}.json", image_cache_key(reference)))
228    }
229}
230
231//--------------------------------------------------------------------------------------------------
232// Functions
233//--------------------------------------------------------------------------------------------------
234
235fn image_cache_key(reference: &Reference) -> String {
236    let mut hasher = Sha256::new();
237    hasher.update(reference.to_string().as_bytes());
238    hex::encode(hasher.finalize())
239}