1use crate::error::{IoOperation, StorageError, StorageResult};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::time::{SystemTime, UNIX_EPOCH};
11use tracing::{debug, warn};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum CacheInvalidationStrategy {
16 #[serde(rename = "ttl")]
18 Ttl(u64),
19 #[serde(rename = "manual")]
21 Manual,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CacheEntry {
27 pub data: String,
29 pub created_at: u64,
31 pub strategy: CacheInvalidationStrategy,
33}
34
35impl CacheEntry {
36 pub fn new(data: String, strategy: CacheInvalidationStrategy) -> Self {
38 let created_at = SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .unwrap_or_default()
41 .as_secs();
42
43 Self {
44 data,
45 created_at,
46 strategy,
47 }
48 }
49
50 pub fn is_expired(&self) -> bool {
52 match self.strategy {
53 CacheInvalidationStrategy::Ttl(ttl_secs) => {
54 let now = SystemTime::now()
55 .duration_since(UNIX_EPOCH)
56 .unwrap_or_default()
57 .as_secs();
58 now > self.created_at + ttl_secs
59 }
60 CacheInvalidationStrategy::Manual => false,
61 }
62 }
63}
64
65pub struct CacheManager {
70 cache_dir: PathBuf,
72}
73
74impl CacheManager {
75 pub fn new(cache_dir: impl AsRef<Path>) -> StorageResult<Self> {
85 let cache_dir = cache_dir.as_ref().to_path_buf();
86
87 if !cache_dir.exists() {
89 fs::create_dir_all(&cache_dir)
90 .map_err(|e| StorageError::directory_creation_failed(cache_dir.clone(), e))?;
91 debug!("Created cache directory: {}", cache_dir.display());
92 }
93
94 Ok(Self { cache_dir })
95 }
96
97 pub fn get(&self, key: &str) -> StorageResult<Option<String>> {
107 let path = self.key_to_path(key);
108
109 if !path.exists() {
110 debug!("Cache miss for key: {}", key);
111 return Ok(None);
112 }
113
114 let content = fs::read_to_string(&path)
115 .map_err(|e| StorageError::io_error(path.clone(), IoOperation::Read, e))?;
116
117 let entry: CacheEntry = serde_json::from_str(&content).map_err(|e| {
118 StorageError::parse_error(
119 path.clone(),
120 "JSON",
121 format!("Failed to deserialize cache entry: {}", e),
122 )
123 })?;
124
125 if entry.is_expired() {
126 debug!("Cache expired for key: {}", key);
127 let _ = fs::remove_file(&path);
129 return Ok(None);
130 }
131
132 debug!("Cache hit for key: {}", key);
133 Ok(Some(entry.data))
134 }
135
136 pub fn set(
148 &self,
149 key: &str,
150 data: String,
151 strategy: CacheInvalidationStrategy,
152 ) -> StorageResult<()> {
153 let path = self.key_to_path(key);
154
155 if let Some(parent) = path.parent() {
157 if !parent.exists() {
158 fs::create_dir_all(parent).map_err(|e| {
159 StorageError::directory_creation_failed(parent.to_path_buf(), e)
160 })?;
161 }
162 }
163
164 let created_at = SystemTime::now()
165 .duration_since(UNIX_EPOCH)
166 .unwrap_or_default()
167 .as_secs();
168
169 let entry = CacheEntry {
170 data,
171 created_at,
172 strategy,
173 };
174
175 let json = serde_json::to_string_pretty(&entry).map_err(|e| {
176 StorageError::parse_error(
177 path.clone(),
178 "JSON",
179 format!("Failed to serialize cache entry: {}", e),
180 )
181 })?;
182
183 fs::write(&path, json)
184 .map_err(|e| StorageError::io_error(path.clone(), IoOperation::Write, e))?;
185
186 debug!("Cached value for key: {}", key);
187 Ok(())
188 }
189
190 pub fn invalidate(&self, key: &str) -> StorageResult<bool> {
200 let path = self.key_to_path(key);
201
202 if !path.exists() {
203 debug!("Cache entry not found for invalidation: {}", key);
204 return Ok(false);
205 }
206
207 fs::remove_file(&path)
208 .map_err(|e| StorageError::io_error(path.clone(), IoOperation::Delete, e))?;
209
210 debug!("Invalidated cache for key: {}", key);
211 Ok(true)
212 }
213
214 pub fn exists(&self, key: &str) -> StorageResult<bool> {
220 let path = self.key_to_path(key);
221
222 if !path.exists() {
223 return Ok(false);
224 }
225
226 let content = fs::read_to_string(&path)
227 .map_err(|e| StorageError::io_error(path.clone(), IoOperation::Read, e))?;
228
229 let entry: CacheEntry = serde_json::from_str(&content).map_err(|e| {
230 StorageError::parse_error(
231 path.clone(),
232 "JSON",
233 format!("Failed to deserialize cache entry: {}", e),
234 )
235 })?;
236
237 Ok(!entry.is_expired())
238 }
239
240 pub fn clear(&self) -> StorageResult<()> {
246 if !self.cache_dir.exists() {
247 return Ok(());
248 }
249
250 fs::remove_dir_all(&self.cache_dir)
251 .map_err(|e| StorageError::io_error(self.cache_dir.clone(), IoOperation::Delete, e))?;
252
253 fs::create_dir_all(&self.cache_dir)
254 .map_err(|e| StorageError::directory_creation_failed(self.cache_dir.clone(), e))?;
255
256 debug!("Cleared all cache entries");
257 Ok(())
258 }
259
260 pub fn cleanup_expired(&self) -> StorageResult<usize> {
268 if !self.cache_dir.exists() {
269 return Ok(0);
270 }
271
272 let mut cleaned = 0;
273
274 for entry in fs::read_dir(&self.cache_dir)
275 .map_err(|e| StorageError::io_error(self.cache_dir.clone(), IoOperation::Read, e))?
276 {
277 let entry = entry.map_err(|e| {
278 StorageError::io_error(self.cache_dir.clone(), IoOperation::Read, e)
279 })?;
280
281 let path = entry.path();
282
283 if path.is_file() {
284 if let Ok(content) = fs::read_to_string(&path) {
285 if let Ok(cache_entry) = serde_json::from_str::<CacheEntry>(&content) {
286 if cache_entry.is_expired() {
287 if let Err(e) = fs::remove_file(&path) {
288 warn!("Failed to remove expired cache entry: {}", e);
289 } else {
290 cleaned += 1;
291 debug!("Cleaned up expired cache entry: {}", path.display());
292 }
293 }
294 }
295 }
296 }
297 }
298
299 debug!("Cleaned up {} expired cache entries", cleaned);
300 Ok(cleaned)
301 }
302
303 fn key_to_path(&self, key: &str) -> PathBuf {
305 let sanitized = key
307 .chars()
308 .map(|c| {
309 if c.is_alphanumeric() || c == '_' || c == '-' {
310 c
311 } else {
312 '_'
313 }
314 })
315 .collect::<String>();
316
317 self.cache_dir.join(format!("{}.json", sanitized))
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use std::time::Duration;
325 use tempfile::TempDir;
326
327 #[test]
328 fn test_cache_set_and_get() -> StorageResult<()> {
329 let temp_dir = TempDir::new().unwrap();
330 let cache = CacheManager::new(temp_dir.path())?;
331
332 cache.set(
333 "test_key",
334 "test_data".to_string(),
335 CacheInvalidationStrategy::Manual,
336 )?;
337
338 let result = cache.get("test_key")?;
339 assert_eq!(result, Some("test_data".to_string()));
340
341 Ok(())
342 }
343
344 #[test]
345 fn test_cache_not_found() -> StorageResult<()> {
346 let temp_dir = TempDir::new().unwrap();
347 let cache = CacheManager::new(temp_dir.path())?;
348
349 let result = cache.get("nonexistent")?;
350 assert_eq!(result, None);
351
352 Ok(())
353 }
354
355 #[test]
356 fn test_cache_invalidate() -> StorageResult<()> {
357 let temp_dir = TempDir::new().unwrap();
358 let cache = CacheManager::new(temp_dir.path())?;
359
360 cache.set(
361 "test_key",
362 "test_data".to_string(),
363 CacheInvalidationStrategy::Manual,
364 )?;
365
366 let invalidated = cache.invalidate("test_key")?;
367 assert!(invalidated);
368
369 let result = cache.get("test_key")?;
370 assert_eq!(result, None);
371
372 Ok(())
373 }
374
375 #[test]
376 fn test_cache_exists() -> StorageResult<()> {
377 let temp_dir = TempDir::new().unwrap();
378 let cache = CacheManager::new(temp_dir.path())?;
379
380 cache.set(
381 "test_key",
382 "test_data".to_string(),
383 CacheInvalidationStrategy::Manual,
384 )?;
385
386 assert!(cache.exists("test_key")?);
387 assert!(!cache.exists("nonexistent")?);
388
389 Ok(())
390 }
391
392 #[test]
393 fn test_cache_clear() -> StorageResult<()> {
394 let temp_dir = TempDir::new().unwrap();
395 let cache = CacheManager::new(temp_dir.path())?;
396
397 cache.set(
398 "key1",
399 "data1".to_string(),
400 CacheInvalidationStrategy::Manual,
401 )?;
402 cache.set(
403 "key2",
404 "data2".to_string(),
405 CacheInvalidationStrategy::Manual,
406 )?;
407
408 cache.clear()?;
409
410 assert!(!cache.exists("key1")?);
411 assert!(!cache.exists("key2")?);
412
413 Ok(())
414 }
415
416 #[test]
417 fn test_cache_ttl_expiration() -> StorageResult<()> {
418 let temp_dir = TempDir::new().unwrap();
419 let cache = CacheManager::new(temp_dir.path())?;
420
421 cache.set(
423 "test_key",
424 "test_data".to_string(),
425 CacheInvalidationStrategy::Ttl(1),
426 )?;
427
428 assert!(cache.exists("test_key")?);
430
431 std::thread::sleep(Duration::from_secs(2));
433
434 let result = cache.get("test_key")?;
436 assert_eq!(result, None);
437
438 Ok(())
439 }
440
441 #[test]
442 fn test_cache_cleanup_expired() -> StorageResult<()> {
443 let temp_dir = TempDir::new().unwrap();
444 let cache = CacheManager::new(temp_dir.path())?;
445
446 cache.set(
448 "expired_key",
449 "data".to_string(),
450 CacheInvalidationStrategy::Ttl(1),
451 )?;
452
453 cache.set(
455 "manual_key",
456 "data".to_string(),
457 CacheInvalidationStrategy::Manual,
458 )?;
459
460 std::thread::sleep(Duration::from_secs(2));
462
463 let cleaned = cache.cleanup_expired()?;
465 assert_eq!(cleaned, 1);
466
467 assert!(cache.exists("manual_key")?);
469
470 Ok(())
471 }
472}