1use anyhow::Result;
2use sha2::{Digest, Sha256};
3use std::fs::{self, File};
4use std::io::{BufReader, Read};
5use std::path::Path;
6use std::time::SystemTime;
7
8use super::types::{CacheEntry, CacheKey, CacheMetadata};
9
10#[derive(Debug)]
12pub struct FileCache {
13 cache_dir: std::path::PathBuf,
14}
15
16impl FileCache {
17 pub fn new(cache_dir: std::path::PathBuf) -> Result<Self> {
19 fs::create_dir_all(&cache_dir)?;
21 Ok(Self { cache_dir })
22 }
23
24 pub fn hash_file(path: &Path) -> Result<String> {
27 let file = File::open(path)?;
28 let mut reader = BufReader::with_capacity(65536, file); let mut hasher = Sha256::new();
30 let mut buffer = [0u8; 65536];
31
32 loop {
33 let bytes_read = reader.read(&mut buffer)?;
34 if bytes_read == 0 {
35 break;
36 }
37 hasher.update(&buffer[..bytes_read]);
38 }
39
40 let result = hasher.finalize();
41 Ok(format!("{:x}", result))
42 }
43
44 pub fn generate_key(path: &Path) -> Result<CacheKey> {
46 let file_hash = Self::hash_file(path)?;
47 Ok(CacheKey {
48 file_path: path.to_path_buf(),
49 file_hash,
50 })
51 }
52
53 pub fn save<T>(&self, key: &CacheKey, data: &T) -> Result<()>
55 where
56 T: serde::Serialize,
57 {
58 let serialized = bincode::serialize(data)?;
60 let original_size = serialized.len();
61
62 let compressed = lz4::block::compress(&serialized, None, true)?;
64 let compressed_size = compressed.len();
65
66 let metadata = CacheMetadata {
68 created_at: SystemTime::now(),
69 last_accessed: SystemTime::now(),
70 file_size: original_size as u64,
71 compressed_size,
72 compression_ratio: original_size as f32 / compressed_size as f32,
73 };
74
75 let entry = CacheEntry {
77 key: key.clone(),
78 data: compressed,
79 metadata,
80 };
81
82 let cache_path = self.cache_path(key);
84
85 if let Some(parent) = cache_path.parent() {
87 fs::create_dir_all(parent)?;
88 }
89
90 let entry_data = bincode::serialize(&entry)?;
92 fs::write(cache_path, entry_data)?;
93
94 Ok(())
95 }
96
97 pub fn load<T>(&self, key: &CacheKey) -> Result<Option<T>>
99 where
100 T: serde::de::DeserializeOwned,
101 {
102 let cache_path = self.cache_path(key);
103
104 if !cache_path.exists() {
106 return Ok(None);
107 }
108
109 let entry_data = fs::read(&cache_path)?;
111 let mut entry: CacheEntry<Vec<u8>> = bincode::deserialize(&entry_data)?;
112
113 entry.metadata.last_accessed = SystemTime::now();
115
116 let decompressed =
118 lz4::block::decompress(&entry.data, Some(entry.metadata.file_size as i32))?;
119
120 let data: T = bincode::deserialize(&decompressed)?;
122
123 Ok(Some(data))
124 }
125
126 pub fn is_valid(&self, key: &CacheKey) -> Result<bool> {
128 if !key.file_path.exists() {
130 return Ok(false);
131 }
132
133 let current_hash = Self::hash_file(&key.file_path)?;
135 Ok(current_hash == key.file_hash)
136 }
137
138 pub fn remove(&self, key: &CacheKey) -> Result<()> {
140 let cache_path = self.cache_path(key);
141 if cache_path.exists() {
142 fs::remove_file(cache_path)?;
143 }
144 Ok(())
145 }
146
147 fn cache_path(&self, key: &CacheKey) -> std::path::PathBuf {
149 let hash_prefix = &key.file_hash[..2];
151 let cache_name = format!(
152 "{}_{}.cache",
153 key.file_path
154 .file_name()
155 .and_then(|n| n.to_str())
156 .unwrap_or("unknown"),
157 &key.file_hash[..8]
158 );
159
160 self.cache_dir.join(hash_prefix).join(cache_name)
161 }
162
163 pub fn get_stats(&self) -> Result<CacheStats> {
165 let mut total_entries = 0;
166 let mut total_size = 0;
167 let mut total_compressed_size = 0;
168
169 for entry in fs::read_dir(&self.cache_dir)? {
171 let entry = entry?;
172 if entry.path().is_dir() {
173 for cache_file in fs::read_dir(entry.path())? {
174 let cache_file = cache_file?;
175 let metadata = cache_file.metadata()?;
176 total_entries += 1;
177 total_compressed_size += metadata.len() as usize;
178 total_size += (metadata.len() as f32 * 3.0) as usize;
180 }
181 }
182 }
183
184 Ok(CacheStats {
185 total_entries,
186 total_size,
187 total_compressed_size,
188 compression_ratio: if total_compressed_size > 0 {
189 total_size as f32 / total_compressed_size as f32
190 } else {
191 1.0
192 },
193 cache_dir: self.cache_dir.clone(),
194 })
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct CacheStats {
201 pub total_entries: usize,
202 pub total_size: usize,
203 pub total_compressed_size: usize,
204 pub compression_ratio: f32,
205 pub cache_dir: std::path::PathBuf,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
215 fn test_cache_key_structure() {
216 let path = Path::new("src/main.rs");
218 let file_hash = "abc123def456".to_string();
219
220 let key = CacheKey {
221 file_path: path.to_path_buf(),
222 file_hash: file_hash.clone(),
223 };
224
225 assert_eq!(key.file_hash, "abc123def456");
226 assert_eq!(key.file_path.file_name().unwrap(), "main.rs");
227 }
228
229 #[test]
230 fn test_cache_metadata_structure() {
231 let now = SystemTime::now();
233
234 let metadata = CacheMetadata {
235 created_at: now,
236 last_accessed: now,
237 file_size: 1024,
238 compressed_size: 512,
239 compression_ratio: 2.0,
240 };
241
242 assert_eq!(metadata.file_size, 1024);
243 assert_eq!(metadata.compressed_size, 512);
244 assert_eq!(metadata.compression_ratio, 2.0);
245 }
246
247 #[test]
248 fn test_cache_entry_structure() {
249 let key = CacheKey {
251 file_path: Path::new("test.rs").to_path_buf(),
252 file_hash: "test_hash".to_string(),
253 };
254
255 let metadata = CacheMetadata {
256 created_at: SystemTime::now(),
257 last_accessed: SystemTime::now(),
258 file_size: 100,
259 compressed_size: 50,
260 compression_ratio: 2.0,
261 };
262
263 let entry = CacheEntry {
264 key: key.clone(),
265 data: vec![1, 2, 3, 4, 5],
266 metadata,
267 };
268
269 assert_eq!(entry.data.len(), 5);
270 assert_eq!(entry.key.file_hash, "test_hash");
271 }
272
273 #[test]
274 fn test_cache_stats_structure() {
275 let stats = CacheStats {
277 total_entries: 100,
278 total_size: 1_000_000,
279 total_compressed_size: 500_000,
280 compression_ratio: 2.0,
281 cache_dir: Path::new("/cache").to_path_buf(),
282 };
283
284 assert_eq!(stats.total_entries, 100);
285 assert_eq!(stats.total_size, 1_000_000);
286 assert_eq!(stats.compression_ratio, 2.0);
287 }
288
289 #[test]
290 fn test_compression_ratio_calculation() {
291 let test_cases = vec![
293 (1000, 500, 2.0),
294 (2000, 1000, 2.0),
295 (3000, 1000, 3.0),
296 (1000, 250, 4.0),
297 ];
298
299 for (original, compressed, expected) in test_cases {
300 let ratio = original as f32 / compressed as f32;
301 assert!((ratio - expected).abs() < 0.01);
302 }
303 }
304
305 #[test]
306 fn test_cache_path_construction() {
307 let hash = "abc123def456";
309 let prefix = &hash[..2]; assert_eq!(prefix, "ab", "Prefix should be first 2 chars of hash");
312 }
313
314 #[test]
315 fn test_cache_file_naming() {
316 let file_name = "main.rs";
318 let hash_short = "abc12345";
319
320 let cache_name = format!("{}_{}.cache", file_name, hash_short);
321
322 assert!(
323 cache_name.contains("main.rs"),
324 "Should include original filename"
325 );
326 assert!(cache_name.contains("abc12345"), "Should include hash short");
327 assert!(cache_name.ends_with(".cache"), "Should end with .cache");
328 }
329
330 #[test]
331 fn test_cache_stats_compression_ratio_zero_handling() {
332 let total_size = 1000;
334 let compressed_size = 0;
335
336 let ratio = if compressed_size > 0 {
337 total_size as f32 / compressed_size as f32
338 } else {
339 1.0 };
341
342 assert_eq!(
343 ratio, 1.0,
344 "Should default to 1.0 when compressed size is 0"
345 );
346 }
347}