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 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 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}