random_image_server/
cache.rs

1use std::{collections::HashMap, fs, path::PathBuf};
2
3use rand::prelude::*;
4use tempfile::TempDir;
5use url::Url;
6
7pub trait CacheBackend: std::fmt::Debug + Send + Sync {
8    /// report the type of the cache backend
9    fn backend_type(&self) -> &'static str;
10
11    /// Create a new cache backend
12    fn new() -> Self
13    where
14        Self: Sized;
15
16    /// Get an image from the cache by its key
17    fn get(&self, key: CacheKey) -> Option<CacheValue>;
18
19    /// Get a random image from the cache
20    fn get_random(&self) -> Option<CacheValue>;
21
22    /// Store an image in the cache with its key
23    ///
24    /// # Errors
25    ///
26    /// Returns an error if the image cannot be stored (e.g. due to size limits), or if the image is invalid
27    fn set(&mut self, key: CacheKey, image: CacheValue) -> Result<(), String>;
28
29    /// Remove an image from the cache by its key
30    fn remove(&mut self, key: &CacheKey) -> Option<CacheValue>;
31
32    /// Get the size of the cache
33    fn size(&self) -> usize;
34
35    /// Check if the cache is empty
36    fn is_empty(&self) -> bool {
37        self.size() == 0
38    }
39
40    /// Retrieve the keys in the cache
41    fn keys(&self) -> &[CacheKey];
42
43    /// Clear the cache
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the cache cannot be cleared.
48    fn clear(&mut self) -> Result<(), String>;
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum CacheKey {
53    /// Cache key for an image URL
54    ImageUrl(Url),
55    /// Cache key for an image path
56    ImagePath(PathBuf),
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct CacheValue {
61    pub data: Vec<u8>,
62    pub content_type: String,
63}
64
65#[derive(Debug)]
66pub struct InMemoryCache {
67    keys: Vec<CacheKey>,
68    cache: HashMap<CacheKey, CacheValue>,
69}
70
71// Implement Default for InMemoryCache specifically
72impl Default for InMemoryCache {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl CacheBackend for InMemoryCache {
79    fn backend_type(&self) -> &'static str {
80        "InMemory"
81    }
82
83    fn new() -> Self {
84        Self {
85            cache: HashMap::new(),
86            keys: Vec::new(),
87        }
88    }
89
90    fn get(&self, key: CacheKey) -> Option<CacheValue> {
91        self.cache.get(&key).cloned()
92    }
93
94    fn get_random(&self) -> Option<CacheValue> {
95        let keys: Vec<&CacheKey> = self.cache.keys().collect();
96        keys.choose(&mut rand::rng())
97            .and_then(|&random_key| self.cache.get(random_key).cloned())
98    }
99
100    fn set(&mut self, key: CacheKey, image: CacheValue) -> Result<(), String> {
101        if !self.keys.contains(&key) {
102            self.keys.push(key.clone());
103        }
104        self.cache.insert(key, image);
105        Ok(())
106    }
107
108    fn remove(&mut self, key: &CacheKey) -> Option<CacheValue> {
109        self.keys.retain(|k| k != key);
110        self.cache.remove(key)
111    }
112
113    fn size(&self) -> usize {
114        self.cache.len()
115    }
116
117    fn clear(&mut self) -> Result<(), String> {
118        self.cache.clear();
119        Ok(())
120    }
121
122    fn keys(&self) -> &[CacheKey] {
123        debug_assert!(
124            self.keys.len() == self.cache.len(),
125            "Keys and cache size mismatch: {} != {}",
126            self.keys.len(),
127            self.cache.len()
128        );
129        &self.keys
130    }
131}
132
133#[derive(Debug)]
134pub struct FileSystemCacheValue {
135    pub path: PathBuf,
136    pub hash: String,
137    pub content_type: String,
138}
139
140#[derive(Debug)]
141pub struct FileSystemCache {
142    tempdir: TempDir,
143    keys: Vec<CacheKey>,
144    // map of keys to file paths and the hash of the file content
145    pub cache: HashMap<CacheKey, FileSystemCacheValue>,
146}
147
148impl CacheBackend for FileSystemCache {
149    fn backend_type(&self) -> &'static str {
150        "FileSystem"
151    }
152
153    fn new() -> Self {
154        let tempdir = TempDir::new().expect("Failed to create temp dir");
155        Self {
156            tempdir,
157            keys: Vec::new(),
158            cache: HashMap::new(),
159        }
160    }
161
162    fn get(&self, key: CacheKey) -> Option<CacheValue> {
163        if let Some(FileSystemCacheValue {
164            path,
165            hash,
166            content_type,
167        }) = self.cache.get(&key)
168        {
169            if path.exists() {
170                let data = std::fs::read(path).ok()?;
171                // Validate the content type based on the file extension
172                if hash != &format!("{:x}", md5::compute(&data)) {
173                    log::warn!("Hash mismatch for cached file: {}", path.display());
174                    fs::remove_file(path).ok()?;
175                    return None;
176                }
177
178                return Some(CacheValue {
179                    data,
180                    content_type: content_type.clone(),
181                });
182            }
183        }
184        None
185    }
186
187    fn get_random(&self) -> Option<CacheValue> {
188        let keys: Vec<&CacheKey> = self.cache.keys().collect();
189        keys.choose(&mut rand::rng())
190            .copied()
191            .and_then(|random_key| self.get(random_key.clone()))
192    }
193
194    fn set(&mut self, key: CacheKey, image: CacheValue) -> Result<(), String> {
195        let file_path = self
196            .tempdir
197            .path()
198            .join(format!("{}.cache", uuid::Uuid::new_v4()));
199        std::fs::write(&file_path, &image.data).map_err(|e| e.to_string())?;
200
201        if self.keys.contains(&key) {
202            log::warn!("Key already exists in cache: {key:?}");
203            if let Some(FileSystemCacheValue { path, .. }) = self.cache.get(&key) {
204                fs::remove_file(path).ok();
205            }
206        } else {
207            self.keys.push(key.clone());
208        }
209
210        let hash = md5::compute(&image.data);
211        let hash_str = format!("{hash:x}");
212
213        let content_type = image.content_type;
214
215        self.cache.insert(
216            key,
217            FileSystemCacheValue {
218                path: file_path,
219                hash: hash_str,
220                content_type,
221            },
222        );
223        Ok(())
224    }
225
226    fn remove(&mut self, key: &CacheKey) -> Option<CacheValue> {
227        if let Some(FileSystemCacheValue { path, .. }) = self.cache.remove(key) {
228            if path.exists() {
229                let content_type = mime_guess::from_path(&path)
230                    .first_or_octet_stream()
231                    .to_string();
232                fs::remove_file(&path).ok()?;
233
234                let data = std::fs::read(path).ok()?;
235                return Some(CacheValue { data, content_type });
236            }
237        }
238        None
239    }
240
241    fn size(&self) -> usize {
242        self.cache.len()
243    }
244
245    fn clear(&mut self) -> Result<(), String> {
246        self.cache.clear();
247        Ok(())
248    }
249
250    fn keys(&self) -> &[CacheKey] {
251        debug_assert!(
252            self.keys.len() == self.cache.len(),
253            "Keys and cache size mismatch: {} != {}",
254            self.keys.len(),
255            self.cache.len()
256        );
257        &self.keys
258    }
259}