1use 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
18pub 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
29pub 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
41pub fn keep(cache: &Path, key: &str, artifacts: &BTreeMap<String, String>) -> Result<()> {
43 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}