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}