use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::{config::CacheConfig, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheEntry<T> {
pub data: T,
pub created_at: SystemTime,
pub expires_at: SystemTime,
pub metadata: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct FileCache {
config: CacheConfig,
}
impl FileCache {
pub fn new(config: CacheConfig) -> Self {
Self { config }
}
pub async fn init(&self) -> Result<()> {
fs::create_dir_all(&self.config.cache_dir).await?;
Ok(())
}
pub async fn store<T>(&self, key: &str, data: T) -> Result<()>
where
T: Serialize,
{
let entry = CacheEntry {
data,
created_at: SystemTime::now(),
expires_at: SystemTime::now() + Duration::from_secs(self.config.ttl_seconds),
metadata: None,
};
let path = self.cache_path(key);
let content = serde_json::to_string(&entry)?;
fs::write(path, content).await?;
Ok(())
}
pub async fn get<T>(&self, key: &str) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de>,
{
let path = self.cache_path(key);
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(path).await?;
let entry: CacheEntry<T> = serde_json::from_str(&content)?;
if entry.expires_at < SystemTime::now() {
self.remove(key).await?;
return Ok(None);
}
Ok(Some(entry.data))
}
pub async fn remove(&self, key: &str) -> Result<()> {
let path = self.cache_path(key);
if path.exists() {
fs::remove_file(path).await?;
}
Ok(())
}
pub async fn clear(&self) -> Result<()> {
if self.config.cache_dir.exists() {
fs::remove_dir_all(&self.config.cache_dir).await?;
fs::create_dir_all(&self.config.cache_dir).await?;
}
Ok(())
}
pub async fn exists(&self, key: &str) -> Result<bool> {
let path = self.cache_path(key);
if !path.exists() {
return Ok(false);
}
let content = fs::read_to_string(path).await?;
let entry: CacheEntry<serde_json::Value> = serde_json::from_str(&content)?;
Ok(entry.expires_at > SystemTime::now())
}
pub async fn stats(&self) -> Result<CacheStats> {
let mut total_size = 0u64;
let mut entry_count = 0u32;
let mut expired_count = 0u32;
let mut entries = fs::read_dir(&self.config.cache_dir).await?;
while let Some(entry) = entries.next_entry().await? {
if entry.file_type().await?.is_file() {
entry_count += 1;
let metadata = entry.metadata().await?;
total_size += metadata.len();
if let Ok(content) = fs::read_to_string(entry.path()).await {
if let Ok(cache_entry) =
serde_json::from_str::<CacheEntry<serde_json::Value>>(&content)
{
if cache_entry.expires_at < SystemTime::now() {
expired_count += 1;
}
}
}
}
}
Ok(CacheStats {
total_size_bytes: total_size,
entry_count,
expired_count,
cache_dir: self.config.cache_dir.clone(),
})
}
fn cache_path(&self, key: &str) -> PathBuf {
let key_hash = format!("{:x}", md5::compute(key.as_bytes()));
self.config.cache_dir.join(format!("{}.json", key_hash))
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub total_size_bytes: u64,
pub entry_count: u32,
pub expired_count: u32,
pub cache_dir: PathBuf,
}