use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
pub struct FileHashCache {
cache: HashMap<PathBuf, CacheEntry>,
}
struct CacheEntry {
hash: blake3::Hash,
mtime: SystemTime,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct DiskEntry {
hash_hex: String,
mtime_secs: u64,
}
impl FileHashCache {
pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
let map: HashMap<String, DiskEntry> = self
.cache
.iter()
.filter_map(|(path, entry)| {
let secs = entry
.mtime
.duration_since(SystemTime::UNIX_EPOCH)
.ok()?
.as_secs();
Some((
path.to_string_lossy().to_string(),
DiskEntry {
hash_hex: entry.hash.to_hex().to_string(),
mtime_secs: secs,
},
))
})
.collect();
serde_json::to_vec(&map)
}
pub fn from_json(data: &[u8]) -> Option<Self> {
let map: HashMap<String, DiskEntry> = serde_json::from_slice(data).ok()?;
let mut cache = HashMap::with_capacity(map.len());
for (path_str, entry) in map {
let hash = blake3::Hash::from_hex(&entry.hash_hex).ok()?;
let mtime = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(entry.mtime_secs);
cache.insert(PathBuf::from(path_str), CacheEntry { hash, mtime });
}
Some(Self { cache })
}
}
#[derive(Debug, PartialEq)]
pub enum ChangeStatus {
Unchanged,
Modified,
New,
Error,
}
impl Default for FileHashCache {
fn default() -> Self {
Self::new()
}
}
impl FileHashCache {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
}
}
pub fn check_file(&mut self, path: &Path) -> ChangeStatus {
let metadata = match std::fs::metadata(path) {
Ok(m) => m,
Err(_) => return ChangeStatus::Error,
};
let mtime = match metadata.modified() {
Ok(t) => t,
Err(_) => return ChangeStatus::Error,
};
if let Some(entry) = self.cache.get(path) {
if entry.mtime == mtime {
return ChangeStatus::Unchanged;
}
let hash = match hash_file(path) {
Some(h) => h,
None => return ChangeStatus::Error,
};
if hash == entry.hash {
self.cache.get_mut(path).unwrap().mtime = mtime;
return ChangeStatus::Unchanged;
}
self.cache
.insert(path.to_path_buf(), CacheEntry { hash, mtime });
return ChangeStatus::Modified;
}
let hash = match hash_file(path) {
Some(h) => h,
None => return ChangeStatus::Error,
};
self.cache
.insert(path.to_path_buf(), CacheEntry { hash, mtime });
ChangeStatus::New
}
pub fn invalidate(&mut self, path: &Path) {
self.cache.remove(path);
}
}
fn hash_file(path: &Path) -> Option<blake3::Hash> {
use std::io::Read;
let file = std::fs::File::open(path).ok()?;
let mut hasher = blake3::Hasher::new();
let mut reader = std::io::BufReader::new(file);
let mut buf = [0u8; 8192];
loop {
let n = reader.read(&mut buf).ok()?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Some(hasher.finalize())
}