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 pub content_ttl_secs: u32,
50 pub negative_ttl_secs: u32,
52}
53
54impl Default for FileCacheConfig {
55 fn default() -> Self {
56 Self {
57 max_size_mb: 100,
58 ttl_secs: 60,
59 content_ttl_secs: 300,
60 negative_ttl_secs: 10,
61 }
62 }
63}
64
65impl FileCacheConfig {
66 pub fn from_basic(max_size_mb: u32, ttl_secs: u32) -> Self {
68 Self {
69 max_size_mb,
70 ttl_secs,
71 content_ttl_secs: ttl_secs.saturating_mul(5),
72 negative_ttl_secs: 10,
73 }
74 }
75}
76
77#[derive(Clone)]
79pub struct FileCache {
80 content: Cache<String, CachedContent>,
82 attrs: Cache<String, CachedAttr>,
84 dirs: Cache<String, Vec<CachedDirEntry>>,
86 negative: Cache<String, ()>,
88 config: FileCacheConfig,
90}
91
92impl FileCache {
93 pub fn new(config: FileCacheConfig) -> Self {
95 let metadata_ttl = Duration::from_secs(config.ttl_secs as u64);
96 let content_ttl = Duration::from_secs(config.content_ttl_secs as u64);
97 let negative_ttl = Duration::from_secs(config.negative_ttl_secs as u64);
98 let max_capacity = (config.max_size_mb as u64) * 1024;
100
101 Self {
102 content: Cache::builder()
103 .time_to_live(content_ttl)
104 .max_capacity(max_capacity)
105 .build(),
106 attrs: Cache::builder()
107 .time_to_live(metadata_ttl)
108 .max_capacity(max_capacity * 10) .build(),
110 dirs: Cache::builder()
111 .time_to_live(metadata_ttl)
112 .max_capacity(max_capacity)
113 .build(),
114 negative: Cache::builder()
115 .time_to_live(negative_ttl)
116 .max_capacity(10_000) .build(),
118 config,
119 }
120 }
121
122 pub fn get_content(&self, path: &str) -> Option<CachedContent> {
124 self.content.get(&Self::normalize_key(path))
125 }
126
127 pub fn put_content(&self, path: &str, content: CachedContent) {
129 self.content.insert(Self::normalize_key(path), content);
130 }
131
132 pub fn get_attr(&self, path: &str) -> Option<CachedAttr> {
134 self.attrs.get(&Self::normalize_key(path))
135 }
136
137 pub fn put_attr(&self, path: &str, attr: CachedAttr) {
139 self.attrs.insert(Self::normalize_key(path), attr);
140 }
141
142 pub fn get_dir(&self, path: &str) -> Option<Vec<CachedDirEntry>> {
144 self.dirs.get(&Self::normalize_key(path))
145 }
146
147 pub fn put_dir(&self, path: &str, entries: Vec<CachedDirEntry>) {
149 self.dirs.insert(Self::normalize_key(path), entries);
150 }
151
152 pub fn is_negative(&self, path: &str) -> bool {
154 self.negative.contains_key(&Self::normalize_key(path))
155 }
156
157 pub fn put_negative(&self, path: &str) {
159 self.negative.insert(Self::normalize_key(path), ());
160 }
161
162 pub fn invalidate(&self, path: &str) {
164 let key = Self::normalize_key(path);
165 self.content.invalidate(&key);
166 self.attrs.invalidate(&key);
167 self.dirs.invalidate(&key);
168 self.negative.invalidate(&key);
169 }
170
171 pub fn invalidate_prefix(&self, prefix: &str) {
173 let prefix = Self::normalize_key(prefix);
174
175 self.content.run_pending_tasks();
178 if prefix == "/" {
181 self.invalidate_all();
182 }
183 }
184
185 pub fn invalidate_all(&self) {
187 self.content.invalidate_all();
188 self.attrs.invalidate_all();
189 self.dirs.invalidate_all();
190 self.negative.invalidate_all();
191 }
192
193 pub fn stats(&self) -> CacheStats {
195 CacheStats {
196 content_count: self.content.entry_count(),
197 attr_count: self.attrs.entry_count(),
198 dir_count: self.dirs.entry_count(),
199 negative_count: self.negative.entry_count(),
200 max_size_mb: self.config.max_size_mb,
201 metadata_ttl_secs: self.config.ttl_secs,
202 content_ttl_secs: self.config.content_ttl_secs,
203 negative_ttl_secs: self.config.negative_ttl_secs,
204 }
205 }
206
207 fn normalize_key(path: &str) -> String {
209 let path = path.trim();
210 if path.is_empty() || path == "/" {
211 return "/".to_string();
212 }
213
214 let mut key = if path.starts_with('/') {
215 path.to_string()
216 } else {
217 format!("/{}", path)
218 };
219
220 if key.len() > 1 && key.ends_with('/') {
221 key.pop();
222 }
223
224 key
225 }
226}
227
228impl std::fmt::Debug for FileCache {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 f.debug_struct("FileCache")
231 .field("config", &self.config)
232 .field("content_count", &self.content.entry_count())
233 .field("attr_count", &self.attrs.entry_count())
234 .field("dir_count", &self.dirs.entry_count())
235 .field("negative_count", &self.negative.entry_count())
236 .finish()
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct CacheStats {
243 pub content_count: u64,
244 pub attr_count: u64,
245 pub dir_count: u64,
246 pub negative_count: u64,
247 pub max_size_mb: u32,
248 pub metadata_ttl_secs: u32,
249 pub content_ttl_secs: u32,
250 pub negative_ttl_secs: u32,
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_content_cache() {
259 let cache = FileCache::new(FileCacheConfig::default());
260
261 let content = CachedContent {
262 data: Arc::new(vec![1, 2, 3]),
263 mime_type: "text/plain".to_string(),
264 };
265
266 cache.put_content("/foo.txt", content.clone());
267
268 let cached = cache.get_content("/foo.txt").unwrap();
269 assert_eq!(cached.data.as_ref(), &[1, 2, 3]);
270 assert_eq!(cached.mime_type, "text/plain");
271 }
272
273 #[test]
274 fn test_attr_cache() {
275 let cache = FileCache::new(FileCacheConfig::default());
276
277 let attr = CachedAttr {
278 size: 100,
279 is_dir: false,
280 mime_type: Some("text/plain".to_string()),
281 mtime: 1234567890,
282 };
283
284 cache.put_attr("/foo.txt", attr.clone());
285
286 let cached = cache.get_attr("/foo.txt").unwrap();
287 assert_eq!(cached.size, 100);
288 assert!(!cached.is_dir);
289 }
290
291 #[test]
292 fn test_dir_cache() {
293 let cache = FileCache::new(FileCacheConfig::default());
294
295 let entries = vec![
296 CachedDirEntry {
297 name: "file.txt".to_string(),
298 is_dir: false,
299 },
300 CachedDirEntry {
301 name: "subdir".to_string(),
302 is_dir: true,
303 },
304 ];
305
306 cache.put_dir("/", entries.clone());
307
308 let cached = cache.get_dir("/").unwrap();
309 assert_eq!(cached.len(), 2);
310 assert_eq!(cached[0].name, "file.txt");
311 }
312
313 #[test]
314 fn test_invalidate() {
315 let cache = FileCache::new(FileCacheConfig::default());
316
317 let content = CachedContent {
318 data: Arc::new(vec![1, 2, 3]),
319 mime_type: "text/plain".to_string(),
320 };
321
322 cache.put_content("/foo.txt", content);
323 assert!(cache.get_content("/foo.txt").is_some());
324
325 cache.invalidate("/foo.txt");
326 assert!(cache.get_content("/foo.txt").is_none());
327 }
328
329 #[test]
330 fn test_invalidate_all() {
331 let cache = FileCache::new(FileCacheConfig::default());
332
333 cache.put_content(
334 "/a.txt",
335 CachedContent {
336 data: Arc::new(vec![1]),
337 mime_type: "text/plain".to_string(),
338 },
339 );
340 cache.put_content(
341 "/b.txt",
342 CachedContent {
343 data: Arc::new(vec![2]),
344 mime_type: "text/plain".to_string(),
345 },
346 );
347
348 cache.invalidate_all();
349
350 assert!(cache.get_content("/a.txt").is_none());
351 assert!(cache.get_content("/b.txt").is_none());
352 }
353
354 #[test]
355 fn test_negative_cache() {
356 let cache = FileCache::new(FileCacheConfig::default());
357
358 assert!(!cache.is_negative("/nonexistent"));
359
360 cache.put_negative("/nonexistent");
361 assert!(cache.is_negative("/nonexistent"));
362
363 cache.invalidate("/nonexistent");
365 assert!(!cache.is_negative("/nonexistent"));
366 }
367
368 #[test]
369 fn test_negative_cache_invalidate_all() {
370 let cache = FileCache::new(FileCacheConfig::default());
371
372 cache.put_negative("/a");
373 cache.put_negative("/b");
374 assert!(cache.is_negative("/a"));
375 assert!(cache.is_negative("/b"));
376
377 cache.invalidate_all();
378 assert!(!cache.is_negative("/a"));
379 assert!(!cache.is_negative("/b"));
380 }
381
382 #[test]
383 fn test_normalize_key() {
384 assert_eq!(FileCache::normalize_key(""), "/");
385 assert_eq!(FileCache::normalize_key("/"), "/");
386 assert_eq!(FileCache::normalize_key("foo"), "/foo");
387 assert_eq!(FileCache::normalize_key("/foo"), "/foo");
388 assert_eq!(FileCache::normalize_key("/foo/"), "/foo");
389 }
390}