specfold_core/files/
hash_cache.rs1use super::hash;
6use crate::config::paths;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::time::UNIX_EPOCH;
12
13const FORMAT: &str = "specfold-hashes-1";
14
15#[derive(Serialize, Deserialize, Clone)]
16struct Entry {
17 mtime_ns: u128,
18 size: u64,
19 sha256: String,
20}
21
22#[derive(Serialize, Deserialize, Default)]
23struct Stored {
24 format: String,
25 entries: HashMap<String, Entry>,
26}
27
28pub struct HashCache {
29 file: Option<PathBuf>,
30 stored: Stored,
31 dirty: bool,
32}
33
34impl HashCache {
35 pub fn open(root: &Path) -> HashCache {
36 let dir = if cfg!(test) { None } else { paths::cache_dir() };
37 let key = Sha256::digest(root.to_string_lossy().as_bytes());
38 let name: String = key[..8].iter().map(|b| format!("{b:02x}")).collect();
39 let file = dir.map(|d| d.join(format!("hashes-{name}.json")));
40 if let Some(f) = &file {
41 crate::config::cache_gc::touch(f);
42 }
43 let stored = file
44 .as_ref()
45 .and_then(|f| std::fs::read(f).ok())
46 .and_then(|b| serde_json::from_slice::<Stored>(&b).ok())
47 .filter(|s| s.format == FORMAT)
48 .unwrap_or(Stored { format: FORMAT.into(), entries: HashMap::new() });
49 HashCache { file, stored, dirty: false }
50 }
51
52 pub fn file(&mut self, path: &Path) -> std::io::Result<(String, u64)> {
54 let meta = std::fs::metadata(path)?;
55 let mtime_ns = meta.modified()?.duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
56 let key = path.to_string_lossy().into_owned();
57 if let Some(e) = self.stored.entries.get(&key).filter(|e| e.mtime_ns == mtime_ns && e.size == meta.len()) {
58 return Ok((e.sha256.clone(), e.size));
59 }
60 let (sha256, size) = hash::file(path)?;
61 self.stored.entries.insert(key, Entry { mtime_ns, size, sha256: sha256.clone() });
62 self.dirty = true;
63 Ok((sha256, size))
64 }
65
66 pub fn save(&self) {
68 let (Some(file), true) = (&self.file, self.dirty) else { return };
69 let Some(dir) = file.parent() else { return };
70 if std::fs::create_dir_all(dir).is_err() {
71 return;
72 }
73 let tmp = file.with_extension(format!("tmp{}", std::process::id()));
74 if let Ok(bytes) = serde_json::to_vec(&self.stored)
75 && std::fs::write(&tmp, bytes).is_ok()
76 {
77 let _ = std::fs::rename(&tmp, file);
78 }
79 }
80}