lechange_core/
file_ops.rs1use crate::error::{Error, Result};
4use parking_lot::RwLock;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8struct BoundedCache<K, V> {
15 map: HashMap<K, V>,
16 max_size: usize,
17}
18
19impl<K: Eq + std::hash::Hash + Clone, V> BoundedCache<K, V> {
20 fn new(max_size: usize) -> Self {
21 Self {
22 map: HashMap::with_capacity(max_size),
23 max_size,
24 }
25 }
26
27 fn get(&self, key: &K) -> Option<&V> {
28 self.map.get(key)
29 }
30
31 fn put(&mut self, key: K, value: V) {
32 if self.map.len() >= self.max_size {
33 self.map.clear();
34 }
35 self.map.insert(key, value);
36 }
37}
38
39pub struct FileOps {
41 symlink_cache: RwLock<BoundedCache<PathBuf, bool>>,
42}
43
44impl FileOps {
45 pub fn new() -> Self {
47 Self::with_cache_size(1024)
48 }
49
50 pub fn with_cache_size(cache_size: usize) -> Self {
52 Self {
53 symlink_cache: RwLock::new(BoundedCache::new(cache_size)),
54 }
55 }
56
57 pub fn is_symlink_sync(&self, path: &Path) -> Result<bool> {
59 let path_buf = path.to_path_buf();
60
61 {
63 let cache = self.symlink_cache.read();
64 if let Some(&cached) = cache.get(&path_buf) {
65 return Ok(cached);
66 }
67 }
68
69 let metadata = std::fs::symlink_metadata(&path_buf).map_err(Error::Io)?;
71
72 let is_link = metadata.file_type().is_symlink();
73
74 {
76 let mut cache = self.symlink_cache.write();
77 cache.put(path_buf, is_link);
78 }
79
80 Ok(is_link)
81 }
82}
83
84impl Default for FileOps {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::fs;
94 use tempfile::TempDir;
95
96 #[test]
97 fn test_is_symlink_sync() {
98 let dir = TempDir::new().unwrap();
99 let file_path = dir.path().join("regular_file.txt");
100 fs::write(&file_path, "content").unwrap();
101
102 let ops = FileOps::new();
103 let is_link = ops.is_symlink_sync(&file_path).unwrap();
104 assert!(!is_link);
105 }
106
107 #[test]
108 fn test_symlink_cache() {
109 let dir = TempDir::new().unwrap();
110 let file_path = dir.path().join("test.txt");
111 fs::write(&file_path, "content").unwrap();
112
113 let ops = FileOps::new();
114
115 let result1 = ops.is_symlink_sync(&file_path).unwrap();
117
118 let result2 = ops.is_symlink_sync(&file_path).unwrap();
120
121 assert_eq!(result1, result2);
122 assert!(!result1);
123 }
124
125 #[test]
126 fn test_cache_eviction_clears_when_full() {
127 let dir = TempDir::new().unwrap();
128 let ops = FileOps::with_cache_size(2);
129
130 for i in 0..5 {
131 let p = dir.path().join(format!("f{i}.txt"));
132 fs::write(&p, "content").unwrap();
133 assert!(!ops.is_symlink_sync(&p).unwrap());
134 }
135
136 let p0 = dir.path().join("f0.txt");
138 assert!(!ops.is_symlink_sync(&p0).unwrap());
139 }
140}