use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use crate::error::{Result, SxmcError};
pub struct Cache {
dir: PathBuf,
default_ttl: Duration,
}
#[derive(Serialize, Deserialize)]
struct CacheEntry {
data: String,
created_at: u64,
ttl_secs: u64,
}
impl Cache {
pub fn new(ttl_secs: u64) -> Result<Self> {
let dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("sxmc");
std::fs::create_dir_all(&dir)
.map_err(|e| SxmcError::Other(format!("Failed to create cache dir: {}", e)))?;
Ok(Self {
dir,
default_ttl: Duration::from_secs(ttl_secs),
})
}
pub fn get(&self, key: &str) -> Option<String> {
let path = self.key_path(key);
let content = std::fs::read_to_string(&path).ok()?;
let entry: CacheEntry = serde_json::from_str(&content).ok()?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()?
.as_secs();
if now - entry.created_at > entry.ttl_secs {
let _ = std::fs::remove_file(&path);
return None;
}
Some(entry.data)
}
pub fn set(&self, key: &str, data: &str) -> Result<()> {
self.set_with_ttl(key, data, self.default_ttl.as_secs())
}
pub fn set_with_ttl(&self, key: &str, data: &str, ttl_secs: u64) -> Result<()> {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| SxmcError::Other(format!("System time error: {}", e)))?
.as_secs();
let entry = CacheEntry {
data: data.to_string(),
created_at: now,
ttl_secs,
};
let json = serde_json::to_string(&entry)?;
let path = self.key_path(key);
std::fs::write(&path, json)
.map_err(|e| SxmcError::Other(format!("Failed to write cache: {}", e)))?;
Ok(())
}
pub fn remove(&self, key: &str) {
let _ = std::fs::remove_file(self.key_path(key));
}
pub fn clear(&self) -> Result<()> {
if self.dir.exists() {
for entry in std::fs::read_dir(&self.dir)
.map_err(|e| SxmcError::Other(format!("Failed to read cache dir: {}", e)))?
.flatten()
{
let _ = std::fs::remove_file(entry.path());
}
}
Ok(())
}
fn key_path(&self, key: &str) -> PathBuf {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
self.dir.join(format!("{:x}.json", hasher.finish()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_set_get() {
let cache = Cache::new(3600).unwrap();
let key = "test_cache_set_get";
cache.set(key, "hello world").unwrap();
assert_eq!(cache.get(key), Some("hello world".to_string()));
cache.remove(key);
}
#[test]
fn test_cache_miss() {
let cache = Cache::new(3600).unwrap();
assert_eq!(cache.get("nonexistent_key_12345"), None);
}
#[test]
fn test_cache_expired() {
let cache = Cache::new(3600).unwrap();
let key = "test_cache_expired";
cache.set_with_ttl(key, "expired data", 0).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1100));
assert_eq!(cache.get(key), None);
}
}