use crate::core::{ForgeGuardError, ProjectConfig};
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[allow(dead_code)]
pub struct Cache {
enabled: bool,
cache_dir: PathBuf,
memory_cache: HashMap<String, String>,
max_size: u64,
ttl_seconds: u64,
}
impl Cache {
pub fn new(config: &ProjectConfig) -> Result<Self, ForgeGuardError> {
let cache_dir = config.project_root.join(&config.cache.directory);
let enabled = config.cache.enabled;
if enabled {
std::fs::create_dir_all(&cache_dir).ok();
Self::clean_expired(&cache_dir, config.cache.ttl_seconds);
}
Ok(Self {
enabled,
cache_dir,
memory_cache: HashMap::new(),
max_size: config.cache.max_size_mb,
ttl_seconds: config.cache.ttl_seconds,
})
}
pub fn store<T: Serialize>(&self, key: &str, value: &T) -> Result<(), ForgeGuardError> {
if !self.enabled {
return Ok(());
}
let json = serde_json::to_string(value)?;
let cache_file = self.cache_path(key);
std::fs::write(&cache_file, &json)?;
let _ = filetime::set_file_mtime(&cache_file, filetime::FileTime::now());
Ok(())
}
pub fn load<T: serde::de::DeserializeOwned>(
&self,
key: &str,
) -> Result<Option<T>, ForgeGuardError> {
if !self.enabled {
return Ok(None);
}
let cache_file = self.cache_path(key);
if !cache_file.exists() {
return Ok(None);
}
if let Ok(metadata) = std::fs::metadata(&cache_file) {
if let Ok(modified) = metadata.modified() {
let age = SystemTime::now()
.duration_since(modified)
.unwrap_or_default()
.as_secs();
if age > self.ttl_seconds {
std::fs::remove_file(&cache_file).ok();
return Ok(None);
}
}
}
let content = std::fs::read_to_string(&cache_file)?;
let value = serde_json::from_str(&content)?;
Ok(Some(value))
}
pub fn has(&self, key: &str) -> bool {
if !self.enabled {
return false;
}
let cache_file = self.cache_path(key);
if !cache_file.exists() {
return false;
}
if let Ok(metadata) = std::fs::metadata(&cache_file) {
if let Ok(modified) = metadata.modified() {
let age = SystemTime::now()
.duration_since(modified)
.unwrap_or_default()
.as_secs();
return age <= self.ttl_seconds;
}
}
false
}
pub fn clear(&self) -> Result<(), ForgeGuardError> {
if self.cache_dir.exists() {
std::fs::remove_dir_all(&self.cache_dir)?;
std::fs::create_dir_all(&self.cache_dir)?;
}
Ok(())
}
pub fn size(&self) -> u64 {
let mut total = 0;
if let Ok(entries) = std::fs::read_dir(&self.cache_dir) {
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
total += metadata.len();
}
}
}
total
}
fn cache_path(&self, key: &str) -> PathBuf {
let sanitized: String = key
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
self.cache_dir.join(format!("{}.json", sanitized))
}
pub fn file_hash(path: &Path) -> Result<String, ForgeGuardError> {
let content = std::fs::read(path)?;
let hash = Sha256::digest(&content);
Ok(hex::encode(hash))
}
pub fn is_file_unchanged(&self, file_path: &Path) -> bool {
if !self.enabled {
return false;
}
let hash = match Self::file_hash(file_path) {
Ok(h) => h,
Err(_) => return false,
};
let key = &format!("file_hash_{}", file_path.to_string_lossy());
match self.load::<String>(key) {
Ok(Some(cached)) => cached == hash,
_ => false,
}
}
pub fn record_file_hash(&self, file_path: &Path) -> Result<(), ForgeGuardError> {
if !self.enabled {
return Ok(());
}
let hash = Self::file_hash(file_path)?;
let key = &format!("file_hash_{}", file_path.to_string_lossy());
self.store(key, &hash)
}
pub fn filter_changed_files(&self, files: &[PathBuf]) -> Vec<PathBuf> {
if !self.enabled {
return files.to_vec();
}
files
.iter()
.filter(|f| !self.is_file_unchanged(f))
.cloned()
.collect()
}
fn clean_expired(cache_dir: &PathBuf, ttl_seconds: u64) {
if let Ok(entries) = std::fs::read_dir(cache_dir) {
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
if let Ok(modified) = metadata.modified() {
let age = SystemTime::now()
.duration_since(modified)
.unwrap_or_default()
.as_secs();
if age > ttl_seconds {
std::fs::remove_file(entry.path()).ok();
}
}
}
}
}
}
}
mod filetime {
use std::fs;
use std::time::SystemTime;
pub struct FileTime;
impl FileTime {
pub fn now() -> SystemTime {
SystemTime::now()
}
}
pub fn set_file_mtime(path: &std::path::Path, _time: SystemTime) -> std::io::Result<()> {
let _ = fs::File::options().write(true).open(path)?;
Ok(())
}
}