1use crate::error::PathError;
7use crate::metadata::FileEntry;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::Mutex;
11use std::time::{Duration, SystemTime};
12
13#[cfg(feature = "search")]
14use crate::search::SearchRequest;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18#[non_exhaustive]
19pub enum CachePolicy {
20 #[default]
22 Bypass,
23 ReadThrough,
25 Refresh,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31#[non_exhaustive]
32pub enum CacheMode {
33 #[default]
35 Disabled,
36 Memory,
38 Persistent,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct CacheOptions {
45 pub mode: CacheMode,
47 pub ttl: Option<Duration>,
49 pub max_entries: usize,
51 pub validate_metadata: bool,
56}
57
58impl Default for CacheOptions {
59 fn default() -> Self {
60 Self {
61 mode: CacheMode::Disabled,
62 ttl: None,
63 max_entries: 128,
64 validate_metadata: false,
65 }
66 }
67}
68
69impl CacheOptions {
70 pub fn new() -> Self {
72 Self::default()
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash)]
78pub struct CacheKey {
79 pub root: PathBuf,
81 pub patterns: Vec<String>,
83 pub exclude_patterns: Vec<String>,
85 pub recursive: bool,
87 pub follow_symlinks: bool,
89 pub include_hidden: bool,
91 pub include_files: bool,
93 pub include_directories: bool,
95 pub include_symlinks: bool,
97 pub max_depth: Option<usize>,
99 pub sort: crate::metadata::SortMode,
101}
102
103impl CacheKey {
104 #[cfg(feature = "search")]
106 pub fn from_search(request: &SearchRequest) -> Self {
107 Self {
108 root: request.root.clone(),
109 patterns: request.patterns.clone(),
110 exclude_patterns: request.exclude_patterns.clone(),
111 recursive: request.options.recursive,
112 follow_symlinks: request.options.follow_symlinks,
113 include_hidden: request.options.include_hidden,
114 include_files: request.options.include_files,
115 include_directories: request.options.include_directories,
116 include_symlinks: request.options.include_symlinks,
117 max_depth: request.options.max_depth,
118 sort: request.options.sort,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct CacheValue {
126 pub entries: Vec<FileEntry>,
128 pub stored_at: SystemTime,
130 pub ttl: Option<Duration>,
132}
133
134impl CacheValue {
135 pub fn is_expired(&self) -> bool {
137 let Some(ttl) = self.ttl else {
138 return false;
139 };
140 self.stored_at.elapsed().map(|e| e > ttl).unwrap_or(true)
141 }
142
143 pub fn is_expired_with_default(&self, default_ttl: Option<Duration>) -> bool {
145 let ttl = self.ttl.or(default_ttl);
146 let Some(ttl) = ttl else {
147 return false;
148 };
149 self.stored_at.elapsed().map(|e| e > ttl).unwrap_or(true)
150 }
151}
152
153pub trait DiscoveryCache: Send + Sync {
155 fn get(&self, key: &CacheKey) -> Result<Option<CacheValue>, PathError>;
157 fn put(&self, key: CacheKey, value: CacheValue) -> Result<(), PathError>;
159 fn invalidate(&self, root: &Path) -> Result<(), PathError>;
161 fn clear(&self) -> Result<(), PathError>;
163}
164
165#[derive(Debug)]
167pub struct MemoryCache {
168 options: CacheOptions,
169 inner: Mutex<HashMap<CacheKey, CacheValue>>,
170}
171
172impl MemoryCache {
173 pub fn new(options: CacheOptions) -> Self {
177 Self {
178 options,
179 inner: Mutex::new(HashMap::new()),
180 }
181 }
182
183 pub fn len(&self) -> Result<usize, PathError> {
185 let guard = self
186 .inner
187 .lock()
188 .map_err(|_| PathError::cache("memory cache lock poisoned"))?;
189 Ok(guard.len())
190 }
191
192 pub fn is_empty(&self) -> Result<bool, PathError> {
194 Ok(self.len()? == 0)
195 }
196
197 #[cfg(feature = "persistent-cache")]
198 pub(crate) fn snapshot(&self) -> Result<Vec<(CacheKey, CacheValue)>, PathError> {
199 let guard = self
200 .inner
201 .lock()
202 .map_err(|_| PathError::cache("memory cache lock poisoned"))?;
203 Ok(guard.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
204 }
205}
206
207impl DiscoveryCache for MemoryCache {
208 fn get(&self, key: &CacheKey) -> Result<Option<CacheValue>, PathError> {
209 if self.options.mode == CacheMode::Disabled {
210 return Ok(None);
211 }
212 let mut guard = self
213 .inner
214 .lock()
215 .map_err(|_| PathError::cache("memory cache lock poisoned"))?;
216
217 if let Some(value) = guard.get(key) {
218 if value.is_expired_with_default(self.options.ttl) {
219 guard.remove(key);
220 return Ok(None);
221 }
222 return Ok(Some(value.clone()));
223 }
224 Ok(None)
225 }
226
227 fn put(&self, key: CacheKey, mut value: CacheValue) -> Result<(), PathError> {
228 if self.options.mode == CacheMode::Disabled {
229 return Ok(());
230 }
231 if value.ttl.is_none() {
232 value.ttl = self.options.ttl;
233 }
234 let mut guard = self
235 .inner
236 .lock()
237 .map_err(|_| PathError::cache("memory cache lock poisoned"))?;
238
239 if guard.len() >= self.options.max_entries && !guard.contains_key(&key) {
240 if let Some(evict) = guard.keys().next().cloned() {
242 guard.remove(&evict);
243 }
244 }
245 guard.insert(key, value);
246 Ok(())
247 }
248
249 fn invalidate(&self, root: &Path) -> Result<(), PathError> {
250 let mut guard = self
251 .inner
252 .lock()
253 .map_err(|_| PathError::cache("memory cache lock poisoned"))?;
254 guard.retain(|k, _| k.root != root && !k.root.starts_with(root));
255 Ok(())
256 }
257
258 fn clear(&self) -> Result<(), PathError> {
259 let mut guard = self
260 .inner
261 .lock()
262 .map_err(|_| PathError::cache("memory cache lock poisoned"))?;
263 guard.clear();
264 Ok(())
265 }
266}
267
268#[cfg(feature = "persistent-cache")]
275#[derive(Debug)]
276pub struct PersistentCache {
277 path: PathBuf,
278 options: CacheOptions,
279 memory: MemoryCache,
280}
281
282#[cfg(feature = "persistent-cache")]
283mod persistent_schema {
284 use serde::{Deserialize, Serialize};
285
286 pub const SCHEMA_VERSION: u32 = 1;
287
288 #[derive(Serialize, Deserialize)]
289 pub struct PersistentCacheFile {
290 pub schema_version: u32,
291 pub entries: Vec<PersistentCacheRecord>,
292 }
293
294 #[derive(Serialize, Deserialize)]
295 pub struct PersistentCacheRecord {
296 pub key: PersistentKey,
297 pub paths: Vec<String>,
298 pub stored_at_epoch_ms: u128,
299 pub ttl_ms: Option<u128>,
300 }
301
302 #[derive(Serialize, Deserialize, Clone)]
303 pub struct PersistentKey {
304 pub root: String,
305 pub patterns: Vec<String>,
306 pub exclude_patterns: Vec<String>,
307 pub recursive: bool,
308 pub follow_symlinks: bool,
309 pub include_hidden: bool,
310 pub include_files: bool,
311 pub include_directories: bool,
312 pub include_symlinks: bool,
313 pub max_depth: Option<usize>,
314 pub sort: u8,
315 }
316}
317
318#[cfg(feature = "persistent-cache")]
319fn sort_to_u8(sort: crate::metadata::SortMode) -> u8 {
320 use crate::metadata::SortMode;
321 match sort {
322 SortMode::None => 0,
323 SortMode::Path => 1,
324 SortMode::Name => 2,
325 SortMode::DirsFirst => 3,
326 }
327}
328
329#[cfg(feature = "persistent-cache")]
330fn sort_from_u8(v: u8) -> crate::metadata::SortMode {
331 use crate::metadata::SortMode;
332 match v {
333 0 => SortMode::None,
334 2 => SortMode::Name,
335 3 => SortMode::DirsFirst,
336 _ => SortMode::Path,
337 }
338}
339
340#[cfg(feature = "persistent-cache")]
341impl PersistentCache {
342 pub fn open(app_name: &str, file_name: &str, options: CacheOptions) -> Result<Self, PathError> {
344 use std::fs;
345 let dir = crate::dirs::cache_dir(app_name)?;
346 fs::create_dir_all(&dir).map_err(|e| PathError::filesystem(&dir, e))?;
347 let path = dir.join(file_name);
348 let memory = MemoryCache::new(CacheOptions {
349 mode: CacheMode::Memory,
350 ttl: options.ttl,
351 max_entries: options.max_entries,
352 validate_metadata: options.validate_metadata,
353 });
354 let cache = Self {
355 path,
356 options,
357 memory,
358 };
359 let _ = cache.load_into_memory();
360 Ok(cache)
361 }
362
363 pub fn path(&self) -> &Path {
365 &self.path
366 }
367
368 fn load_into_memory(&self) -> Result<(), PathError> {
369 use persistent_schema::{PersistentCacheFile, SCHEMA_VERSION};
370 use std::fs;
371
372 if !self.path.exists() {
373 return Ok(());
374 }
375 let data = fs::read(&self.path).map_err(|e| PathError::filesystem(&self.path, e))?;
376 let parsed: PersistentCacheFile = match serde_json::from_slice(&data) {
377 Ok(v) => v,
378 Err(_) => return Ok(()),
379 };
380 if parsed.schema_version != SCHEMA_VERSION {
381 return Ok(());
382 }
383 for rec in parsed.entries {
384 let key = CacheKey {
385 root: PathBuf::from(&rec.key.root),
386 patterns: rec.key.patterns,
387 exclude_patterns: rec.key.exclude_patterns,
388 recursive: rec.key.recursive,
389 follow_symlinks: rec.key.follow_symlinks,
390 include_hidden: rec.key.include_hidden,
391 include_files: rec.key.include_files,
392 include_directories: rec.key.include_directories,
393 include_symlinks: rec.key.include_symlinks,
394 max_depth: rec.key.max_depth,
395 sort: sort_from_u8(rec.key.sort),
396 };
397 let stored_at =
398 SystemTime::UNIX_EPOCH + Duration::from_millis(rec.stored_at_epoch_ms as u64);
399 let ttl = rec.ttl_ms.map(|ms| Duration::from_millis(ms as u64));
400 let entries = rec
401 .paths
402 .into_iter()
403 .map(|p| FileEntry::new(PathBuf::from(p), crate::metadata::EntryKind::Other))
404 .collect();
405 let value = CacheValue {
406 entries,
407 stored_at,
408 ttl,
409 };
410 if !value.is_expired_with_default(self.options.ttl) {
411 self.memory.put(key, value)?;
412 }
413 }
414 Ok(())
415 }
416
417 fn flush(&self) -> Result<(), PathError> {
418 use persistent_schema::{
419 PersistentCacheFile, PersistentCacheRecord, PersistentKey, SCHEMA_VERSION,
420 };
421 use std::fs;
422 use std::io::Write;
423
424 let snapshot = self.memory.snapshot()?;
425 let mut records = Vec::new();
426 for (key, value) in snapshot {
427 if value.is_expired_with_default(self.options.ttl) {
428 continue;
429 }
430 let stored_at_epoch_ms = value
431 .stored_at
432 .duration_since(SystemTime::UNIX_EPOCH)
433 .map(|d| d.as_millis())
434 .unwrap_or(0);
435 records.push(PersistentCacheRecord {
436 key: PersistentKey {
437 root: key.root.to_string_lossy().into_owned(),
438 patterns: key.patterns.clone(),
439 exclude_patterns: key.exclude_patterns.clone(),
440 recursive: key.recursive,
441 follow_symlinks: key.follow_symlinks,
442 include_hidden: key.include_hidden,
443 include_files: key.include_files,
444 include_directories: key.include_directories,
445 include_symlinks: key.include_symlinks,
446 max_depth: key.max_depth,
447 sort: sort_to_u8(key.sort),
448 },
449 paths: value
450 .entries
451 .iter()
452 .map(|e| e.path.to_string_lossy().into_owned())
453 .collect(),
454 stored_at_epoch_ms,
455 ttl_ms: value.ttl.map(|d| d.as_millis()),
456 });
457 }
458
459 let file = PersistentCacheFile {
460 schema_version: SCHEMA_VERSION,
461 entries: records,
462 };
463 let json = serde_json::to_vec_pretty(&file)
464 .map_err(|e| PathError::cache(format!("serialize failed: {e}")))?;
465
466 let tmp_path = self.path.with_extension("json.tmp");
467 {
468 let mut f =
469 fs::File::create(&tmp_path).map_err(|e| PathError::filesystem(&tmp_path, e))?;
470 f.write_all(&json)
471 .map_err(|e| PathError::filesystem(&tmp_path, e))?;
472 f.sync_all()
473 .map_err(|e| PathError::filesystem(&tmp_path, e))?;
474 }
475 fs::rename(&tmp_path, &self.path).map_err(|e| PathError::filesystem(&self.path, e))?;
476 Ok(())
477 }
478}
479
480#[cfg(feature = "persistent-cache")]
481impl DiscoveryCache for PersistentCache {
482 fn get(&self, key: &CacheKey) -> Result<Option<CacheValue>, PathError> {
483 if matches!(self.options.mode, CacheMode::Disabled) {
484 return Ok(None);
485 }
486 self.memory.get(key)
487 }
488
489 fn put(&self, key: CacheKey, value: CacheValue) -> Result<(), PathError> {
490 if matches!(self.options.mode, CacheMode::Disabled) {
491 return Ok(());
492 }
493 self.memory.put(key, value)?;
494 self.flush()
495 }
496
497 fn invalidate(&self, root: &Path) -> Result<(), PathError> {
498 self.memory.invalidate(root)?;
499 self.flush()
500 }
501
502 fn clear(&self) -> Result<(), PathError> {
503 use std::fs;
504 self.memory.clear()?;
505 if self.path.exists() {
506 fs::remove_file(&self.path).map_err(|e| PathError::filesystem(&self.path, e))?;
507 }
508 Ok(())
509 }
510}