use dirs::cache_dir;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Serialize, Deserialize, Clone)]
pub struct CacheEntry {
pub sizes: HashMap<PathBuf, u64>,
pub total_files: usize,
pub timestamp: u64,
pub base_path: PathBuf,
}
pub struct Cache {
cache_dir: PathBuf,
}
impl Cache {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let cache_dir = cache_dir()
.ok_or("Could not determine cache directory")?
.join("rudu");
fs::create_dir_all(&cache_dir)?;
Ok(Cache { cache_dir })
}
#[cfg(test)]
pub fn with_dir(cache_dir: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
fs::create_dir_all(&cache_dir)?;
Ok(Cache { cache_dir })
}
fn cache_key(&self, path: &Path) -> String {
use ahash::AHasher;
use std::hash::{Hash, Hasher};
let mut hasher = AHasher::default();
path.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
fn cache_file_path(&self, path: &Path) -> PathBuf {
self.cache_dir
.join(format!("{}.json", self.cache_key(path)))
}
fn is_cache_valid(&self, entry: &CacheEntry, max_age_seconds: u64) -> bool {
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
current_time - entry.timestamp <= max_age_seconds
}
pub fn store(
&self,
path: &Path,
sizes: &HashMap<PathBuf, u64>,
total_files: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let entry = CacheEntry {
sizes: sizes.clone(),
total_files,
timestamp,
base_path: path.to_path_buf(),
};
let cache_file = self.cache_file_path(path);
let json = serde_json::to_string_pretty(&entry)?;
fs::write(cache_file, json)?;
Ok(())
}
pub fn retrieve(
&self,
path: &Path,
max_age_seconds: u64,
) -> Result<Option<CacheEntry>, Box<dyn std::error::Error>> {
let cache_file = self.cache_file_path(path);
if !cache_file.exists() {
return Ok(None);
}
let json = fs::read_to_string(cache_file)?;
let entry: CacheEntry = serde_json::from_str(&json)?;
if self.is_cache_valid(&entry, max_age_seconds) {
Ok(Some(entry))
} else {
let _ = fs::remove_file(self.cache_file_path(path));
Ok(None)
}
}
pub fn can_use_for_subdir(
&self,
parent_cache: &CacheEntry,
subdir: &Path,
) -> Option<(HashMap<PathBuf, u64>, usize)> {
if !subdir.starts_with(&parent_cache.base_path) {
return None;
}
let mut filtered_sizes = HashMap::new();
let mut file_count = 0;
for (path, size) in &parent_cache.sizes {
if path.starts_with(subdir) {
filtered_sizes.insert(path.clone(), *size);
if path == subdir {
file_count = (*size / 1024).max(1) as usize; }
}
}
if !filtered_sizes.is_empty() {
Some((filtered_sizes, file_count))
} else {
None
}
}
pub fn clear(&self) -> Result<(), Box<dyn std::error::Error>> {
if self.cache_dir.exists() {
fs::remove_dir_all(&self.cache_dir)?;
fs::create_dir_all(&self.cache_dir)?;
}
Ok(())
}
pub fn cache_directory(&self) -> &Path {
&self.cache_dir
}
pub fn stats(&self) -> Result<(usize, u64), Box<dyn std::error::Error>> {
let mut count = 0;
let mut total_size = 0;
if self.cache_dir.exists() {
for entry in fs::read_dir(&self.cache_dir)? {
let entry = entry?;
if entry.path().extension().and_then(|s| s.to_str()) == Some("json") {
count += 1;
total_size += entry.metadata()?.len();
}
}
}
Ok((count, total_size))
}
}
impl Default for Cache {
fn default() -> Self {
Self::new().expect("Failed to create cache")
}
}