Skip to main content

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        let compute_hash = |data: &[u8]| format!("{:x}", md5::compute(data));
164
165        if let Some(FileSystemCacheValue {
166            path,
167            hash,
168            content_type,
169        }) = self.cache.get(&key)
170            && path.exists()
171        {
172            let data = std::fs::read(path).ok()?;
173            // Validate the content type based on the file extension
174            if hash != &compute_hash(&data) {
175                tracing::warn!("Hash mismatch for cached file: {}", path.display());
176                fs::remove_file(path).ok()?;
177                return None;
178            }
179
180            return Some(CacheValue {
181                data,
182                content_type: content_type.clone(),
183            });
184        }
185        None
186    }
187
188    fn get_random(&self) -> Option<CacheValue> {
189        let keys: Vec<&CacheKey> = self.cache.keys().collect();
190        keys.choose(&mut rand::rng())
191            .copied()
192            .and_then(|random_key| self.get(random_key.clone()))
193    }
194
195    fn set(&mut self, key: CacheKey, image: CacheValue) -> Result<(), String> {
196        let file_path = self
197            .tempdir
198            .path()
199            .join(format!("{}.cache", uuid::Uuid::new_v4()));
200        std::fs::write(&file_path, &image.data).map_err(|e| e.to_string())?;
201
202        if self.keys.contains(&key) {
203            tracing::warn!("Key already exists in cache: {key:?}");
204            if let Some(FileSystemCacheValue { path, .. }) = self.cache.get(&key) {
205                fs::remove_file(path).ok();
206            }
207        } else {
208            self.keys.push(key.clone());
209        }
210
211        let hash = md5::compute(&image.data);
212        let hash_str = format!("{hash:x}");
213
214        let content_type = image.content_type;
215
216        self.cache.insert(
217            key,
218            FileSystemCacheValue {
219                path: file_path,
220                hash: hash_str,
221                content_type,
222            },
223        );
224        Ok(())
225    }
226
227    fn remove(&mut self, key: &CacheKey) -> Option<CacheValue> {
228        if let Some(FileSystemCacheValue { path, .. }) = self.cache.remove(key)
229            && path.exists()
230        {
231            let content_type = mime_guess::from_path(&path)
232                .first_or_octet_stream()
233                .to_string();
234            fs::remove_file(&path).ok()?;
235
236            let data = std::fs::read(path).ok()?;
237            return Some(CacheValue { data, content_type });
238        }
239        None
240    }
241
242    fn size(&self) -> usize {
243        self.cache.len()
244    }
245
246    fn clear(&mut self) -> Result<(), String> {
247        self.cache.clear();
248        Ok(())
249    }
250
251    fn keys(&self) -> &[CacheKey] {
252        debug_assert!(
253            self.keys.len() == self.cache.len(),
254            "Keys and cache size mismatch: {} != {}",
255            self.keys.len(),
256            self.cache.len()
257        );
258        &self.keys
259    }
260}