1use std::sync::Arc;
7use std::time::Duration;
8
9use moka::sync::Cache;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone)]
14pub struct CachedContent {
15 pub data: Arc<Vec<u8>>,
17 pub mime_type: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CachedAttr {
24 pub size: u64,
26 pub is_dir: bool,
28 pub mime_type: Option<String>,
30 pub mtime: i64,
32}
33
34#[derive(Debug, Clone)]
36pub struct CachedDirEntry {
37 pub name: String,
38 pub is_dir: bool,
39}
40
41#[derive(Debug, Clone)]
43pub struct FileCacheConfig {
44 pub max_size_mb: u32,
46 pub ttl_secs: u32,
48}
49
50impl Default for FileCacheConfig {
51 fn default() -> Self {
52 Self {
53 max_size_mb: 100,
54 ttl_secs: 60,
55 }
56 }
57}
58
59#[derive(Clone)]
61pub struct FileCache {
62 content: Cache<String, CachedContent>,
64 attrs: Cache<String, CachedAttr>,
66 dirs: Cache<String, Vec<CachedDirEntry>>,
68 config: FileCacheConfig,
70}
71
72impl FileCache {
73 pub fn new(config: FileCacheConfig) -> Self {
75 let ttl = Duration::from_secs(config.ttl_secs as u64);
76 let max_capacity = (config.max_size_mb as u64) * 1024;
78
79 Self {
80 content: Cache::builder()
81 .time_to_live(ttl)
82 .max_capacity(max_capacity)
83 .build(),
84 attrs: Cache::builder()
85 .time_to_live(ttl)
86 .max_capacity(max_capacity * 10) .build(),
88 dirs: Cache::builder()
89 .time_to_live(ttl)
90 .max_capacity(max_capacity)
91 .build(),
92 config,
93 }
94 }
95
96 pub fn get_content(&self, path: &str) -> Option<CachedContent> {
98 self.content.get(&Self::normalize_key(path))
99 }
100
101 pub fn put_content(&self, path: &str, content: CachedContent) {
103 self.content.insert(Self::normalize_key(path), content);
104 }
105
106 pub fn get_attr(&self, path: &str) -> Option<CachedAttr> {
108 self.attrs.get(&Self::normalize_key(path))
109 }
110
111 pub fn put_attr(&self, path: &str, attr: CachedAttr) {
113 self.attrs.insert(Self::normalize_key(path), attr);
114 }
115
116 pub fn get_dir(&self, path: &str) -> Option<Vec<CachedDirEntry>> {
118 self.dirs.get(&Self::normalize_key(path))
119 }
120
121 pub fn put_dir(&self, path: &str, entries: Vec<CachedDirEntry>) {
123 self.dirs.insert(Self::normalize_key(path), entries);
124 }
125
126 pub fn invalidate(&self, path: &str) {
128 let key = Self::normalize_key(path);
129 self.content.invalidate(&key);
130 self.attrs.invalidate(&key);
131 self.dirs.invalidate(&key);
132 }
133
134 pub fn invalidate_prefix(&self, prefix: &str) {
136 let prefix = Self::normalize_key(prefix);
137
138 self.content.run_pending_tasks();
141 if prefix == "/" {
144 self.invalidate_all();
145 }
146 }
147
148 pub fn invalidate_all(&self) {
150 self.content.invalidate_all();
151 self.attrs.invalidate_all();
152 self.dirs.invalidate_all();
153 }
154
155 pub fn stats(&self) -> CacheStats {
157 CacheStats {
158 content_count: self.content.entry_count(),
159 attr_count: self.attrs.entry_count(),
160 dir_count: self.dirs.entry_count(),
161 max_size_mb: self.config.max_size_mb,
162 ttl_secs: self.config.ttl_secs,
163 }
164 }
165
166 fn normalize_key(path: &str) -> String {
168 let path = path.trim();
169 if path.is_empty() || path == "/" {
170 return "/".to_string();
171 }
172
173 let mut key = if path.starts_with('/') {
174 path.to_string()
175 } else {
176 format!("/{}", path)
177 };
178
179 if key.len() > 1 && key.ends_with('/') {
180 key.pop();
181 }
182
183 key
184 }
185}
186
187impl std::fmt::Debug for FileCache {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct("FileCache")
190 .field("config", &self.config)
191 .field("content_count", &self.content.entry_count())
192 .field("attr_count", &self.attrs.entry_count())
193 .field("dir_count", &self.dirs.entry_count())
194 .finish()
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct CacheStats {
201 pub content_count: u64,
202 pub attr_count: u64,
203 pub dir_count: u64,
204 pub max_size_mb: u32,
205 pub ttl_secs: u32,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_content_cache() {
214 let cache = FileCache::new(FileCacheConfig::default());
215
216 let content = CachedContent {
217 data: Arc::new(vec![1, 2, 3]),
218 mime_type: "text/plain".to_string(),
219 };
220
221 cache.put_content("/foo.txt", content.clone());
222
223 let cached = cache.get_content("/foo.txt").unwrap();
224 assert_eq!(cached.data.as_ref(), &[1, 2, 3]);
225 assert_eq!(cached.mime_type, "text/plain");
226 }
227
228 #[test]
229 fn test_attr_cache() {
230 let cache = FileCache::new(FileCacheConfig::default());
231
232 let attr = CachedAttr {
233 size: 100,
234 is_dir: false,
235 mime_type: Some("text/plain".to_string()),
236 mtime: 1234567890,
237 };
238
239 cache.put_attr("/foo.txt", attr.clone());
240
241 let cached = cache.get_attr("/foo.txt").unwrap();
242 assert_eq!(cached.size, 100);
243 assert!(!cached.is_dir);
244 }
245
246 #[test]
247 fn test_dir_cache() {
248 let cache = FileCache::new(FileCacheConfig::default());
249
250 let entries = vec![
251 CachedDirEntry {
252 name: "file.txt".to_string(),
253 is_dir: false,
254 },
255 CachedDirEntry {
256 name: "subdir".to_string(),
257 is_dir: true,
258 },
259 ];
260
261 cache.put_dir("/", entries.clone());
262
263 let cached = cache.get_dir("/").unwrap();
264 assert_eq!(cached.len(), 2);
265 assert_eq!(cached[0].name, "file.txt");
266 }
267
268 #[test]
269 fn test_invalidate() {
270 let cache = FileCache::new(FileCacheConfig::default());
271
272 let content = CachedContent {
273 data: Arc::new(vec![1, 2, 3]),
274 mime_type: "text/plain".to_string(),
275 };
276
277 cache.put_content("/foo.txt", content);
278 assert!(cache.get_content("/foo.txt").is_some());
279
280 cache.invalidate("/foo.txt");
281 assert!(cache.get_content("/foo.txt").is_none());
282 }
283
284 #[test]
285 fn test_invalidate_all() {
286 let cache = FileCache::new(FileCacheConfig::default());
287
288 cache.put_content(
289 "/a.txt",
290 CachedContent {
291 data: Arc::new(vec![1]),
292 mime_type: "text/plain".to_string(),
293 },
294 );
295 cache.put_content(
296 "/b.txt",
297 CachedContent {
298 data: Arc::new(vec![2]),
299 mime_type: "text/plain".to_string(),
300 },
301 );
302
303 cache.invalidate_all();
304
305 assert!(cache.get_content("/a.txt").is_none());
306 assert!(cache.get_content("/b.txt").is_none());
307 }
308
309 #[test]
310 fn test_normalize_key() {
311 assert_eq!(FileCache::normalize_key(""), "/");
312 assert_eq!(FileCache::normalize_key("/"), "/");
313 assert_eq!(FileCache::normalize_key("foo"), "/foo");
314 assert_eq!(FileCache::normalize_key("/foo"), "/foo");
315 assert_eq!(FileCache::normalize_key("/foo/"), "/foo");
316 }
317}