spin_loader/
cache.rs

1//! Cache for OCI registry entities.
2
3use anyhow::{ensure, Context, Result};
4
5use std::{
6    path::PathBuf,
7    sync::atomic::{AtomicBool, Ordering},
8};
9
10use crate::fs::{create_dir_all, write_file};
11
12const CONFIG_DIR: &str = "spin";
13const REGISTRY_CACHE_DIR: &str = "registry";
14const MANIFESTS_DIR: &str = "manifests";
15const WASM_DIR: &str = "wasm";
16const DATA_DIR: &str = "data";
17
18/// Cache for registry entities.
19#[derive(Debug)]
20pub struct Cache {
21    /// Root directory for the cache instance.
22    root: PathBuf,
23    /// Whether the cache directories have been checked to exist (and
24    /// created if necessary).
25    dirs_ensured_once: AtomicBool,
26}
27
28impl Cache {
29    /// Create a new cache given an optional root directory.
30    pub async fn new(root: Option<PathBuf>) -> Result<Self> {
31        let root = match root {
32            Some(root) => root,
33            None => dirs::cache_dir()
34                .context("cannot get cache directory")?
35                .join(CONFIG_DIR),
36        };
37        let root = root.join(REGISTRY_CACHE_DIR);
38
39        Ok(Self {
40            root,
41            dirs_ensured_once: AtomicBool::new(false),
42        })
43    }
44
45    /// The manifests directory for the current cache.
46    pub fn manifests_dir(&self) -> PathBuf {
47        self.root.join(MANIFESTS_DIR)
48    }
49
50    /// The Wasm bytes directory for the current cache.
51    fn wasm_dir(&self) -> PathBuf {
52        self.root.join(WASM_DIR)
53    }
54
55    /// The data directory for the current cache.
56    fn data_dir(&self) -> PathBuf {
57        self.root.join(DATA_DIR)
58    }
59
60    /// Return the path to a wasm file given its digest.
61    pub fn wasm_file(&self, digest: impl AsRef<str>) -> Result<PathBuf> {
62        // Check the expected wasm directory first; else check the data directory as a fallback.
63        // (Layers with unknown media types are currently saved to the data directory in client.pull())
64        // This adds a bit of futureproofing for fetching wasm layers with different/updated media types
65        // (see WASM_LAYER_MEDIA_TYPE, which is subject to change in future versions).
66        let mut path = self.wasm_path(&digest);
67        if !path.exists() {
68            path = self.data_path(&digest);
69        }
70        ensure!(
71            path.exists(),
72            "cannot find wasm file for digest {}",
73            digest.as_ref()
74        );
75        Ok(path)
76    }
77
78    /// Return the path to a data file given its digest.
79    pub fn data_file(&self, digest: impl AsRef<str>) -> Result<PathBuf> {
80        let path = self.data_path(&digest);
81        ensure!(
82            path.exists(),
83            "cannot find data file for digest {}",
84            digest.as_ref()
85        );
86        Ok(path)
87    }
88
89    /// Write the contents in the cache's wasm directory.
90    pub async fn write_wasm(&self, bytes: impl AsRef<[u8]>, digest: impl AsRef<str>) -> Result<()> {
91        self.ensure_dirs().await?;
92        write_file(&self.wasm_path(digest), bytes.as_ref()).await?;
93        Ok(())
94    }
95
96    /// Write the contents in the cache's data directory.
97    pub async fn write_data(&self, bytes: impl AsRef<[u8]>, digest: impl AsRef<str>) -> Result<()> {
98        self.ensure_dirs().await?;
99        write_file(&self.data_path(digest), bytes.as_ref()).await?;
100        Ok(())
101    }
102
103    /// The path of contents in the cache's wasm directory, which may or may not exist.
104    pub fn wasm_path(&self, digest: impl AsRef<str>) -> PathBuf {
105        self.wasm_dir().join(safe_name(digest).as_ref())
106    }
107
108    /// The path of contents in the cache's wasm directory, which may or may not exist.
109    pub fn data_path(&self, digest: impl AsRef<str>) -> PathBuf {
110        self.data_dir().join(safe_name(digest).as_ref())
111    }
112
113    /// Ensure the expected configuration directories are found in the root.
114    ///
115    /// ```text
116    /// └── <configuration-root>
117    ///     └── registry
118    ///             └──manifests
119    ///             └──wasm
120    ///             └──data
121    /// ```
122    pub async fn ensure_dirs(&self) -> Result<()> {
123        tracing::debug!("using cache root directory {}", self.root.display());
124
125        // We don't care about ordering as this function is idempotent -
126        // we are using an Atomic only for interior mutability.
127        if self.dirs_ensured_once.load(Ordering::Relaxed) {
128            return Ok(());
129        }
130
131        let root = &self.root;
132
133        let p = root.join(MANIFESTS_DIR);
134        if !p.is_dir() {
135            create_dir_all(&p).await.with_context(|| {
136                format!("failed to create manifests directory `{}`", p.display())
137            })?;
138        }
139
140        let p = root.join(WASM_DIR);
141        if !p.is_dir() {
142            create_dir_all(&p)
143                .await
144                .with_context(|| format!("failed to create wasm directory `{}`", p.display()))?;
145        }
146
147        let p = root.join(DATA_DIR);
148        if !p.is_dir() {
149            create_dir_all(&p)
150                .await
151                .with_context(|| format!("failed to create assets directory `{}`", p.display()))?;
152        }
153
154        self.dirs_ensured_once.store(true, Ordering::Relaxed);
155
156        Ok(())
157    }
158}
159
160#[cfg(windows)]
161fn safe_name(digest: impl AsRef<str>) -> impl AsRef<std::path::Path> {
162    digest.as_ref().replace(':', "_")
163}
164
165#[cfg(not(windows))]
166fn safe_name(digest: impl AsRef<str>) -> impl AsRef<str> {
167    digest
168}
169
170#[cfg(test)]
171mod test {
172    use spin_common::sha256::hex_digest_from_bytes;
173
174    use super::*;
175
176    #[tokio::test]
177    async fn accepts_prefixed_digests() -> anyhow::Result<()> {
178        let temp_dir = tempfile::tempdir()?;
179        let cache = Cache::new(Some(temp_dir.path().to_owned())).await?;
180
181        let wasm = "Wasm".as_bytes();
182        let digest = format!("sha256:{}", hex_digest_from_bytes(wasm));
183        cache.write_wasm(wasm, &digest).await?;
184        assert_eq!(wasm, std::fs::read(cache.wasm_path(&digest))?);
185
186        let data = "hello".as_bytes();
187        let digest = format!("sha256:{}", hex_digest_from_bytes(data));
188        cache.write_data(data, &digest).await?;
189        assert_eq!(data, std::fs::read(cache.data_path(&digest))?);
190
191        Ok(())
192    }
193}