use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use base64::Engine;
use crate::error::{CacheError, Result};
#[derive(Debug)]
pub struct Cache {
dir: PathBuf,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CacheEntry {
expires_at: u64,
value: String,
}
impl Cache {
pub fn new(dir: &Path) -> Self {
Self {
dir: dir.to_path_buf(),
}
}
pub fn default_for(app_name: &str) -> Option<Self> {
default_cache_dir(app_name).map(|dir| Self::new(&dir))
}
pub fn set(&self, key: &str, value: &[u8], ttl: Duration) -> Result<()> {
std::fs::create_dir_all(&self.dir).map_err(CacheError::from)?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let expires_at = now.as_secs() + ttl.as_secs();
let entry = CacheEntry {
expires_at,
value: base64::engine::general_purpose::STANDARD.encode(value),
};
let path = self.key_path(key);
let json = serde_json::to_vec(&entry).map_err(CacheError::from)?;
std::fs::write(&path, json).map_err(CacheError::from)?;
tracing::debug!(key, expires_at, "cache entry written");
Ok(())
}
pub fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
let path = self.key_path(key);
let data = match std::fs::read(&path) {
Ok(data) => data,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(CacheError::from(e).into()),
};
let entry: CacheEntry = serde_json::from_slice(&data).map_err(CacheError::Json)?;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if now >= entry.expires_at {
tracing::debug!(key, "cache entry expired");
let _ = std::fs::remove_file(&path);
return Ok(None);
}
let value = base64::engine::general_purpose::STANDARD
.decode(&entry.value)
.map_err(CacheError::from)?;
Ok(Some(value))
}
pub fn remove(&self, key: &str) -> Result<()> {
let path = self.key_path(key);
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(CacheError::from(e).into()),
}
}
pub fn clear(&self) -> Result<()> {
if self.dir.exists() {
for entry in std::fs::read_dir(&self.dir)
.map_err(CacheError::from)?
.flatten()
{
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("json") {
let _ = std::fs::remove_file(&path);
}
}
}
Ok(())
}
pub fn dir(&self) -> &Path {
&self.dir
}
fn key_path(&self, key: &str) -> PathBuf {
let safe_key = key.replace(|c: char| !c.is_alphanumeric() && c != '-' && c != '_', "_");
self.dir.join(format!("{safe_key}.json"))
}
}
pub fn default_cache_dir(app_name: &str) -> Option<PathBuf> {
let proj_dirs = directories::ProjectDirs::from("", "", app_name)?;
Some(proj_dirs.cache_dir().join("librebar"))
}