1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11use std::time::{SystemTime, UNIX_EPOCH};
12use thiserror::Error;
13
14#[derive(Error, Debug)]
16pub enum CacheError {
17 #[error("缓存操作失败: {0}")]
18 OperationFailed(String),
19 #[error("IO 错误: {0}")]
20 Io(#[from] std::io::Error),
21 #[error("序列化错误: {0}")]
22 Serialization(#[from] serde_json::Error),
23}
24
25pub type CacheResult<T> = Result<T, CacheError>;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29struct CacheItem<T> {
30 value: T,
31 expires_at: Option<u64>, }
33
34pub struct FsCacheStore {
38 cache_dir: PathBuf,
40 default_ttl: i64,
42 memory_cache: parking_lot::RwLock<HashMap<String, (serde_json::Value, Option<u64>)>>,
44}
45
46impl FsCacheStore {
47 pub fn new(options: CacheOptions) -> CacheResult<Self> {
55 let cache_dir = PathBuf::from(options.path.unwrap_or_else(|| "cache".to_string()));
56 let default_ttl = options.ttl.unwrap_or(-1);
57
58 if !cache_dir.exists() {
60 fs::create_dir_all(&cache_dir)?;
61 }
62
63 Ok(Self {
64 cache_dir,
65 default_ttl,
66 memory_cache: parking_lot::RwLock::new(HashMap::new()),
67 })
68 }
69
70 pub fn get<T>(&self, key: &str) -> CacheResult<Option<T>>
74 where
75 T: for<'de> Deserialize<'de>,
76 {
77 {
79 let memory = self.memory_cache.read();
80 if let Some((value, expires_at)) = memory.get(key) {
81 if let Some(expires) = *expires_at {
83 let now = SystemTime::now()
84 .duration_since(UNIX_EPOCH)
85 .unwrap()
86 .as_secs();
87 if now >= expires {
88 drop(memory);
90 let mut memory = self.memory_cache.write();
91 memory.remove(key);
92 } else {
93 let value: T = serde_json::from_value(value.clone())?;
95 return Ok(Some(value));
96 }
97 } else {
98 let value: T = serde_json::from_value(value.clone())?;
100 return Ok(Some(value));
101 }
102 }
103 }
104
105 let file_path = self.get_file_path(key);
107 if !file_path.exists() {
108 return Ok(None);
109 }
110
111 let content = fs::read_to_string(&file_path)?;
112 let item: CacheItem<T> = serde_json::from_str(&content)?;
113
114 if let Some(expires_at) = item.expires_at {
116 let now = SystemTime::now()
117 .duration_since(UNIX_EPOCH)
118 .unwrap()
119 .as_secs();
120 if now >= expires_at {
121 let _ = fs::remove_file(&file_path);
123 return Ok(None);
124 }
125 }
126
127 Ok(Some(item.value))
133 }
134
135 pub fn set<T>(&self, key: &str, value: T, ttl: Option<i64>) -> CacheResult<()>
139 where
140 T: Serialize,
141 {
142 let ttl_seconds = ttl.unwrap_or(self.default_ttl);
143 let expires_at = if ttl_seconds > 0 {
144 let now = SystemTime::now()
145 .duration_since(UNIX_EPOCH)
146 .unwrap()
147 .as_secs();
148 Some(now + ttl_seconds as u64)
149 } else {
150 None };
152
153 let item = CacheItem { value, expires_at };
154
155 let file_path = self.get_file_path(key);
157 if let Some(parent) = file_path.parent() {
158 fs::create_dir_all(parent)?;
159 }
160 let content = serde_json::to_string(&item)?;
161 fs::write(&file_path, content)?;
162
163 Ok(())
169 }
170
171 pub fn del(&self, key: &str) -> CacheResult<()> {
175 {
177 let mut memory = self.memory_cache.write();
178 memory.remove(key);
179 }
180
181 let file_path = self.get_file_path(key);
183 if file_path.exists() {
184 fs::remove_file(&file_path)?;
185 }
186
187 Ok(())
188 }
189
190 pub fn reset(&self) -> CacheResult<()> {
194 {
196 let mut memory = self.memory_cache.write();
197 memory.clear();
198 }
199
200 if self.cache_dir.exists() {
202 for entry in fs::read_dir(&self.cache_dir)? {
203 let entry = entry?;
204 let path = entry.path();
205 if path.is_file() {
206 fs::remove_file(path)?;
207 }
208 }
209 }
210
211 Ok(())
212 }
213
214 fn get_file_path(&self, key: &str) -> PathBuf {
216 use std::collections::hash_map::DefaultHasher;
218 use std::hash::{Hash, Hasher};
219
220 let mut hasher = DefaultHasher::new();
221 key.hash(&mut hasher);
222 let hash = hasher.finish();
223 let filename = format!("{:x}.json", hash);
224
225 self.cache_dir.join(filename)
226 }
227}
228
229#[derive(Debug, Clone, Default)]
231pub struct CacheOptions {
232 pub path: Option<String>,
234 pub ttl: Option<i64>,
236}
237
238pub fn create_cache_store(options: CacheOptions) -> CacheResult<FsCacheStore> {
242 FsCacheStore::new(options)
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[tokio::test]
250 async fn test_cache_store() {
251 let temp_dir = std::env::temp_dir().join("cool_plugin_cache_test");
252 let _ = fs::remove_dir_all(&temp_dir);
253
254 let options = CacheOptions {
255 path: Some(temp_dir.to_string_lossy().to_string()),
256 ttl: Some(60),
257 };
258
259 let store = FsCacheStore::new(options).unwrap();
260
261 store.set("test_key", "test_value", None).unwrap();
263 let value: Option<String> = store.get("test_key").unwrap();
264 assert_eq!(value, Some("test_value".to_string()));
265
266 store.del("test_key").unwrap();
268 let value: Option<String> = store.get("test_key").unwrap();
269 assert_eq!(value, None);
270
271 let _ = fs::remove_dir_all(&temp_dir);
273 }
274}