random_image_server/
cache.rs1use 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 fn backend_type(&self) -> &'static str;
10
11 fn new() -> Self
13 where
14 Self: Sized;
15
16 fn get(&self, key: CacheKey) -> Option<CacheValue>;
18
19 fn get_random(&self) -> Option<CacheValue>;
21
22 fn set(&mut self, key: CacheKey, image: CacheValue) -> Result<(), String>;
28
29 fn remove(&mut self, key: &CacheKey) -> Option<CacheValue>;
31
32 fn size(&self) -> usize;
34
35 fn is_empty(&self) -> bool {
37 self.size() == 0
38 }
39
40 fn keys(&self) -> &[CacheKey];
42
43 fn clear(&mut self) -> Result<(), String>;
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum CacheKey {
53 ImageUrl(Url),
55 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
71impl 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 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 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}