1use serde::{Deserialize, Serialize};
4use std::{
5 collections::HashMap,
6 fs, io,
7 path::{Path, PathBuf},
8};
9
10use crate::error::DiskUseError;
11use crate::scanner::DirStat;
12
13#[derive(Serialize, Deserialize, Debug, Default)]
15pub(crate) struct Cache {
16 pub(crate) roots: HashMap<PathBuf, DirStat>,
17 pub(crate) version: u32,
18}
19
20pub struct CacheManager {
22 cache: Cache,
23 cache_path: PathBuf,
24 dirty: bool, }
26
27impl CacheManager {
28 pub fn new(cache_path: impl AsRef<Path>) -> Self {
30 let cache_path = cache_path.as_ref().to_path_buf();
31 let cache = Self::load_from_file(&cache_path);
32
33 Self {
34 cache,
35 cache_path,
36 dirty: false,
37 }
38 }
39
40 fn load_from_file(cache_path: &Path) -> Cache {
42 match fs::read(cache_path) {
43 Ok(bytes) => match bincode::deserialize::<Cache>(&bytes) {
44 Ok(cache) => cache,
45 Err(_) => {
46 eprintln!(
47 "Warning: Cache file '{}' is corrupted, starting with empty cache",
48 cache_path.display()
49 );
50 Cache::default()
51 }
52 },
53 Err(err) => {
54 if err.kind() != io::ErrorKind::NotFound {
56 let disk_err = DiskUseError::CacheReadError {
57 path: cache_path.to_path_buf(),
58 source: err,
59 };
60 eprintln!("Warning: {}", disk_err);
61 }
62 Cache::default()
63 }
64 }
65 }
66
67 pub fn save(&mut self) -> io::Result<()> {
69 if !self.dirty {
70 return Ok(()); }
72
73 if let Some(parent) = self.cache_path.parent() {
75 fs::create_dir_all(parent).map_err(|err| {
76 io::Error::from(DiskUseError::CacheWriteError {
77 path: parent.to_path_buf(),
78 source: err,
79 })
80 })?;
81 }
82
83 let bytes = bincode::serialize(&self.cache).map_err(|e| {
85 io::Error::from(DiskUseError::CacheSerializationError {
86 path: self.cache_path.clone(),
87 message: e.to_string(),
88 })
89 })?;
90
91 fs::write(&self.cache_path, bytes).map_err(|err| {
92 io::Error::from(DiskUseError::CacheWriteError {
93 path: self.cache_path.clone(),
94 source: err,
95 })
96 })?;
97
98 self.dirty = false;
99 Ok(())
100 }
101
102 pub fn get(&self, path: &Path) -> Option<&DirStat> {
104 let lookup_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
106
107 if let Some(stat) = self.cache.roots.get(&lookup_path) {
109 return Some(stat);
110 }
111
112 let mut best_root: Option<&DirStat> = None;
115
116 for root_stat in self.cache.roots.values() {
117 if lookup_path.starts_with(&root_stat.path) {
118 match best_root {
119 None => best_root = Some(root_stat),
120 Some(current_best) => {
121 if root_stat.path.components().count()
123 > current_best.path.components().count()
124 {
125 best_root = Some(root_stat);
126 }
127 }
128 }
129 }
130 }
131
132 if let Some(mut current) = best_root {
134 if let Ok(relative) = lookup_path.strip_prefix(¤t.path) {
136 let mut path_so_far = current.path.clone();
137
138 for component in relative.components() {
139 path_so_far.push(component);
140
141 if let Some(child) = current.children.get(&path_so_far) {
143 current = child;
144 } else {
145 return None;
147 }
148 }
149
150 return Some(current);
152 }
153 }
154
155 None
156 }
157
158 #[allow(dead_code)]
161 pub fn insert(&mut self, path: PathBuf, stats: DirStat) {
162 let canonical_path = path.canonicalize().unwrap_or(path);
164 self.cache.roots.insert(canonical_path, stats);
165 self.dirty = true;
166 }
167
168 pub fn update(&mut self, path: &Path, new_stats: DirStat) {
171 self.insert(path.to_path_buf(), new_stats);
172 }
173
174 pub fn clear(&mut self) -> io::Result<()> {
176 self.cache = Cache::default();
177 self.dirty = true;
178 self.save()
179 }
180
181 pub fn delete(&self) -> io::Result<()> {
183 if self.cache_path.exists() {
184 fs::remove_file(&self.cache_path)
185 } else {
186 Ok(())
187 }
188 }
189
190 pub fn path(&self) -> &Path {
192 &self.cache_path
193 }
194
195 pub fn get_roots(&self) -> Vec<&DirStat> {
197 self.cache.roots.values().collect()
198 }
199
200 pub fn is_empty(&self) -> bool {
202 self.cache.roots.is_empty()
203 }
204}
205
206impl Drop for CacheManager {
208 fn drop(&mut self) {
209 if self.dirty {
210 let _ = self.save();
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use std::time::SystemTime;
220 use tempfile::TempDir;
221
222 #[test]
223 fn test_cache_manager_basic_operations() -> io::Result<()> {
224 let temp_dir = TempDir::new()?;
230 let cache_file = temp_dir.path().join("test_cache.json");
231
232 let mut cache_mgr = CacheManager::new(&cache_file);
233
234 let test_stat = DirStat {
236 path: PathBuf::from("/test/path"),
237 total_size: 1000,
238 file_count: 10,
239 last_scan: SystemTime::now(),
240 children: HashMap::new(),
241 };
242
243 cache_mgr.insert(PathBuf::from("/test/path"), test_stat.clone());
244
245 let retrieved = cache_mgr.get(Path::new("/test/path"));
247 assert!(retrieved.is_some());
248 assert_eq!(retrieved.unwrap().total_size, 1000);
249 assert_eq!(retrieved.unwrap().file_count, 10);
250
251 cache_mgr.save()?;
253 assert!(cache_file.exists());
254
255 let cache_mgr2 = CacheManager::new(&cache_file);
257 let retrieved2 = cache_mgr2.get(Path::new("/test/path"));
258 assert!(retrieved2.is_some());
259 assert_eq!(retrieved2.unwrap().total_size, 1000);
260
261 Ok(())
262 }
263
264 #[test]
265 fn test_cache_clear_and_delete() -> io::Result<()> {
266 let temp_dir = TempDir::new()?;
270 let cache_file = temp_dir.path().join("test_cache.json");
271
272 let mut cache_mgr = CacheManager::new(&cache_file);
273
274 let test_stat = DirStat {
275 path: PathBuf::from("/test"),
276 total_size: 500,
277 file_count: 5,
278 last_scan: SystemTime::now(),
279 children: HashMap::new(),
280 };
281
282 cache_mgr.insert(PathBuf::from("/test"), test_stat);
283 cache_mgr.save()?;
284
285 cache_mgr.clear()?;
287 assert!(cache_mgr.get(Path::new("/test")).is_none());
288
289 cache_mgr.delete()?;
291 assert!(!cache_file.exists());
292
293 Ok(())
294 }
295
296 #[test]
297 fn test_get_nested_path() -> io::Result<()> {
298 let temp_dir = TempDir::new()?;
304 let cache_file = temp_dir.path().join("test_cache.json");
305 let mut cache_mgr = CacheManager::new(&cache_file);
306
307 let grandchild_path = PathBuf::from("/root/child/grandchild");
313 let grandchild_stat = DirStat {
314 path: grandchild_path.clone(),
315 total_size: 10,
316 file_count: 1,
317 last_scan: SystemTime::now(),
318 children: HashMap::new(),
319 };
320
321 let child_path = PathBuf::from("/root/child");
322 let mut child_stat = DirStat {
323 path: child_path.clone(),
324 total_size: 20,
325 file_count: 2,
326 last_scan: SystemTime::now(),
327 children: HashMap::new(),
328 };
329 child_stat
330 .children
331 .insert(grandchild_path.clone(), grandchild_stat);
332
333 let root_path = PathBuf::from("/root");
334 let mut root_stat = DirStat {
335 path: root_path.clone(),
336 total_size: 30,
337 file_count: 3,
338 last_scan: SystemTime::now(),
339 children: HashMap::new(),
340 };
341 root_stat.children.insert(child_path.clone(), child_stat);
342
343 cache_mgr.insert(root_path, root_stat);
344
345 let retrieved_child = cache_mgr.get(Path::new("/root/child"));
347 assert!(retrieved_child.is_some());
348 assert_eq!(retrieved_child.unwrap().total_size, 20);
349
350 let retrieved_grandchild = cache_mgr.get(Path::new("/root/child/grandchild"));
351 assert!(retrieved_grandchild.is_some());
352 assert_eq!(retrieved_grandchild.unwrap().total_size, 10);
353
354 assert!(cache_mgr.get(Path::new("/root/nonexistent")).is_none());
356 assert!(cache_mgr
357 .get(Path::new("/root/child/nonexistent"))
358 .is_none());
359
360 Ok(())
361 }
362
363 #[test]
364 fn test_cache_write_to_readonly_location() {
365 let cache_file = PathBuf::from("/dev/null/cannot_write_here");
368 let mut cache_mgr = CacheManager::new(&cache_file);
369
370 let test_stat = DirStat {
371 path: PathBuf::from("/test"),
372 total_size: 100,
373 file_count: 1,
374 last_scan: SystemTime::now(),
375 children: HashMap::new(),
376 };
377
378 cache_mgr.insert(PathBuf::from("/test"), test_stat);
379
380 let result = cache_mgr.save();
382 assert!(result.is_err());
383 let err = result.unwrap_err();
384 assert!(
385 err.to_string().contains("Failed to write cache file")
386 || err.to_string().contains("Failed to serialize"),
387 "Error message should be descriptive: {}",
388 err
389 );
390 }
391}