patternhunt/batch_io.rs
1// batch_io.rs
2use crate::error::GlobError;
3use lru::LruCache;
4use std::{
5 fs,
6 num::NonZeroUsize,
7 path::{Path, PathBuf},
8 sync::Mutex,
9 time::{Duration, Instant},
10};
11
12/// Configuration for metadata caching
13const METADATA_CACHE_TTL: Duration = Duration::from_secs(30);
14
15/// A cached metadata entry with expiration timestamp
16#[derive(Debug, Clone)]
17struct CachedMetadata {
18 metadata: fs::Metadata,
19 expires_at: Instant,
20}
21
22/// Batch I/O operations with metadata caching
23///
24/// This struct provides efficient access to filesystem metadata
25/// with LRU caching and configurable symlink following behavior.
26#[derive(Debug)]
27pub struct BatchIO {
28 metadata_cache: Mutex<LruCache<PathBuf, CachedMetadata>>,
29 follow_symlinks: bool,
30}
31
32impl BatchIO {
33 /// Creates a new BatchIO instance with specified cache size and symlink behavior
34 ///
35 /// # Arguments
36 ///
37 /// * `cache_size` - Maximum number of metadata entries to cache
38 /// * `follow_symlinks` - Whether to follow symlinks when retrieving metadata
39 ///
40 /// # Returns
41 ///
42 /// A new BatchIO instance
43 pub fn new(cache_size: usize, follow_symlinks: bool) -> Self {
44 Self {
45 metadata_cache: Mutex::new(LruCache::new(NonZeroUsize::new(cache_size).unwrap())),
46 follow_symlinks,
47 }
48 }
49
50 /// Retrieves metadata for a path with caching
51 ///
52 /// This method checks the cache first, and if not found or expired,
53 /// queries the filesystem. Also performs permission checks.
54 ///
55 /// # Arguments
56 ///
57 /// * `path` - Path to retrieve metadata for
58 ///
59 /// # Returns
60 ///
61 /// `Ok(Metadata)` if successful, `Err(GlobError)` otherwise
62 ///
63 /// # Errors
64 ///
65 /// Returns `GlobError::PermissionDenied` if file is read-only
66 /// Returns `GlobError::Io` for I/O errors
67 /// Returns `GlobError::Other` for symlinks when not allowed
68 pub fn stat(&self, path: &Path) -> Result<fs::Metadata, GlobError> {
69 let mut cache = self.metadata_cache.lock().unwrap();
70
71 // Check cache first
72 if let Some(cached) = cache.get(path) {
73 if cached.expires_at > Instant::now() {
74 return Ok(cached.metadata.clone());
75 }
76 // Remove expired entry
77 cache.pop(path);
78 }
79
80 // Check symlink restrictions
81 if !self.follow_symlinks && path.is_symlink() {
82 return Err(GlobError::Other("Symlinks not allowed".into()));
83 }
84
85 // Query filesystem
86 let meta = fs::metadata(path).map_err(GlobError::Io)?;
87
88 // Permission check
89 if meta.permissions().readonly() {
90 return Err(GlobError::PermissionDenied);
91 }
92
93 // Cache the result
94 let cached_meta = CachedMetadata {
95 metadata: meta.clone(),
96 expires_at: Instant::now() + METADATA_CACHE_TTL,
97 };
98 cache.put(path.to_path_buf(), cached_meta);
99
100 Ok(meta)
101 }
102
103 /// Retrieves metadata for a symlink without following it
104 ///
105 /// This method always queries the filesystem directly without caching,
106 /// as symlink metadata is typically less frequently accessed.
107 ///
108 /// # Arguments
109 ///
110 /// * `path` - Path to the symlink
111 ///
112 /// # Returns
113 ///
114 /// `Ok(Metadata)` if successful, `Err(GlobError)` otherwise
115 pub fn stat_symlink(&self, path: &Path) -> Result<fs::Metadata, GlobError> {
116 fs::symlink_metadata(path).map_err(GlobError::Io)
117 }
118
119 /// Clears the metadata cache
120 ///
121 /// Useful when filesystem changes are expected and cached data
122 /// might become stale.
123 pub fn clear_cache(&self) {
124 let mut cache = self.metadata_cache.lock().unwrap();
125 cache.clear();
126 }
127}