1use anyhow::{Context, Result};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::entry::CacheEntry;
8
9pub struct CacheStorage {
13 cache_dir: PathBuf,
15}
16
17impl CacheStorage {
18 pub fn new<P: AsRef<Path>>(cache_dir: P) -> Result<Self> {
20 let cache_dir = cache_dir.as_ref().to_path_buf();
21
22 if cache_dir.symlink_metadata().is_ok() && !cache_dir.is_dir() {
24 fs::remove_file(&cache_dir).with_context(|| {
25 format!(
26 "Failed to remove invalid cache path: {}",
27 cache_dir.display()
28 )
29 })?;
30 }
31 if !cache_dir.exists() {
32 fs::create_dir_all(&cache_dir).with_context(|| {
33 format!("Failed to create cache directory: {}", cache_dir.display())
34 })?;
35 }
36
37 Ok(Self { cache_dir })
38 }
39
40 fn get_cache_path(&self, namespace: &str, key: &str) -> PathBuf {
45 let safe_namespace = namespace.replace(['/', '\\', '\0'], "_");
47 let safe_namespace = if safe_namespace.contains("..") {
48 safe_namespace.replace("..", "__")
49 } else {
50 safe_namespace
51 };
52 let prefix = if key.len() >= 2 { &key[..2] } else { key };
53
54 self.cache_dir
55 .join(&safe_namespace)
56 .join(prefix)
57 .join(format!("{}.json", key))
58 }
59
60 pub fn exists(&self, namespace: &str, key: &str) -> bool {
62 self.get_cache_path(namespace, key).exists()
63 }
64
65 pub fn get(&self, namespace: &str, key: &str) -> Result<Option<CacheEntry>> {
67 let path = self.get_cache_path(namespace, key);
68
69 if !path.exists() {
70 return Ok(None);
71 }
72
73 let content = fs::read_to_string(&path)
74 .with_context(|| format!("Failed to read cache file: {}", path.display()))?;
75
76 let mut entry: CacheEntry = serde_json::from_str(&content)
77 .with_context(|| format!("Failed to parse cache entry: {}", path.display()))?;
78
79 entry.record_access();
80
81 let updated_content = serde_json::to_string_pretty(&entry)?;
82 fs::write(&path, updated_content)
83 .with_context(|| format!("Failed to update cache metadata: {}", path.display()))?;
84
85 Ok(Some(entry))
86 }
87
88 pub fn set(&self, entry: &CacheEntry) -> Result<()> {
90 let path = self.get_cache_path(&entry.namespace, &entry.key);
91
92 if let Some(parent) = path.parent() {
93 fs::create_dir_all(parent).with_context(|| {
94 format!("Failed to create cache subdirectory: {}", parent.display())
95 })?;
96 }
97
98 let content =
99 serde_json::to_string_pretty(entry).context("Failed to serialize cache entry")?;
100
101 fs::write(&path, content)
102 .with_context(|| format!("Failed to write cache file: {}", path.display()))?;
103
104 log::debug!("Cache entry saved: {}", path.display());
105
106 Ok(())
107 }
108
109 pub fn delete(&self, namespace: &str, key: &str) -> Result<()> {
111 let path = self.get_cache_path(namespace, key);
112
113 if path.exists() {
114 fs::remove_file(&path)
115 .with_context(|| format!("Failed to delete cache file: {}", path.display()))?;
116 log::debug!("Cache entry deleted: {}", path.display());
117 }
118
119 Ok(())
120 }
121
122 pub fn cache_dir(&self) -> &Path {
124 &self.cache_dir
125 }
126
127 pub fn total_size(&self) -> Result<u64> {
129 let mut total = 0u64;
130
131 for entry in walkdir::WalkDir::new(&self.cache_dir)
132 .into_iter()
133 .filter_map(|e| e.ok())
134 {
135 if entry.file_type().is_file() {
136 if let Ok(metadata) = entry.metadata() {
137 total += metadata.len();
138 }
139 }
140 }
141
142 Ok(total)
143 }
144
145 pub fn entry_count(&self) -> Result<usize> {
147 let mut count = 0;
148
149 for entry in walkdir::WalkDir::new(&self.cache_dir)
150 .into_iter()
151 .filter_map(|e| e.ok())
152 {
153 if entry.file_type().is_file()
154 && entry.path().extension().map_or(false, |ext| ext == "json")
155 {
156 count += 1;
157 }
158 }
159
160 Ok(count)
161 }
162
163 pub fn clear_all(&self) -> Result<usize> {
165 let mut removed = 0;
166
167 for entry in walkdir::WalkDir::new(&self.cache_dir)
168 .into_iter()
169 .filter_map(|e| e.ok())
170 {
171 if entry.file_type().is_file()
172 && entry.path().extension().map_or(false, |ext| ext == "json")
173 {
174 fs::remove_file(entry.path())?;
175 removed += 1;
176 }
177 }
178
179 Ok(removed)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::entry::CacheEntry;
187 use tempfile::TempDir;
188
189 #[test]
190 fn test_storage_creation() {
191 let temp_dir = TempDir::new().unwrap();
192 let storage = CacheStorage::new(temp_dir.path()).unwrap();
193
194 assert!(storage.cache_dir().exists());
195 }
196
197 #[test]
198 fn test_set_and_get() {
199 let temp_dir = TempDir::new().unwrap();
200 let storage = CacheStorage::new(temp_dir.path()).unwrap();
201
202 let entry = CacheEntry::new(
203 "1.0.0".to_string(),
204 "my-ns".to_string(),
205 "abc123".to_string(),
206 "test value".to_string(),
207 100,
208 );
209
210 storage.set(&entry).unwrap();
211
212 let retrieved = storage.get("my-ns", "abc123").unwrap();
213 assert!(retrieved.is_some());
214
215 let retrieved_entry = retrieved.unwrap();
216 assert_eq!(retrieved_entry.value, "test value");
217 assert_eq!(retrieved_entry.metadata.access_count, 1);
218 }
219
220 #[test]
221 fn test_exists() {
222 let temp_dir = TempDir::new().unwrap();
223 let storage = CacheStorage::new(temp_dir.path()).unwrap();
224
225 assert!(!storage.exists("ns", "nonexistent"));
226
227 let entry = CacheEntry::new(
228 "1.0.0".to_string(),
229 "ns".to_string(),
230 "abc123".to_string(),
231 "test".to_string(),
232 10,
233 );
234
235 storage.set(&entry).unwrap();
236
237 assert!(storage.exists("ns", "abc123"));
238 }
239
240 #[test]
241 fn test_delete() {
242 let temp_dir = TempDir::new().unwrap();
243 let storage = CacheStorage::new(temp_dir.path()).unwrap();
244
245 let entry = CacheEntry::new(
246 "1.0.0".to_string(),
247 "ns".to_string(),
248 "abc123".to_string(),
249 "test".to_string(),
250 10,
251 );
252
253 storage.set(&entry).unwrap();
254 assert!(storage.exists("ns", "abc123"));
255
256 storage.delete("ns", "abc123").unwrap();
257 assert!(!storage.exists("ns", "abc123"));
258 }
259
260 #[test]
261 fn test_total_size() {
262 let temp_dir = TempDir::new().unwrap();
263 let storage = CacheStorage::new(temp_dir.path()).unwrap();
264
265 let initial_size = storage.total_size().unwrap();
266
267 let entry = CacheEntry::new(
268 "1.0.0".to_string(),
269 "ns".to_string(),
270 "abc123".to_string(),
271 "test value".to_string(),
272 100,
273 );
274
275 storage.set(&entry).unwrap();
276
277 let new_size = storage.total_size().unwrap();
278 assert!(new_size > initial_size);
279 }
280
281 #[test]
282 fn test_entry_count() {
283 let temp_dir = TempDir::new().unwrap();
284 let storage = CacheStorage::new(temp_dir.path()).unwrap();
285
286 assert_eq!(storage.entry_count().unwrap(), 0);
287
288 let entry1 = CacheEntry::new(
289 "1.0.0".to_string(),
290 "ns".to_string(),
291 "abc123".to_string(),
292 "test1".to_string(),
293 10,
294 );
295
296 storage.set(&entry1).unwrap();
297 assert_eq!(storage.entry_count().unwrap(), 1);
298
299 let entry2 = CacheEntry::new(
300 "1.0.0".to_string(),
301 "ns".to_string(),
302 "def456".to_string(),
303 "test2".to_string(),
304 10,
305 );
306
307 storage.set(&entry2).unwrap();
308 assert_eq!(storage.entry_count().unwrap(), 2);
309 }
310
311 #[test]
312 fn test_clear_all() {
313 let temp_dir = TempDir::new().unwrap();
314 let storage = CacheStorage::new(temp_dir.path()).unwrap();
315
316 let entry1 = CacheEntry::new(
317 "1.0.0".to_string(),
318 "ns".to_string(),
319 "abc123".to_string(),
320 "test1".to_string(),
321 10,
322 );
323
324 let entry2 = CacheEntry::new(
325 "1.0.0".to_string(),
326 "ns".to_string(),
327 "def456".to_string(),
328 "test2".to_string(),
329 10,
330 );
331
332 storage.set(&entry1).unwrap();
333 storage.set(&entry2).unwrap();
334
335 assert_eq!(storage.entry_count().unwrap(), 2);
336
337 let removed = storage.clear_all().unwrap();
338 assert_eq!(removed, 2);
339 assert_eq!(storage.entry_count().unwrap(), 0);
340 }
341
342 #[test]
343 fn test_get_cache_path_short_key() {
344 let temp_dir = TempDir::new().unwrap();
345 let storage = CacheStorage::new(temp_dir.path()).unwrap();
346
347 let entry = CacheEntry::new(
348 "1.0.0".to_string(),
349 "ns".to_string(),
350 "a".to_string(),
351 "test".to_string(),
352 10,
353 );
354
355 storage.set(&entry).unwrap();
356 assert!(storage.exists("ns", "a"));
357 }
358
359 #[test]
360 fn test_get_cache_path_exact_2_char_key() {
361 let temp_dir = TempDir::new().unwrap();
362 let storage = CacheStorage::new(temp_dir.path()).unwrap();
363
364 let entry = CacheEntry::new(
365 "1.0.0".to_string(),
366 "ns".to_string(),
367 "ab".to_string(),
368 "test".to_string(),
369 10,
370 );
371
372 storage.set(&entry).unwrap();
373 assert!(storage.exists("ns", "ab"));
374 }
375
376 #[test]
377 fn test_entry_count_ignores_non_json_files() {
378 let temp_dir = TempDir::new().unwrap();
379 let storage = CacheStorage::new(temp_dir.path()).unwrap();
380
381 fs::write(temp_dir.path().join("not_a_cache.txt"), "hello").unwrap();
382 assert_eq!(storage.entry_count().unwrap(), 0);
383
384 let entry = CacheEntry::new(
385 "1.0.0".to_string(),
386 "ns".to_string(),
387 "abc123".to_string(),
388 "test".to_string(),
389 10,
390 );
391 storage.set(&entry).unwrap();
392 assert_eq!(storage.entry_count().unwrap(), 1);
393 }
394
395 #[test]
396 fn test_clear_all_ignores_non_json_files() {
397 let temp_dir = TempDir::new().unwrap();
398 let storage = CacheStorage::new(temp_dir.path()).unwrap();
399
400 let txt_path = temp_dir.path().join("not_a_cache.txt");
401 fs::write(&txt_path, "hello").unwrap();
402
403 let entry = CacheEntry::new(
404 "1.0.0".to_string(),
405 "ns".to_string(),
406 "abc123".to_string(),
407 "test".to_string(),
408 10,
409 );
410 storage.set(&entry).unwrap();
411
412 let removed = storage.clear_all().unwrap();
413 assert_eq!(removed, 1);
414 assert!(txt_path.exists());
415 }
416
417 #[test]
418 fn test_clear_all_returns_exact_count() {
419 let temp_dir = TempDir::new().unwrap();
420 let storage = CacheStorage::new(temp_dir.path()).unwrap();
421
422 assert_eq!(storage.clear_all().unwrap(), 0);
423
424 for i in 0..3 {
425 let entry = CacheEntry::new(
426 "1.0.0".to_string(),
427 "ns".to_string(),
428 format!("hash{:03}", i),
429 "test".to_string(),
430 10,
431 );
432 storage.set(&entry).unwrap();
433 }
434
435 assert_eq!(storage.clear_all().unwrap(), 3);
436 }
437}