Skip to main content

qk_cache/
projections.rs

1//! Change projection manifests, kept under a key of everything that decides
2//! them, so that projecting the same revision again reads the manifest
3//! instead of checking out the revision and running the adapter.
4
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8use anyhow::Result;
9use serde::{Deserialize, Serialize};
10
11#[derive(Serialize, Deserialize)]
12struct Stored {
13    version: u32,
14    key: String,
15    artifacts: BTreeMap<String, String>,
16}
17
18/// The key of a manifest decided by `parts`.
19pub fn key(parts: &impl Serialize) -> Result<String> {
20    Ok(blake3::hash(&serde_json::to_vec(parts)?)
21        .to_hex()
22        .to_string())
23}
24
25fn location(cache: &Path, key: &str) -> PathBuf {
26    cache.join("projections").join(format!("{key}.json"))
27}
28
29/// The artifacts kept under `key`, marked as used so eviction keeps them; none
30/// when there are none or they cannot be read.
31pub fn load(cache: &Path, key: &str) -> Option<BTreeMap<String, String>> {
32    let path = location(cache, key);
33    let stored: Stored = serde_json::from_slice(&std::fs::read(&path).ok()?).ok()?;
34    if stored.version != 1 || stored.key != key {
35        return None;
36    }
37    crate::evict::touch(&path);
38    Some(stored.artifacts)
39}
40
41/// Keeps `artifacts` under `key`, in place of any manifest kept there before.
42pub fn keep(cache: &Path, key: &str, artifacts: &BTreeMap<String, String>) -> Result<()> {
43    // The whole layout, so a cache that only ever kept manifests is pruned too
44    crate::Cache::new(cache.to_path_buf()).initialize()?;
45    std::fs::create_dir_all(cache.join("projections"))?;
46    crate::record::publish(
47        cache,
48        &location(cache, key),
49        &Stored {
50            version: 1,
51            key: key.to_owned(),
52            artifacts: artifacts.clone(),
53        },
54    )
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use std::time::SystemTime;
61
62    fn age(cache: &Path, key: &str) -> SystemTime {
63        let file = std::fs::File::options()
64            .write(true)
65            .open(location(cache, key))
66            .unwrap();
67        file.set_modified(SystemTime::UNIX_EPOCH).unwrap();
68        SystemTime::UNIX_EPOCH
69    }
70
71    fn used(cache: &Path, key: &str) -> SystemTime {
72        std::fs::metadata(location(cache, key))
73            .unwrap()
74            .modified()
75            .unwrap()
76    }
77
78    #[test]
79    fn a_manifest_read_again_counts_as_used() {
80        let cache = tempfile::tempdir().unwrap();
81        let artifacts = BTreeMap::from([("a.js".to_owned(), "sha256:1".to_owned())]);
82        keep(cache.path(), "k", &artifacts).unwrap();
83        let aged = age(cache.path(), "k");
84
85        assert_eq!(load(cache.path(), "k"), Some(artifacts));
86        assert!(used(cache.path(), "k") > aged);
87    }
88
89    #[test]
90    fn a_cache_of_manifests_alone_ages_them_out() {
91        let cache = tempfile::tempdir().unwrap();
92        keep(cache.path(), "old", &BTreeMap::new()).unwrap();
93        age(cache.path(), "old");
94        keep(cache.path(), "new", &BTreeMap::new()).unwrap();
95
96        crate::prune(cache.path(), u64::MAX).unwrap();
97        assert!(!location(cache.path(), "old").exists());
98        assert!(location(cache.path(), "new").exists());
99    }
100}