torrust_index/cache/image/
manager.rs1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::{Duration, SystemTime};
4
5use bytes::Bytes;
6use tokio::sync::RwLock;
7
8use crate::cache::BytesCache;
9use crate::config::Configuration;
10use crate::models::user::UserId;
11
12pub enum Error {
13 UrlIsUnreachable,
14 UrlIsNotAnImage,
15 ImageTooBig,
16 UserQuotaMet,
17 Unauthenticated,
18}
19
20type UserQuotas = HashMap<i64, ImageCacheQuota>;
21
22#[must_use]
28pub fn now_in_secs() -> u64 {
29 SystemTime::now()
30 .duration_since(SystemTime::UNIX_EPOCH)
31 .expect("SystemTime before UNIX EPOCH!")
32 .as_secs()
33}
34
35#[derive(Clone)]
36pub struct ImageCacheQuota {
37 pub user_id: i64,
38 pub usage: usize,
39 pub max_usage: usize,
40 pub date_start_secs: u64,
41 pub period_secs: u64,
42}
43
44impl ImageCacheQuota {
45 #[must_use]
46 pub fn new(user_id: i64, max_usage: usize, period_secs: u64) -> Self {
47 Self {
48 user_id,
49 usage: 0,
50 max_usage,
51 date_start_secs: now_in_secs(),
52 period_secs,
53 }
54 }
55
56 pub fn add_usage(&mut self, amount: usize) -> Result<(), Error> {
62 if now_in_secs() - self.date_start_secs > self.period_secs {
64 self.reset();
65 }
66
67 if self.is_reached() {
68 return Err(Error::UserQuotaMet);
69 }
70
71 self.usage = self.usage.saturating_add(amount);
72
73 Ok(())
74 }
75
76 pub fn reset(&mut self) {
77 self.usage = 0;
78 self.date_start_secs = now_in_secs();
79 }
80
81 #[must_use]
82 pub fn is_reached(&self) -> bool {
83 self.usage >= self.max_usage
84 }
85}
86
87pub struct ImageCacheService {
88 image_cache: RwLock<BytesCache>,
89 user_quotas: RwLock<UserQuotas>,
90 reqwest_client: reqwest::Client,
91 cfg: Arc<Configuration>,
92}
93
94impl ImageCacheService {
95 pub async fn new(cfg: Arc<Configuration>) -> Self {
101 let settings = cfg.settings.read().await;
102
103 let image_cache =
104 BytesCache::with_capacity_and_entry_size_limit(settings.image_cache.capacity, settings.image_cache.entry_size_limit)
105 .expect("Could not create image cache.");
106
107 let reqwest_client = reqwest::Client::builder()
108 .timeout(Duration::from_millis(settings.image_cache.max_request_timeout_ms))
109 .build()
110 .expect("unable to build client request");
111
112 drop(settings);
113
114 Self {
115 image_cache: RwLock::new(image_cache),
116 user_quotas: RwLock::new(HashMap::new()),
117 reqwest_client,
118 cfg,
119 }
120 }
121
122 pub async fn get_image_by_url(&self, url: &str, user_id: UserId) -> Result<Bytes, Error> {
129 if let Some(entry) = self.image_cache.read().await.get(url).await {
130 return Ok(entry.bytes);
131 }
132 self.check_user_quota(&user_id).await?;
133
134 let image_bytes = self.get_image_from_url_as_bytes(url).await?;
135
136 self.check_image_size(&image_bytes).await?;
137
138 self.update_image_cache(url, &image_bytes).await?;
144
145 self.update_user_quota(&user_id, image_bytes.len()).await?;
146
147 Ok(image_bytes)
148 }
149
150 async fn get_image_from_url_as_bytes(&self, url: &str) -> Result<Bytes, Error> {
151 let res = self
152 .reqwest_client
153 .clone()
154 .get(url)
155 .send()
156 .await
157 .map_err(|_| Error::UrlIsUnreachable)?;
158
159 if let Some(content_type) = res.headers().get("Content-Type") {
162 if content_type != "image/jpeg" && content_type != "image/png" {
163 return Err(Error::UrlIsNotAnImage);
164 }
165 } else {
166 return Err(Error::UrlIsNotAnImage);
167 }
168
169 res.bytes().await.map_err(|_| Error::UrlIsNotAnImage)
170 }
171
172 async fn check_user_quota(&self, user_id: &UserId) -> Result<(), Error> {
173 if let Some(quota) = self.user_quotas.read().await.get(user_id) {
174 if quota.is_reached() {
175 return Err(Error::UserQuotaMet);
176 }
177 }
178
179 Ok(())
180 }
181
182 async fn check_image_size(&self, image_bytes: &Bytes) -> Result<(), Error> {
183 let settings = self.cfg.settings.read().await;
184
185 if image_bytes.len() > settings.image_cache.entry_size_limit {
186 return Err(Error::ImageTooBig);
187 }
188
189 Ok(())
190 }
191
192 async fn update_image_cache(&self, url: &str, image_bytes: &Bytes) -> Result<(), Error> {
193 if self
194 .image_cache
195 .write()
196 .await
197 .set(url.to_string(), image_bytes.clone())
198 .await
199 .is_err()
200 {
201 return Err(Error::ImageTooBig);
202 }
203
204 Ok(())
205 }
206
207 async fn update_user_quota(&self, user_id: &UserId, amount: usize) -> Result<(), Error> {
208 let settings = self.cfg.settings.read().await;
209
210 let mut quota = self
211 .user_quotas
212 .read()
213 .await
214 .get(user_id)
215 .cloned()
216 .unwrap_or(ImageCacheQuota::new(
217 *user_id,
218 settings.image_cache.user_quota_bytes,
219 settings.image_cache.user_quota_period_seconds,
220 ));
221
222 let _ = quota.add_usage(amount);
223
224 let _ = self.user_quotas.write().await.insert(*user_id, quota);
225
226 Ok(())
227 }
228}