ricecoder_images/
cache.rs1use crate::error::{ImageError, ImageResult};
4use crate::models::ImageAnalysisResult;
5use ricecoder_storage::cache::{CacheInvalidationStrategy, CacheManager};
6use sha2::{Digest, Sha256};
7use std::path::PathBuf;
8use tracing::{debug, warn};
9
10pub struct ImageCache {
15 cache_manager: CacheManager,
17 ttl_seconds: u64,
19 max_size_mb: u64,
21}
22
23impl ImageCache {
24 pub fn new() -> ImageResult<Self> {
34 Self::with_config(86400, 100) }
36
37 pub fn with_config(ttl_seconds: u64, max_size_mb: u64) -> ImageResult<Self> {
48 let cache_dir = if let Ok(project_cache) = std::env::current_dir() {
50 project_cache
51 .join(".agent")
52 .join("cache")
53 .join("images")
54 } else {
55 if let Ok(home) = std::env::var("HOME") {
57 PathBuf::from(home)
58 .join(".ricecoder")
59 .join("cache")
60 .join("images")
61 } else {
62 return Err(ImageError::CacheError(
63 "Cannot determine cache directory: HOME not set".to_string(),
64 ));
65 }
66 };
67
68 let cache_manager = CacheManager::new(&cache_dir)
69 .map_err(|e| ImageError::CacheError(format!("Failed to create cache manager: {}", e)))?;
70
71 debug!(
72 "Created image cache at: {} (TTL: {}s, Max: {}MB)",
73 cache_dir.display(),
74 ttl_seconds,
75 max_size_mb
76 );
77
78 Ok(Self {
79 cache_manager,
80 ttl_seconds,
81 max_size_mb,
82 })
83 }
84
85 pub fn get(&self, image_hash: &str) -> ImageResult<Option<ImageAnalysisResult>> {
95 let cache_key = self.hash_to_cache_key(image_hash);
96
97 match self.cache_manager.get(&cache_key) {
98 Ok(Some(json)) => {
99 match serde_json::from_str::<ImageAnalysisResult>(&json) {
100 Ok(result) => {
101 debug!("Cache hit for image: {}", image_hash);
102 Ok(Some(result))
103 }
104 Err(e) => {
105 warn!("Failed to deserialize cached analysis: {}", e);
106 let _ = self.cache_manager.invalidate(&cache_key);
108 Ok(None)
109 }
110 }
111 }
112 Ok(None) => {
113 debug!("Cache miss for image: {}", image_hash);
114 Ok(None)
115 }
116 Err(e) => {
117 warn!("Cache lookup failed: {}", e);
118 Ok(None) }
120 }
121 }
122
123 pub fn set(&self, image_hash: &str, result: &ImageAnalysisResult) -> ImageResult<()> {
134 let cache_key = self.hash_to_cache_key(image_hash);
135
136 let json = serde_json::to_string(result)
137 .map_err(|e| ImageError::CacheError(format!("Failed to serialize analysis: {}", e)))?;
138
139 self.cache_manager
140 .set(
141 &cache_key,
142 json,
143 CacheInvalidationStrategy::Ttl(self.ttl_seconds),
144 )
145 .map_err(|e| ImageError::CacheError(format!("Failed to cache analysis: {}", e)))?;
146
147 debug!("Cached analysis for image: {}", image_hash);
148 Ok(())
149 }
150
151 pub fn exists(&self, image_hash: &str) -> ImageResult<bool> {
157 let cache_key = self.hash_to_cache_key(image_hash);
158
159 self.cache_manager
160 .exists(&cache_key)
161 .map_err(|e| ImageError::CacheError(format!("Failed to check cache: {}", e)))
162 }
163
164 pub fn invalidate(&self, image_hash: &str) -> ImageResult<bool> {
174 let cache_key = self.hash_to_cache_key(image_hash);
175
176 self.cache_manager
177 .invalidate(&cache_key)
178 .map_err(|e| ImageError::CacheError(format!("Failed to invalidate cache: {}", e)))
179 }
180
181 pub fn clear(&self) -> ImageResult<()> {
187 self.cache_manager
188 .clear()
189 .map_err(|e| ImageError::CacheError(format!("Failed to clear cache: {}", e)))?;
190
191 debug!("Cleared all cached analyses");
192 Ok(())
193 }
194
195 pub fn cleanup_expired(&self) -> ImageResult<usize> {
201 let cleaned = self
202 .cache_manager
203 .cleanup_expired()
204 .map_err(|e| ImageError::CacheError(format!("Failed to cleanup cache: {}", e)))?;
205
206 debug!("Cleaned up {} expired cache entries", cleaned);
207 Ok(cleaned)
208 }
209
210 pub fn compute_hash(data: &[u8]) -> String {
220 let mut hasher = Sha256::new();
221 hasher.update(data);
222 format!("{:x}", hasher.finalize())
223 }
224
225 fn hash_to_cache_key(&self, image_hash: &str) -> String {
227 format!("image_{}", image_hash)
228 }
229
230 pub fn stats(&self) -> (u64, u64) {
236 (self.ttl_seconds, self.max_size_mb)
237 }
238}
239
240impl Default for ImageCache {
241 fn default() -> Self {
242 Self::new().expect("Failed to create default image cache")
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::models::ImageAnalysisResult;
250
251 #[test]
252 fn test_cache_creation() {
253 let cache = ImageCache::new();
254 assert!(cache.is_ok());
255 }
256
257 #[test]
258 fn test_cache_with_config() {
259 let cache = ImageCache::with_config(3600, 50);
260 assert!(cache.is_ok());
261
262 let cache = cache.unwrap();
263 let (ttl, max_size) = cache.stats();
264 assert_eq!(ttl, 3600);
265 assert_eq!(max_size, 50);
266 }
267
268 #[test]
269 fn test_compute_hash() {
270 let data = b"test image data";
271 let hash1 = ImageCache::compute_hash(data);
272 let hash2 = ImageCache::compute_hash(data);
273
274 assert_eq!(hash1, hash2);
276
277 let different_data = b"different data";
279 let hash3 = ImageCache::compute_hash(different_data);
280 assert_ne!(hash1, hash3);
281 }
282
283 #[test]
284 fn test_hash_to_cache_key() {
285 let cache = ImageCache::new().unwrap();
286 let key = cache.hash_to_cache_key("abc123");
287 assert_eq!(key, "image_abc123");
288 }
289
290 #[test]
291 fn test_cache_set_and_get() {
292 let cache = ImageCache::new().unwrap();
293 let unique_hash = format!("hash123_test_{}", std::time::SystemTime::now()
294 .duration_since(std::time::UNIX_EPOCH)
295 .unwrap()
296 .as_nanos());
297 let result = ImageAnalysisResult::new(
298 unique_hash.clone(),
299 "This is a test image".to_string(),
300 "openai".to_string(),
301 100,
302 );
303
304 let set_result = cache.set(&unique_hash, &result);
306 assert!(set_result.is_ok());
307
308 let get_result = cache.get(&unique_hash);
310 assert!(get_result.is_ok());
311
312 if let Ok(Some(cached)) = get_result {
313 assert_eq!(cached.image_hash, unique_hash);
314 assert_eq!(cached.analysis, "This is a test image");
315 assert_eq!(cached.provider, "openai");
316 assert_eq!(cached.tokens_used, 100);
317 } else {
318 panic!("Expected cached result");
319 }
320 }
321
322 #[test]
323 fn test_cache_miss() {
324 let cache = ImageCache::new().unwrap();
325 let unique_hash = format!("nonexistent_{}", std::time::SystemTime::now()
326 .duration_since(std::time::UNIX_EPOCH)
327 .unwrap()
328 .as_nanos());
329 let result = cache.get(&unique_hash);
330 assert!(result.is_ok());
331 assert!(result.unwrap().is_none());
332 }
333
334 #[test]
335 fn test_cache_exists() {
336 let cache = ImageCache::new().unwrap();
337 let unique_hash = format!("hash_exists_{}", std::time::SystemTime::now()
338 .duration_since(std::time::UNIX_EPOCH)
339 .unwrap()
340 .as_nanos());
341 let result = ImageAnalysisResult::new(
342 unique_hash.clone(),
343 "Analysis".to_string(),
344 "openai".to_string(),
345 100,
346 );
347
348 cache.set(&unique_hash, &result).unwrap();
349
350 let exists = cache.exists(&unique_hash);
351 assert!(exists.is_ok());
352 assert!(exists.unwrap());
353
354 let not_exists = cache.exists("nonexistent_hash_that_never_existed");
355 assert!(not_exists.is_ok());
356 assert!(!not_exists.unwrap());
357 }
358
359 #[test]
360 fn test_cache_invalidate() {
361 let cache = ImageCache::new().unwrap();
362 let unique_hash = format!("hash_invalidate_{}", std::time::SystemTime::now()
363 .duration_since(std::time::UNIX_EPOCH)
364 .unwrap()
365 .as_nanos());
366 let result = ImageAnalysisResult::new(
367 unique_hash.clone(),
368 "Analysis".to_string(),
369 "openai".to_string(),
370 100,
371 );
372
373 cache.set(&unique_hash, &result).unwrap();
374 assert!(cache.exists(&unique_hash).unwrap());
375
376 let invalidated = cache.invalidate(&unique_hash);
377 assert!(invalidated.is_ok());
378 assert!(invalidated.unwrap());
379
380 assert!(!cache.exists(&unique_hash).unwrap());
381 }
382
383 #[test]
384 fn test_cache_clear() {
385 let test_id = std::time::SystemTime::now()
387 .duration_since(std::time::UNIX_EPOCH)
388 .unwrap()
389 .as_nanos();
390
391 let temp_dir = std::env::temp_dir().join(format!("ricecoder_cache_test_{}", test_id));
393 let _ = std::fs::create_dir_all(&temp_dir);
394
395 let cache_manager = ricecoder_storage::cache::CacheManager::new(&temp_dir)
396 .expect("Failed to create test cache manager");
397
398 let cache = ImageCache {
399 cache_manager,
400 ttl_seconds: 86400,
401 max_size_mb: 100,
402 };
403
404 let unique_hash = format!("hash_clear_{}", test_id);
405 let result = ImageAnalysisResult::new(
406 unique_hash.clone(),
407 "Analysis".to_string(),
408 "openai".to_string(),
409 100,
410 );
411
412 cache.set(&unique_hash, &result).unwrap();
413 assert!(cache.exists(&unique_hash).unwrap());
414
415 let clear_result = cache.clear();
416 assert!(clear_result.is_ok());
417
418 assert!(!cache.exists(&unique_hash).unwrap());
419
420 let _ = std::fs::remove_dir_all(&temp_dir);
422 }
423}