1use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use crate::{Result, StorageError};
12
13#[derive(Debug, Clone)]
15struct CacheEntry {
16 size: u64,
17 last_access: u64,
18}
19
20pub struct FileCache {
22 dir: PathBuf,
23 max_size: u64,
24 entries: BTreeMap<String, CacheEntry>,
25 current_size: u64,
26}
27
28impl FileCache {
29 pub fn new(dir: &Path, max_size: u64) -> Result<Self> {
31 std::fs::create_dir_all(dir)
32 .map_err(|e| StorageError::Cache(format!("create cache dir failed: {e}")))?;
33
34 Ok(Self {
35 dir: dir.to_path_buf(),
36 max_size,
37 entries: BTreeMap::new(),
38 current_size: 0,
39 })
40 }
41
42 pub fn get(&mut self, cid: &str) -> Option<Vec<u8>> {
44 if !self.entries.contains_key(cid) {
45 return None;
46 }
47
48 let path = self.path_for(cid);
49 match std::fs::read(&path) {
50 Ok(data) => {
51 if let Some(entry) = self.entries.get_mut(cid) {
53 entry.last_access = now_secs();
54 }
55 Some(data)
56 }
57 Err(_) => {
58 self.entries.remove(cid);
60 None
61 }
62 }
63 }
64
65 pub fn put(&mut self, cid: &str, data: &[u8]) -> Result<()> {
67 let size = data.len() as u64;
68
69 if size > self.max_size {
71 return Ok(());
72 }
73
74 while self.current_size + size > self.max_size {
76 if !self.evict_oldest() {
77 break;
78 }
79 }
80
81 let path = self.path_for(cid);
82 std::fs::write(&path, data)
83 .map_err(|e| StorageError::Cache(format!("write cache file failed: {e}")))?;
84
85 self.current_size += size;
86 self.entries.insert(
87 cid.to_string(),
88 CacheEntry {
89 size,
90 last_access: now_secs(),
91 },
92 );
93
94 Ok(())
95 }
96
97 pub fn remove(&mut self, cid: &str) -> Result<()> {
99 if let Some(entry) = self.entries.remove(cid) {
100 self.current_size = self.current_size.saturating_sub(entry.size);
101 let path = self.path_for(cid);
102 let _ = std::fs::remove_file(path);
103 }
104 Ok(())
105 }
106
107 pub fn contains(&self, cid: &str) -> bool {
109 self.entries.contains_key(cid)
110 }
111
112 pub fn len(&self) -> usize {
114 self.entries.len()
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.entries.is_empty()
120 }
121
122 pub fn size(&self) -> u64 {
124 self.current_size
125 }
126
127 pub fn clear(&mut self) -> Result<()> {
129 for cid in self.entries.keys().cloned().collect::<Vec<_>>() {
130 let path = self.path_for(&cid);
131 let _ = std::fs::remove_file(path);
132 }
133 self.entries.clear();
134 self.current_size = 0;
135 Ok(())
136 }
137
138 fn path_for(&self, cid: &str) -> PathBuf {
140 let safe_name: String = cid.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
142 self.dir.join(safe_name)
143 }
144
145 fn evict_oldest(&mut self) -> bool {
147 let oldest_cid = self
148 .entries
149 .iter()
150 .min_by_key(|(_, e)| e.last_access)
151 .map(|(k, _)| k.clone());
152
153 if let Some(cid) = oldest_cid {
154 if let Some(entry) = self.entries.remove(&cid) {
155 self.current_size = self.current_size.saturating_sub(entry.size);
156 let path = self.path_for(&cid);
157 let _ = std::fs::remove_file(path);
158 return true;
159 }
160 }
161 false
162 }
163}
164
165fn now_secs() -> u64 {
166 SystemTime::now()
167 .duration_since(UNIX_EPOCH)
168 .unwrap_or_default()
169 .as_secs()
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use tempfile::TempDir;
176
177 fn test_cache(max_size: u64) -> (FileCache, TempDir) {
178 let tmp = TempDir::new().unwrap();
179 let cache = FileCache::new(tmp.path(), max_size).unwrap();
180 (cache, tmp)
181 }
182
183 #[test]
184 fn put_and_get() {
185 let (mut cache, _tmp) = test_cache(1_000_000);
186 cache.put("QmTest1", b"hello world").unwrap();
187 assert_eq!(cache.get("QmTest1").unwrap(), b"hello world");
188 }
189
190 #[test]
191 fn miss_returns_none() {
192 let (mut cache, _tmp) = test_cache(1_000_000);
193 assert!(cache.get("QmNope").is_none());
194 }
195
196 #[test]
197 fn contains_check() {
198 let (mut cache, _tmp) = test_cache(1_000_000);
199 assert!(!cache.contains("Qm1"));
200 cache.put("Qm1", b"data").unwrap();
201 assert!(cache.contains("Qm1"));
202 }
203
204 #[test]
205 fn remove_entry() {
206 let (mut cache, _tmp) = test_cache(1_000_000);
207 cache.put("Qm1", b"data").unwrap();
208 cache.remove("Qm1").unwrap();
209 assert!(!cache.contains("Qm1"));
210 assert!(cache.is_empty());
211 }
212
213 #[test]
214 fn eviction_when_full() {
215 let (mut cache, _tmp) = test_cache(50);
218 cache.put("Qm1", &[0u8; 30]).unwrap();
219 cache.put("Qm2", &[1u8; 30]).unwrap();
220 assert!(!cache.contains("Qm1")); assert!(cache.contains("Qm2"));
222 }
223
224 #[test]
225 fn skip_oversized() {
226 let (mut cache, _tmp) = test_cache(10);
227 cache.put("QmBig", &[0u8; 100]).unwrap(); assert!(!cache.contains("QmBig"));
229 }
230
231 #[test]
232 fn clear_all() {
233 let (mut cache, _tmp) = test_cache(1_000_000);
234 cache.put("Qm1", b"a").unwrap();
235 cache.put("Qm2", b"b").unwrap();
236 cache.clear().unwrap();
237 assert!(cache.is_empty());
238 assert_eq!(cache.size(), 0);
239 }
240
241 #[test]
242 fn size_tracking() {
243 let (mut cache, _tmp) = test_cache(1_000_000);
244 cache.put("Qm1", &[0u8; 100]).unwrap();
245 cache.put("Qm2", &[0u8; 200]).unwrap();
246 assert_eq!(cache.size(), 300);
247 cache.remove("Qm1").unwrap();
248 assert_eq!(cache.size(), 200);
249 }
250}