1use 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#[derive(Debug)]
20pub struct Cache {
21    root: PathBuf,
23    dirs_ensured_once: AtomicBool,
26}
27
28impl Cache {
29    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    pub fn manifests_dir(&self) -> PathBuf {
47        self.root.join(MANIFESTS_DIR)
48    }
49
50    fn wasm_dir(&self) -> PathBuf {
52        self.root.join(WASM_DIR)
53    }
54
55    fn data_dir(&self) -> PathBuf {
57        self.root.join(DATA_DIR)
58    }
59
60    pub fn wasm_file(&self, digest: impl AsRef<str>) -> Result<PathBuf> {
62        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    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    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    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    pub fn wasm_path(&self, digest: impl AsRef<str>) -> PathBuf {
105        self.wasm_dir().join(safe_name(digest).as_ref())
106    }
107
108    pub fn data_path(&self, digest: impl AsRef<str>) -> PathBuf {
110        self.data_dir().join(safe_name(digest).as_ref())
111    }
112
113    pub async fn ensure_dirs(&self) -> Result<()> {
123        tracing::debug!("using cache root directory {}", self.root.display());
124
125        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}