1use std::sync::Arc;
2
3use futures::{Stream, StreamExt, pin_mut};
4use ruma::{Mxc, OwnedMxcUri, OwnedUserId, UserId, http_headers::ContentDisposition};
5use serde::Deserialize;
6#[cfg(feature = "url_preview")]
7use serde::Serialize;
8use tuwunel_core::{
9 Err, Result, at, debug, debug_info, err,
10 utils::{
11 ReadyExt, str_from_bytes,
12 stream::{TryExpect, TryIgnore},
13 string_from_bytes,
14 },
15};
16use tuwunel_database::{Cbor, Database, Deserialized, Ignore, Interfix, Map, Txn, serialize_key};
17
18use super::{Media, preview::CachedPreview, thumbnail::Dim};
19
20pub(crate) struct Data {
21 db: Arc<Database>,
22 mediaid_file: Arc<Map>,
23 mediaid_lazy: Arc<Map>,
24 mediaid_lazycontent: Arc<Map>,
25 mediaid_pending: Arc<Map>,
26 mediaid_user: Arc<Map>,
27 url_preview: Arc<Map>,
28}
29
30#[derive(Debug)]
31pub struct Metadata {
32 pub content_disposition: Option<ContentDisposition>,
33 pub content_type: Option<String>,
34 pub(super) key: Vec<u8>,
35}
36
37#[cfg(feature = "url_preview")]
39#[derive(Serialize)]
40struct LazyContentRef<'a> {
41 content_type: Option<&'a str>,
42 content_disposition: Option<&'a str>,
43 #[serde(with = "serde_bytes")]
44 content: &'a [u8],
45}
46
47#[derive(Deserialize)]
50struct LazyContent {
51 content_type: Option<String>,
52 content_disposition: Option<String>,
53 #[serde(with = "serde_bytes")]
54 content: Vec<u8>,
55}
56
57impl From<LazyContent> for Media {
58 fn from(lazy: LazyContent) -> Self {
59 let content_disposition = lazy
60 .content_disposition
61 .and_then(|disposition| disposition.parse().ok());
62
63 Self {
64 content: lazy.content,
65 content_type: lazy.content_type,
66 content_disposition,
67 }
68 }
69}
70
71impl Data {
72 pub(super) fn new(db: &Arc<Database>) -> Self {
73 Self {
74 db: db.clone(),
75 mediaid_file: db["mediaid_file"].clone(),
76 mediaid_lazy: db["mediaid_lazy"].clone(),
77 mediaid_lazycontent: db["mediaid_lazycontent"].clone(),
78 mediaid_pending: db["mediaid_pending"].clone(),
79 mediaid_user: db["mediaid_user"].clone(),
80 url_preview: db["url_preview"].clone(),
81 }
82 }
83
84 pub(super) fn create_file_metadata(
85 &self,
86 mxc: &Mxc<'_>,
87 user: Option<&UserId>,
88 dim: &Dim,
89 content_disposition: Option<&ContentDisposition>,
90 content_type: Option<&str>,
91 ) -> Result<Vec<u8>> {
92 let dim: &[u32] = &[dim.width, dim.height];
93 let key = (mxc, dim, content_disposition, content_type);
94 let key = serialize_key(key)?;
95 let mut txn = self.db.txn();
96
97 txn.insert_raw(&self.mediaid_file, &key, []);
98 if let Some(user) = user {
99 let key = (mxc, user);
100
101 txn.put_raw(&self.mediaid_user, key, user);
102 }
103
104 txn.execute();
105
106 Ok(key.to_vec())
107 }
108
109 pub(super) fn insert_pending_mxc(
111 &self,
112 mxc: &Mxc<'_>,
113 user: &UserId,
114 unused_expires_at: u64,
115 ) {
116 let value = (unused_expires_at, user);
117 debug!(?mxc, ?user, ?unused_expires_at, "Inserting pending");
118
119 self.mediaid_pending
120 .raw_put(mxc.to_string(), value);
121 }
122
123 pub(super) fn remove_pending_mxc(&self, mxc: &Mxc<'_>) {
125 self.mediaid_pending.remove(&mxc.to_string());
126 }
127
128 pub(super) async fn count_pending_mxc_for_user(&self, user_id: &UserId) -> (usize, u64) {
130 type KeyVal<'a> = (Ignore, (u64, &'a UserId));
131
132 self.mediaid_pending
133 .stream()
134 .expect_ok()
135 .ready_filter(|(_, (_, pending_user_id)): &KeyVal<'_>| user_id == *pending_user_id)
136 .ready_fold(
137 (0_usize, u64::MAX),
138 |(count, earliest_expiration), (_, (expires_at, _))| {
139 (count.saturating_add(1), earliest_expiration.min(expires_at))
140 },
141 )
142 .await
143 }
144
145 pub(super) async fn search_pending_mxc(&self, mxc: &Mxc<'_>) -> Result<(OwnedUserId, u64)> {
147 type Value<'a> = (u64, OwnedUserId);
148
149 self.mediaid_pending
150 .get(&mxc.to_string())
151 .await
152 .deserialized()
153 .map(|(expires_at, user_id): Value<'_>| (user_id, expires_at))
154 .inspect(|(user_id, expires_at)| debug!(?mxc, ?user_id, ?expires_at, "Found pending"))
155 .map_err(|e| err!(Request(NotFound("Pending not found or error: {e}"))))
156 }
157
158 #[cfg(feature = "url_preview")]
161 pub(super) fn insert_lazy_media(&self, mxc: &str, url: &str) {
162 debug!(?mxc, ?url, "Registering lazy media");
163
164 self.mediaid_lazy.insert(mxc, url.as_bytes());
165 }
166
167 #[cfg(feature = "url_preview")]
168 pub(super) fn queue_lazy_media(&self, txn: &mut Txn, mxc: &str, url: &str) {
169 debug!(?mxc, ?url, "Registering lazy media");
170
171 txn.insert_raw(&self.mediaid_lazy, mxc, url.as_bytes());
172 }
173
174 pub(super) fn remove_lazy_media(&self, txn: &mut Txn, mxc: &str) {
177 txn.del_raw(&self.mediaid_lazy, mxc);
178 }
179
180 pub(super) async fn search_lazy_media(&self, mxc: &Mxc<'_>) -> Result<String> {
182 let handle = self.mediaid_lazy.get(&mxc.to_string()).await?;
183
184 string_from_bytes(&handle)
185 .map_err(|e| err!(Database(error!(?mxc, "Lazy media URL is invalid: {e}"))))
186 }
187
188 #[cfg(feature = "url_preview")]
191 pub(super) fn set_lazy_content(
192 &self,
193 txn: &mut Txn,
194 mxc: &str,
195 content_type: Option<&str>,
196 content_disposition: Option<&str>,
197 content: &[u8],
198 ) {
199 let value = LazyContentRef {
200 content_type,
201 content_disposition,
202 content,
203 };
204
205 txn.raw_put(&self.mediaid_lazycontent, mxc, Cbor(&value));
206 }
207
208 pub(super) async fn get_lazy_content(&self, mxc: &str) -> Result<Media> {
210 self.mediaid_lazycontent
211 .get(mxc)
212 .await
213 .deserialized::<Cbor<LazyContent>>()
214 .map(at!(0))
215 .map(Into::into)
216 }
217
218 pub(super) fn remove_lazy_content(&self, txn: &mut Txn, mxc: &str) {
219 txn.del_raw(&self.mediaid_lazycontent, mxc);
220 }
221
222 pub(super) async fn delete_file_mxc(&self, mxc: &Mxc<'_>) {
223 debug!("MXC URI: {mxc}");
224
225 let prefix = (mxc, Interfix);
226 let txn = self
227 .mediaid_file
228 .keys_prefix_raw(&prefix)
229 .ignore_err()
230 .ready_fold(self.db.txn(), |mut txn, key| {
231 txn.del_raw(&self.mediaid_file, key);
232
233 txn
234 })
235 .await;
236
237 let txn = self
238 .mediaid_user
239 .stream_prefix_raw(&prefix)
240 .ignore_err()
241 .ready_fold(txn, |mut txn, (key, val)| {
242 debug_assert!(
243 key.starts_with(mxc.to_string().as_bytes()),
244 "key should start with the mxc"
245 );
246
247 let user = str_from_bytes(val).unwrap_or_default();
248 debug_info!("Deleting key {key:?} which was uploaded by user {user}");
249
250 txn.del_raw(&self.mediaid_user, key);
251
252 txn
253 })
254 .await;
255
256 txn.execute();
257 }
258
259 pub(super) async fn search_mxc_metadata_prefix(&self, mxc: &Mxc<'_>) -> Result<Vec<Vec<u8>>> {
261 debug!("MXC URI: {mxc}");
262
263 let prefix = (mxc, Interfix);
264 let keys: Vec<Vec<u8>> = self
265 .mediaid_file
266 .keys_prefix_raw(&prefix)
267 .ignore_err()
268 .map(<[u8]>::to_vec)
269 .collect()
270 .await;
271
272 if keys.is_empty() {
273 return Err!(Database("Failed to find any keys in database for `{mxc}`",));
274 }
275
276 debug!("Got the following keys: {keys:?}");
277
278 Ok(keys)
279 }
280
281 pub(super) async fn file_metadata_exists(&self, mxc: &Mxc<'_>, dim: &Dim) -> bool {
282 let dim: &[u32] = &[dim.width, dim.height];
283 let prefix = (mxc, dim, Interfix);
284 let keys = self
285 .mediaid_file
286 .keys_prefix_raw(&prefix)
287 .ignore_err();
288
289 pin_mut!(keys);
290 keys.next().await.is_some()
291 }
292
293 pub(super) async fn search_file_metadata(
294 &self,
295 mxc: &Mxc<'_>,
296 dim: &Dim,
297 ) -> Result<Metadata> {
298 let dim: &[u32] = &[dim.width, dim.height];
299 let prefix = (mxc, dim, Interfix);
300
301 let keys = self
302 .mediaid_file
303 .keys_prefix_raw(&prefix)
304 .ignore_err()
305 .map(ToOwned::to_owned);
306
307 pin_mut!(keys);
308 let key = keys
309 .next()
310 .await
311 .ok_or_else(|| err!(Request(NotFound("Media not found"))))?;
312
313 let mut parts = key.rsplit(|&b| b == 0xFF);
314
315 let content_type = parts
316 .next()
317 .map(string_from_bytes)
318 .transpose()
319 .map_err(|e| err!(Database(error!(?mxc, "Content-type is invalid: {e}"))))?;
320
321 let content_disposition = parts
322 .next()
323 .map(Some)
324 .ok_or_else(|| err!(Database(error!(?mxc, "Media ID in db is invalid."))))?
325 .filter(|bytes| !bytes.is_empty())
326 .map(string_from_bytes)
327 .transpose()
328 .map_err(|e| err!(Database(error!(?mxc, "Content-disposition is invalid: {e}"))))?
329 .as_deref()
330 .map(str::parse)
331 .transpose()
332 .map_err(|e| err!(Database(error!(?mxc, "Content-disposition is invalid: {e}"))))?;
333
334 Ok(Metadata { content_disposition, content_type, key })
335 }
336
337 pub(super) async fn mxc_user(&self, mxc: &Mxc<'_>) -> Option<OwnedUserId> {
340 let prefix = (mxc, Interfix);
341 let users = self
342 .mediaid_user
343 .stream_prefix(&prefix)
344 .ignore_err()
345 .map(|(_, user): (Ignore, &UserId)| user.to_owned());
346
347 pin_mut!(users);
348 users.next().await
349 }
350
351 pub(super) async fn get_all_user_mxcs(&self, user_id: &UserId) -> Vec<OwnedMxcUri> {
353 self.mediaid_user
354 .stream()
355 .ignore_err()
356 .ready_filter_map(|((key, _), user): ((&str, Ignore), &UserId)| {
357 (user == user_id).then(|| key.into())
358 })
359 .collect()
360 .await
361 }
362
363 pub(crate) async fn get_all_media_keys(&self) -> Vec<Vec<u8>> {
366 self.mediaid_file
367 .raw_keys()
368 .ignore_err()
369 .map(<[u8]>::to_vec)
370 .collect()
371 .await
372 }
373
374 pub(super) fn set_url_preview(&self, url: &str, cached: &CachedPreview) -> Result {
375 self.url_preview.raw_put(url, Cbor(cached));
376
377 Ok(())
378 }
379
380 pub(super) async fn get_url_preview(&self, url: &str) -> Result<CachedPreview> {
381 self.url_preview
382 .get(url)
383 .await
384 .deserialized::<Cbor<_>>()
385 .map(at!(0))
386 .ok()
387 .filter(CachedPreview::valid)
388 .ok_or(err!(Request(NotFound("Expired from cache"))))
389 }
390
391 pub(super) fn all_uploads(
393 &self,
394 ) -> impl Stream<Item = (OwnedMxcUri, OwnedUserId)> + Send + '_ {
395 self.mediaid_user
396 .keys()
397 .ignore_err()
398 .map(|(mxc, user): (&str, &UserId)| (mxc.into(), user.to_owned()))
399 }
400}
401
402#[cfg(feature = "url_preview")]
403#[cfg(test)]
404mod tests {
405 use minicbor_serde::{from_slice, to_vec};
406
407 use super::{LazyContent, LazyContentRef, Media};
408
409 #[test]
410 fn lazy_content_roundtrip() {
411 let content: &[u8] = b"\x00\x01\xFF\xFE arbitrary staged bytes";
412 let value = LazyContentRef {
413 content_type: Some("image/png"),
414 content_disposition: Some("inline; filename=\"cat.png\""),
415 content,
416 };
417
418 let bytes = to_vec(&value).expect("encodes");
419 let decoded: LazyContent = from_slice(&bytes).expect("decodes");
420
421 assert_eq!(decoded.content_type.as_deref(), Some("image/png"));
422 assert_eq!(decoded.content.as_slice(), content);
423
424 let media = Media::from(decoded);
425 assert_eq!(media.content.as_slice(), content);
426 assert!(media.content_disposition.is_some(), "disposition re-parses to the ruma type");
427 }
428
429 #[test]
430 fn lazy_content_bytes_compact() {
431 let content = vec![0xAB_u8; 4096];
432 let value = LazyContentRef {
433 content_type: None,
434 content_disposition: None,
435 content: content.as_slice(),
436 };
437
438 let bytes = to_vec(&value).expect("encodes");
439
440 assert!(bytes.len() <= content.len() + 64);
443 }
444}