1use std::collections::HashMap;
2use std::sync::Mutex;
3use std::time::SystemTime;
4
5pub struct FileCache<T> {
11 inner: Mutex<Inner<T>>,
12 capacity: usize,
13}
14
15struct Inner<T> {
16 entries: HashMap<String, Entry<T>>,
17 order: Vec<String>,
19}
20
21struct Entry<T> {
22 modified: Option<SystemTime>,
23 value: T,
24}
25
26impl<T: Clone> FileCache<T> {
27 pub fn new(capacity: usize) -> Self {
28 Self {
29 inner: Mutex::new(Inner {
30 entries: HashMap::new(),
31 order: Vec::new(),
32 }),
33 capacity: capacity.max(1),
34 }
35 }
36
37 pub fn get(&self, key: &str, modified: Option<SystemTime>) -> Option<T> {
38 let inner = self.inner.lock().ok()?;
39 let entry = inner.entries.get(key)?;
40
41 (entry.modified == modified).then(|| entry.value.clone())
42 }
43
44 pub fn insert(&self, key: String, modified: Option<SystemTime>, value: T) {
45 let Ok(mut inner) = self.inner.lock() else {
46 return;
47 };
48
49 if inner
50 .entries
51 .insert(key.clone(), Entry { modified, value })
52 .is_none()
53 {
54 inner.order.push(key);
55 }
56
57 while inner.order.len() > self.capacity {
58 let oldest = inner.order.remove(0);
59 inner.entries.remove(&oldest);
60 }
61 }
62
63 pub fn len(&self) -> usize {
64 self.inner.lock().map(|i| i.entries.len()).unwrap_or(0)
65 }
66
67 pub fn is_empty(&self) -> bool {
68 self.len() == 0
69 }
70
71 pub fn clear(&self) {
72 if let Ok(mut inner) = self.inner.lock() {
73 inner.entries.clear();
74 inner.order.clear();
75 }
76 }
77}
78
79pub fn modified_at(path: &std::path::Path) -> Option<SystemTime> {
80 std::fs::metadata(path).ok()?.modified().ok()
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use std::time::Duration;
87
88 fn at(seconds: u64) -> Option<SystemTime> {
89 Some(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds))
90 }
91
92 #[test]
93 fn returns_what_was_stored() {
94 let cache: FileCache<String> = FileCache::new(4);
95 cache.insert("a".into(), at(1), "value".into());
96
97 assert_eq!(cache.get("a", at(1)), Some("value".to_string()));
98 }
99
100 #[test]
101 fn misses_on_an_unknown_key() {
102 let cache: FileCache<String> = FileCache::new(4);
103 assert_eq!(cache.get("nope", at(1)), None);
104 }
105
106 #[test]
107 fn a_changed_mtime_invalidates_the_entry() {
108 let cache: FileCache<String> = FileCache::new(4);
109 cache.insert("a".into(), at(1), "old".into());
110
111 assert_eq!(cache.get("a", at(2)), None);
112 }
113
114 #[test]
115 fn a_file_that_lost_its_mtime_is_not_served_from_cache() {
116 let cache: FileCache<String> = FileCache::new(4);
117 cache.insert("a".into(), at(1), "value".into());
118
119 assert_eq!(cache.get("a", None), None);
120 }
121
122 #[test]
123 fn evicts_the_oldest_once_full() {
124 let cache: FileCache<String> = FileCache::new(2);
125 cache.insert("a".into(), at(1), "a".into());
126 cache.insert("b".into(), at(1), "b".into());
127 cache.insert("c".into(), at(1), "c".into());
128
129 assert_eq!(cache.len(), 2);
130 assert_eq!(cache.get("a", at(1)), None);
131 assert_eq!(cache.get("c", at(1)), Some("c".to_string()));
132 }
133
134 #[test]
135 fn re_inserting_a_key_does_not_grow_the_order_list() {
136 let cache: FileCache<String> = FileCache::new(2);
137 cache.insert("a".into(), at(1), "one".into());
138 cache.insert("a".into(), at(2), "two".into());
139 cache.insert("b".into(), at(1), "b".into());
140
141 assert_eq!(cache.len(), 2);
142 assert_eq!(cache.get("a", at(2)), Some("two".to_string()));
143 }
144
145 #[test]
146 fn a_zero_capacity_still_holds_one_entry() {
147 let cache: FileCache<String> = FileCache::new(0);
148 cache.insert("a".into(), at(1), "value".into());
149
150 assert_eq!(cache.get("a", at(1)), Some("value".to_string()));
151 }
152
153 #[test]
154 fn clearing_empties_it() {
155 let cache: FileCache<String> = FileCache::new(4);
156 cache.insert("a".into(), at(1), "value".into());
157 cache.clear();
158
159 assert!(cache.is_empty());
160 }
161}