1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use crate::error::Result;
6use crate::logger::{log, warn};
7
8const CACHE_FILE_NAME: &str = "cache.json";
9
10pub struct CacheManager;
14
15impl CacheManager {
16 pub fn write_resolver_cache(data: &[u8]) -> Result<PathBuf> {
20 let dir = std::env::temp_dir().join(format!("skipper-{}", unique()));
21 std::fs::create_dir_all(&dir)?;
22 let file = dir.join(CACHE_FILE_NAME);
23 std::fs::write(&file, data)?;
24 log(format!("wrote resolver cache to {}", file.display()));
25 Ok(file)
26 }
27
28 pub fn write_discovered_ids(dir: &Path, ids: &[String]) -> Result<()> {
30 let file = dir.join(format!("{}.json", unique()));
31 std::fs::write(file, serde_json::to_vec(ids)?)?;
32 Ok(())
33 }
34
35 pub fn merge_discovered_ids(dir: &Path) -> Vec<String> {
37 let mut files: Vec<PathBuf> = std::fs::read_dir(dir)
38 .map(|rd| rd.filter_map(|e| e.ok().map(|e| e.path())).collect())
39 .unwrap_or_default();
40 files.retain(|p| {
41 p.extension().is_some_and(|e| e == "json")
42 && p.file_name().is_some_and(|n| n != CACHE_FILE_NAME)
43 });
44 files.sort();
45
46 let mut seen = HashSet::new();
47 let mut out = Vec::new();
48 for f in files {
49 match std::fs::read(&f)
50 .map_err(crate::Error::from)
51 .and_then(|b| serde_json::from_slice::<Vec<String>>(&b).map_err(Into::into))
52 {
53 Ok(ids) => out.extend(ids.into_iter().filter(|id| seen.insert(id.clone()))),
54 Err(e) => warn(format!("cannot read discovered file {}: {e}", f.display())),
55 }
56 }
57 out
58 }
59
60 pub fn cleanup(dir: &Path) {
61 let _ = std::fs::remove_dir_all(dir);
62 }
63}
64
65fn unique() -> String {
67 use std::sync::atomic::{AtomicU64, Ordering};
68 static COUNTER: AtomicU64 = AtomicU64::new(0);
69 let nanos = SystemTime::now()
70 .duration_since(UNIX_EPOCH)
71 .map(|d| d.as_nanos())
72 .unwrap_or_default();
73 format!(
74 "{}-{nanos}-{:08x}",
75 std::process::id(),
76 COUNTER.fetch_add(1, Ordering::Relaxed)
77 )
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn roundtrip_and_merge() {
86 let file = CacheManager::write_resolver_cache(b"{}").unwrap();
87 let dir = file.parent().unwrap();
88 CacheManager::write_discovered_ids(dir, &["a".into(), "b".into()]).unwrap();
89 CacheManager::write_discovered_ids(dir, &["b".into(), "c".into()]).unwrap();
90 std::fs::write(dir.join("broken.json"), "nope").unwrap();
91 let mut ids = CacheManager::merge_discovered_ids(dir);
92 ids.sort();
93 assert_eq!(ids, ["a", "b", "c"]);
94 CacheManager::cleanup(dir);
95 assert!(!dir.exists());
96 }
97}