Skip to main content

tuwunel_service/media/
data.rs

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/// Borrowed staging-cache value: written zero-copy from the measured bytes.
38#[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/// Owned staging-cache value read back at promotion. `ContentDisposition` is
48/// Serialize-only, so the disposition rides as its header string.
49#[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	/// Insert a pending MXC URI into the database
110	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	/// Remove a pending MXC URI from the database
124	pub(super) fn remove_pending_mxc(&self, mxc: &Mxc<'_>) {
125		self.mediaid_pending.remove(&mxc.to_string());
126	}
127
128	/// Count the number of pending MXC URIs for a specific user
129	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	/// Search for a pending MXC URI in the database
146	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	/// Map a minted mxc:// URI to the external URL it resolves to on first
159	/// download (see `Service::fetch_lazy_media`).
160	#[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	/// Remove a lazy media reference by its mxc:// URI string, unregistering
175	/// the mxc.
176	pub(super) fn remove_lazy_media(&self, txn: &mut Txn, mxc: &str) {
177		txn.del_raw(&self.mediaid_lazy, mxc);
178	}
179
180	/// Look up the external URL a lazy media MXC URI refers to.
181	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	/// Stage the measured preview media bytes under its minted mxc so the
189	/// first client download can promote without touching the origin.
190	#[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	/// Take the staged bytes a preview seeded for a lazy media mxc, if any.
209	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	/// Searches for all files with the given MXC
260	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	/// Uploading local user of the media at the given MXC, from the uploader
338	/// index.
339	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	/// Gets all the MXCs associated with a user
352	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	/// Gets all the media keys in our database (this includes all the metadata
364	/// associated with it such as width, height, content-type, etc)
365	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	/// Streams every (mxc, uploader) pair in the user-media index.
392	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		// serde_bytes must encode a CBOR byte string, not an array-of-uints
441		// (~1.9x); only a small fixed header of overhead is permitted
442		assert!(bytes.len() <= content.len() + 64);
443	}
444}