use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CacheError {
#[error("缓存操作失败: {0}")]
OperationFailed(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("序列化错误: {0}")]
Serialization(#[from] serde_json::Error),
}
pub type CacheResult<T> = Result<T, CacheError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CacheItem<T> {
value: T,
expires_at: Option<u64>, }
pub struct FsCacheStore {
cache_dir: PathBuf,
default_ttl: i64,
memory_cache: parking_lot::RwLock<HashMap<String, (serde_json::Value, Option<u64>)>>,
}
impl FsCacheStore {
pub fn new(options: CacheOptions) -> CacheResult<Self> {
let cache_dir = PathBuf::from(options.path.unwrap_or_else(|| "cache".to_string()));
let default_ttl = options.ttl.unwrap_or(-1);
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)?;
}
Ok(Self {
cache_dir,
default_ttl,
memory_cache: parking_lot::RwLock::new(HashMap::new()),
})
}
pub fn get<T>(&self, key: &str) -> CacheResult<Option<T>>
where
T: for<'de> Deserialize<'de>,
{
{
let memory = self.memory_cache.read();
if let Some((value, expires_at)) = memory.get(key) {
if let Some(expires) = *expires_at {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if now >= expires {
drop(memory);
let mut memory = self.memory_cache.write();
memory.remove(key);
} else {
let value: T = serde_json::from_value(value.clone())?;
return Ok(Some(value));
}
} else {
let value: T = serde_json::from_value(value.clone())?;
return Ok(Some(value));
}
}
}
let file_path = self.get_file_path(key);
if !file_path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&file_path)?;
let item: CacheItem<T> = serde_json::from_str(&content)?;
if let Some(expires_at) = item.expires_at {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if now >= expires_at {
let _ = fs::remove_file(&file_path);
return Ok(None);
}
}
Ok(Some(item.value))
}
pub fn set<T>(&self, key: &str, value: T, ttl: Option<i64>) -> CacheResult<()>
where
T: Serialize,
{
let ttl_seconds = ttl.unwrap_or(self.default_ttl);
let expires_at = if ttl_seconds > 0 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Some(now + ttl_seconds as u64)
} else {
None };
let item = CacheItem { value, expires_at };
let file_path = self.get_file_path(key);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
let content = serde_json::to_string(&item)?;
fs::write(&file_path, content)?;
Ok(())
}
pub fn del(&self, key: &str) -> CacheResult<()> {
{
let mut memory = self.memory_cache.write();
memory.remove(key);
}
let file_path = self.get_file_path(key);
if file_path.exists() {
fs::remove_file(&file_path)?;
}
Ok(())
}
pub fn reset(&self) -> CacheResult<()> {
{
let mut memory = self.memory_cache.write();
memory.clear();
}
if self.cache_dir.exists() {
for entry in fs::read_dir(&self.cache_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
fs::remove_file(path)?;
}
}
}
Ok(())
}
fn get_file_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);
let hash = hasher.finish();
let filename = format!("{:x}.json", hash);
self.cache_dir.join(filename)
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheOptions {
pub path: Option<String>,
pub ttl: Option<i64>,
}
pub fn create_cache_store(options: CacheOptions) -> CacheResult<FsCacheStore> {
FsCacheStore::new(options)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_cache_store() {
let temp_dir = std::env::temp_dir().join("cool_plugin_cache_test");
let _ = fs::remove_dir_all(&temp_dir);
let options = CacheOptions {
path: Some(temp_dir.to_string_lossy().to_string()),
ttl: Some(60),
};
let store = FsCacheStore::new(options).unwrap();
store.set("test_key", "test_value", None).unwrap();
let value: Option<String> = store.get("test_key").unwrap();
assert_eq!(value, Some("test_value".to_string()));
store.del("test_key").unwrap();
let value: Option<String> = store.get("test_key").unwrap();
assert_eq!(value, None);
let _ = fs::remove_dir_all(&temp_dir);
}
}