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