Skip to main content

tuwunel_service/media/
mod.rs

1mod data;
2pub(super) mod migrations;
3mod preview;
4mod remote;
5mod tests;
6mod thumbnail;
7#[cfg(feature = "media_thumbnail")]
8mod video;
9use std::{
10	collections::{HashMap, HashSet},
11	path::PathBuf,
12	sync::{Arc, Mutex},
13	time::{Duration, Instant, SystemTime},
14};
15
16use async_trait::async_trait;
17use base64::{Engine as _, engine::general_purpose};
18use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, pin_mut};
19use http::StatusCode;
20use object_store::ObjectMeta;
21use ruma::{
22	Mxc, OwnedMxcUri, OwnedUserId, UserId,
23	api::error::{ErrorKind, RetryAfter},
24	http_headers::ContentDisposition,
25};
26#[cfg(feature = "media_thumbnail")]
27use tokio::sync::Semaphore;
28use tokio::{fs, sync::Notify};
29use tuwunel_core::{
30	Err, Error, Result, debug, debug_error, debug_info, debug_warn, err, trace,
31	utils::{
32		self, BoolExt, MutexMap,
33		result::LogDebugErr,
34		stream::{BroadbandExt, IterStream, ReadyExt, TryReadyExt},
35		time::now_millis,
36	},
37	warn,
38};
39use url::Url;
40
41#[cfg(feature = "media_thumbnail")]
42use self::video::{FAILURES, Failures, sweep_staging_dir};
43use self::{data::Data, preview::Agent, remote::Fetch};
44pub use self::{data::Metadata, preview::UrlPreviewData, thumbnail::Dim};
45use crate::storage::Provider;
46
47#[derive(Debug)]
48pub struct Media {
49	pub content: Vec<u8>,
50	pub content_type: Option<String>,
51	pub content_disposition: Option<ContentDisposition>,
52}
53
54/// One row of a user's uploaded media, holding only the fields tuwunel can
55/// derive from the uploader index and storage-provider object metadata. The
56/// uploader is carried when the index holds a row for it.
57#[derive(Clone, Debug)]
58pub struct UserMediaEntry {
59	pub mxc: OwnedMxcUri,
60	pub media_type: Option<String>,
61	pub upload_name: Option<String>,
62	pub media_length: Option<u64>,
63	pub created_ts: u64,
64	pub user_id: Option<OwnedUserId>,
65}
66
67/// One locally-uploaded media item's uploader and storage-object byte length
68/// and modification time, the row shape of the media-statistics scan.
69#[derive(Clone, Debug)]
70pub struct UploadStat {
71	pub user_id: OwnedUserId,
72	pub media_length: u64,
73	pub created_ts: u64,
74}
75
76/// For MSC2246
77struct MXCState {
78	/// Save the notifier for each pending media upload
79	notifiers: Mutex<HashMap<OwnedMxcUri, Arc<Notify>>>,
80	/// Save the ratelimiter for each user
81	ratelimiter: Mutex<HashMap<OwnedUserId, (Instant, f64)>>,
82}
83
84pub struct Service {
85	pub(super) db: Data,
86	services: Arc<crate::services::OnceServices>,
87	url_preview_mutex: MutexMap<String, ()>,
88	federation_mutex: MutexMap<String, ()>,
89	mxc_state: MXCState,
90	#[cfg(feature = "media_thumbnail")]
91	video_thumbnail_slots: Semaphore,
92	#[cfg(feature = "media_thumbnail")]
93	video_thumbnail_failures: Mutex<Failures>,
94}
95
96/// generated MXC ID (`media-id`) length
97pub const MXC_LENGTH: usize = 32;
98
99/// Cache control for immutable objects.
100pub const CACHE_CONTROL_IMMUTABLE: &str = "private,max-age=31536000,immutable";
101
102/// Default cross-origin resource policy.
103pub const CORP_CROSS_ORIGIN: &str = "cross-origin";
104
105/// Validity window for a presigned media download redirect (MSC3860).
106const REDIRECT_TTL: Duration = Duration::from_mins(5);
107
108#[async_trait]
109impl crate::Service for Service {
110	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
111		let service = Arc::new(Self {
112			db: Data::new(args.db),
113			services: args.services.clone(),
114			url_preview_mutex: MutexMap::new(),
115			federation_mutex: MutexMap::new(),
116			mxc_state: MXCState {
117				notifiers: Mutex::new(HashMap::new()),
118				ratelimiter: Mutex::new(HashMap::new()),
119			},
120			#[cfg(feature = "media_thumbnail")]
121			video_thumbnail_failures: Failures::new(FAILURES).into(),
122			#[cfg(feature = "media_thumbnail")]
123			video_thumbnail_slots: Semaphore::new(
124				args.server
125					.config
126					.media_video_thumbnail_concurrency
127					.max(1),
128			),
129		});
130
131		#[cfg(feature = "media_thumbnail")]
132		sweep_staging_dir(&args.server.config);
133
134		Ok(service)
135	}
136
137	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
138}
139
140impl Service {
141	/// Create a pending media upload ID.
142	#[tracing::instrument(level = "debug", skip(self))]
143	pub async fn create_pending(
144		&self,
145		mxc: &Mxc<'_>,
146		user: &UserId,
147		unused_expires_at: u64,
148	) -> Result {
149		let config = &self.services.server.config;
150
151		// Rate limiting (rc_media_create)
152		let rate = f64::from(config.media_rc_create_per_second);
153		let burst = f64::from(config.media_rc_create_burst_count);
154
155		// Check rate limiting
156		if rate > 0.0 && burst > 0.0 {
157			let now = Instant::now();
158			let mut ratelimiter = self.mxc_state.ratelimiter.lock()?;
159
160			let (last_time, tokens) = ratelimiter
161				.entry(user.to_owned())
162				.or_insert_with(|| (now, burst));
163
164			let elapsed = now.duration_since(*last_time).as_secs_f64();
165			let new_tokens = elapsed.mul_add(rate, *tokens).min(burst);
166
167			if new_tokens >= 1.0 {
168				*last_time = now;
169				*tokens = new_tokens - 1.0;
170			} else {
171				return Err(Error::Request(
172					ErrorKind::LimitExceeded(ruma::api::error::LimitExceededErrorData {
173						retry_after: None,
174					}),
175					"Too many pending media creation requests.".into(),
176					StatusCode::TOO_MANY_REQUESTS,
177				));
178			}
179		}
180
181		let max_uploads = config.max_pending_media_uploads;
182		let (current_uploads, earliest_expiration) =
183			self.db.count_pending_mxc_for_user(user).await;
184
185		// Check if the user has reached the maximum number of pending media uploads
186		if current_uploads >= max_uploads {
187			let retry_after = earliest_expiration.saturating_sub(now_millis());
188			return Err(Error::Request(
189				ErrorKind::LimitExceeded(ruma::api::error::LimitExceededErrorData {
190					retry_after: Some(RetryAfter::Delay(Duration::from_millis(retry_after))),
191				}),
192				"Maximum number of pending media uploads reached.".into(),
193				StatusCode::TOO_MANY_REQUESTS,
194			));
195		}
196
197		self.db
198			.insert_pending_mxc(mxc, user, unused_expires_at);
199
200		Ok(())
201	}
202
203	/// Uploads content to a pending media ID.
204	#[tracing::instrument(level = "debug", skip(self))]
205	pub async fn upload_pending(
206		&self,
207		mxc: &Mxc<'_>,
208		user: &UserId,
209		content_disposition: Option<&ContentDisposition>,
210		content_type: Option<&str>,
211		file: &[u8],
212	) -> Result {
213		let Ok((owner_id, expires_at)) = self.db.search_pending_mxc(mxc).await else {
214			if self.get_metadata(mxc).await.is_some() {
215				return Err!(Request(CannotOverwriteMedia("Media ID already has content")));
216			}
217
218			return Err!(Request(NotFound("Media not found")));
219		};
220
221		if owner_id != user {
222			return Err!(Request(Forbidden("You did not create this media ID")));
223		}
224
225		let current_time = now_millis();
226		if expires_at < current_time {
227			return Err!(Request(NotFound("Pending media ID expired")));
228		}
229
230		self.create(mxc, Some(user), content_disposition, content_type, file)
231			.await?;
232
233		self.db.remove_pending_mxc(mxc);
234
235		let mxc_uri: OwnedMxcUri = mxc.to_string().into();
236		let notifier = self.mxc_state.notifiers.lock()?.remove(&mxc_uri);
237
238		if let Some(notifier) = notifier {
239			notifier.notify_waiters();
240		}
241
242		Ok(())
243	}
244
245	/// Uploads a file.
246	pub async fn create(
247		&self,
248		mxc: &Mxc<'_>,
249		user: Option<&UserId>,
250		content_disposition: Option<&ContentDisposition>,
251		content_type: Option<&str>,
252		file: &[u8],
253	) -> Result {
254		// Width, Height = 0 if it's not a thumbnail
255		let key = self.db.create_file_metadata(
256			mxc,
257			user,
258			&Dim::default(),
259			content_disposition,
260			content_type,
261		)?;
262
263		//TODO: Dangling metadata in database if creation fails
264		self.create_media_file(&key, file).await
265	}
266
267	/// Deletes a file in the database and from the media directory via an MXC
268	#[tracing::instrument(level = "trace", skip(self))]
269	pub async fn delete(&self, mxc: &Mxc<'_>) -> Result {
270		// lazy URL-preview media has no file keys of its own; drop its reference
271		// and staged bytes whenever present so a delete can't re-mint the media
272		let had_lazy = self.db.search_lazy_media(mxc).await.is_ok();
273		if had_lazy {
274			let key = mxc.to_string();
275			let mut txn = self.services.db.txn();
276
277			self.db.remove_lazy_media(&mut txn, &key);
278			self.db.remove_lazy_content(&mut txn, &key);
279			txn.execute();
280		}
281
282		match self.db.search_mxc_metadata_prefix(mxc).await {
283			| Ok(keys) => {
284				for key in keys {
285					trace!(?mxc, "MXC Key: {key:?}");
286					debug_info!(?mxc, "Deleting from storage provider");
287
288					if let Err(e) = self.remove_media_file(&key).await {
289						debug_error!(?mxc, "Failed to remove media file: {e}");
290					}
291
292					debug_info!(?mxc, "Deleting from database");
293					self.db.delete_file_mxc(mxc).await;
294				}
295
296				Ok(())
297			},
298			| _ if had_lazy => Ok(()),
299			| _ => Err!(Database(error!(
300				"Failed to find any media keys for MXC {mxc} in our database."
301			))),
302		}
303	}
304
305	/// Deletes all media by the specified user
306	///
307	/// currently, this is only practical for local users
308	#[tracing::instrument(level = "trace", skip(self))]
309	pub async fn delete_from_user(&self, user: &UserId) -> Result<usize> {
310		let mxcs = self.db.get_all_user_mxcs(user).await;
311		let mut deletion_count: usize = 0;
312
313		for mxc in mxcs {
314			let Ok(mxc) = mxc.as_str().try_into().inspect_err(|e| {
315				debug_error!(?mxc, "Failed to parse MXC URI from database: {e}");
316			}) else {
317				continue;
318			};
319
320			debug_info!(
321				%deletion_count,
322				"Deleting MXC {mxc} by user {user} from database and filesystem",
323			);
324			match self.delete(&mxc).await {
325				| Ok(()) => {
326					deletion_count = deletion_count.saturating_add(1);
327				},
328				| Err(e) => {
329					debug_error!(
330						%deletion_count,
331						"Failed to delete {mxc} from user {user}, ignoring error: {e}"
332					);
333				},
334			}
335		}
336
337		Ok(deletion_count)
338	}
339
340	/// Get file from local storage or make a federation request if it
341	/// originates remotely.
342	#[tracing::instrument(
343		level = "debug",
344		err(level = "debug")
345		skip(self),
346	)]
347	pub async fn get_or_fetch(&self, mxc: &Mxc<'_>, timeout_ms: Duration) -> Result<Media> {
348		if let Ok(media) = self.get(mxc, Some(timeout_ms)).await {
349			return Ok(media);
350		}
351
352		if self
353			.services
354			.globals
355			.server_is_ours(mxc.server_name)
356		{
357			return Err!(Request(NotFound("Local media not found.")));
358		}
359
360		let lock = self.federation_mutex.lock(&mxc.to_string()).await;
361
362		if self
363			.db
364			.file_metadata_exists(mxc, &Dim::default())
365			.await
366		{
367			drop(lock);
368			return self.get(mxc, None).await;
369		}
370
371		self.fetch_remote_content(mxc, None, timeout_ms)
372			.await
373	}
374
375	/// Get file from local storage while waiting up to a timeout_ms if it is
376	/// pending.
377	#[tracing::instrument(
378		level = "debug",
379		err(level = "trace")
380		skip(self),
381	)]
382	pub async fn get(&self, mxc: &Mxc<'_>, timeout: Option<Duration>) -> Result<Media> {
383		if let Ok(meta) = self.get_stored(mxc).await {
384			return Ok(meta);
385		}
386
387		let Some(timeout) = timeout else {
388			return Err!(Request(NotFound("Media not found.")));
389		};
390
391		let Ok(_pending) = self.db.search_pending_mxc(mxc).await else {
392			return Err!(Request(NotFound("Media not found.")));
393		};
394
395		let notifier = self
396			.mxc_state
397			.notifiers
398			.lock()?
399			.entry(mxc.to_string().into())
400			.or_insert_with(|| Arc::new(Notify::new()))
401			.clone();
402
403		if tokio::time::timeout(timeout, notifier.notified())
404			.await
405			.is_err()
406		{
407			return Err!(Request(NotYetUploaded("Media has not been uploaded yet")));
408		}
409
410		self.get_stored(mxc).await
411	}
412
413	/// Get file from local storage.
414	#[tracing::instrument(level = "debug", skip(self))]
415	pub async fn get_stored(&self, mxc: &Mxc<'_>) -> Result<Media> {
416		let meta = self
417			.db
418			.search_file_metadata(mxc, &Dim::default())
419			.await;
420
421		let Ok(Metadata { content_type, content_disposition, key }) = meta else {
422			return self.fetch_lazy_media(mxc).await;
423		};
424
425		let path = self.get_media_name_sha256(&key);
426		let fetch = self
427			.storage_providers()
428			.stream()
429			.filter_map(async |provider| {
430				provider
431					.get(path.as_str())
432					.await
433					.log_debug_err()
434					.ok()
435			});
436
437		pin_mut!(fetch);
438		let Some(bytes) = fetch.next().await else {
439			return Err!(Request(NotFound("Media not found.")));
440		};
441
442		Ok(Media {
443			content: bytes.to_vec(),
444			content_type,
445			content_disposition,
446		})
447	}
448
449	/// Resolve lazy URL-preview media on first download, promoting the staged
450	/// or freshly-fetched bytes into the media store so the origin is fetched
451	/// at most once per item.
452	#[tracing::instrument(level = "debug", skip(self))]
453	async fn fetch_lazy_media(&self, mxc: &Mxc<'_>) -> Result<Media> {
454		let key = mxc.to_string();
455
456		// bound outbound amplification to one in-flight fetch per mxc; requests
457		// for the same mxc queue rather than fan out
458		let _lock = self.federation_mutex.lock(&key).await;
459
460		// a queued waiter for the same mxc may have promoted it already
461		if self
462			.db
463			.search_file_metadata(mxc, &Dim::default())
464			.await
465			.is_ok()
466		{
467			// box the cold re-entry to break the get_stored/fetch_lazy_media cycle
468			return Box::pin(self.get_stored(mxc)).await;
469		}
470
471		let media = match self.db.get_lazy_content(&key).await {
472			| Ok(media) => media,
473			| Err(_) => {
474				let Ok(url) = self.db.search_lazy_media(mxc).await else {
475					return Err!(Request(NotFound("Media not found.")));
476				};
477
478				let limit = self.services.config.url_preview_max_media_size;
479
480				self.location_request(Fetch::Preview(Agent::Media), &url, limit)
481					.await?
482			},
483		};
484
485		// promote through the ordinary upload path so real file metadata makes
486		// every later download and thumbnail a normal-media hit
487		if let Err(e) = self
488			.create(
489				mxc,
490				None,
491				media.content_disposition.as_ref(),
492				media.content_type.as_deref(),
493				&media.content,
494			)
495			.await
496		{
497			// a failed promotion leaves metadata without bytes, masking the lazy
498			// fallback on every later read; drop it so the next read retries
499			self.db.delete_file_mxc(mxc).await;
500
501			return Err(e);
502		}
503
504		let mut txn = self.services.db.txn();
505
506		self.db.remove_lazy_media(&mut txn, &key);
507		self.db.remove_lazy_content(&mut txn, &key);
508		txn.execute();
509
510		Ok(media)
511	}
512
513	/// Presigned redirect URL for locally-stored media (MSC3860).
514	///
515	/// Returns the first configured provider's signed URL for the object, or
516	/// `None` when redirects are disabled, the media is unknown, or no provider
517	/// can presign (filesystem-only media).
518	#[tracing::instrument(level = "debug", skip(self))]
519	pub async fn redirect_url(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result<Option<Url>> {
520		if !self.services.config.media_allow_redirect {
521			return Ok(None);
522		}
523
524		let Ok(Metadata { key, .. }) = self.db.search_file_metadata(mxc, dim).await else {
525			return Ok(None);
526		};
527
528		let path = self.get_media_name_sha256(&key);
529		let urls = self
530			.storage_providers()
531			.stream()
532			.filter_map(async |provider| {
533				provider
534					.signed_get_url(path.as_str(), REDIRECT_TTL)
535					.await
536					.log_debug_err()
537					.ok()
538					.flatten()
539			});
540
541		pin_mut!(urls);
542
543		Ok(urls.next().await)
544	}
545
546	/// Gets all the MXC URIs in our media database
547	pub async fn get_all_mxcs(&self) -> Result<Vec<OwnedMxcUri>> {
548		let all_keys = self.db.get_all_media_keys().await;
549
550		let mut mxcs = Vec::with_capacity(all_keys.len());
551
552		for key in all_keys {
553			trace!("Full MXC key from database: {key:?}");
554
555			let mut parts = key.split(|&b| b == 0xFF);
556			let mxc = parts
557				.next()
558				.map(|bytes| {
559					utils::string_from_bytes(bytes).map_err(|e| {
560						err!(Database(error!(
561							"Failed to parse MXC unicode bytes from our database: {e}"
562						)))
563					})
564				})
565				.transpose()?;
566
567			let Some(mxc_s) = mxc else {
568				debug_warn!(
569					?mxc,
570					"Parsed MXC URL unicode bytes from database but is still invalid"
571				);
572				continue;
573			};
574
575			trace!("Parsed MXC key to URL: {mxc_s}");
576			let mxc = OwnedMxcUri::from(mxc_s);
577
578			if mxc.is_valid() {
579				mxcs.push(mxc);
580			} else {
581				debug_warn!("{mxc:?} from database was found to not be valid");
582			}
583		}
584
585		Ok(mxcs)
586	}
587
588	/// Every media item uploaded by a local user, carrying the fields tuwunel
589	/// can derive: content type, upload name, byte length and modification time
590	/// from storage-provider object metadata. Untracked columns (last-access,
591	/// quarantine, url-cache) are not represented.
592	#[tracing::instrument(level = "debug", skip(self))]
593	pub async fn user_media(&self, user: &UserId) -> Result<Vec<UserMediaEntry>> {
594		let entries = self
595			.db
596			.get_all_user_mxcs(user)
597			.await
598			.into_iter()
599			.stream()
600			.broad_filter_map(async |mxc| self.user_media_entry(Some(user), mxc).await)
601			.collect()
602			.await;
603
604		Ok(entries)
605	}
606
607	/// Derivable metadata for the single media item at the given MXC, with the
608	/// uploading local user resolved from the uploader index when present.
609	#[tracing::instrument(level = "debug", skip(self))]
610	pub async fn media_entry(&self, mxc: &Mxc<'_>) -> Option<UserMediaEntry> {
611		let user = self.db.mxc_user(mxc).await;
612
613		self.user_media_entry(user.as_deref(), mxc.to_string().into())
614			.await
615	}
616
617	async fn user_media_entry(
618		&self,
619		user: Option<&UserId>,
620		mxc: OwnedMxcUri,
621	) -> Option<UserMediaEntry> {
622		let parts = mxc.parts().ok()?;
623		let Metadata { content_type, content_disposition, key } =
624			self.get_metadata(&parts).await?;
625
626		let object = self.head_meta(&key).await;
627		let upload_name = content_disposition.and_then(|disposition| disposition.filename);
628
629		Some(UserMediaEntry {
630			media_type: content_type,
631			upload_name,
632			media_length: object.as_ref().map(|object| object.size),
633			created_ts: object.as_ref().map(mtime_millis).unwrap_or(0),
634			user_id: user.map(ToOwned::to_owned),
635			mxc,
636		})
637	}
638
639	/// Uploader, byte length and storage modification time of every media item
640	/// uploaded by a local user, one row per upload; media missing from every
641	/// storage provider are skipped.
642	pub fn upload_stats(&self) -> impl Stream<Item = UploadStat> + Send + '_ {
643		self.db
644			.all_uploads()
645			.broad_filter_map(async |(mxc, user_id)| {
646				let parts = mxc.parts().ok()?;
647				let Metadata { key, .. } = self.get_metadata(&parts).await?;
648				let object = self.head_meta(&key).await?;
649
650				Some(UploadStat {
651					user_id,
652					media_length: object.size,
653					created_ts: mtime_millis(&object),
654				})
655			})
656	}
657
658	/// Deletes local media older than `before_ts` (by storage-provider mtime)
659	/// and strictly larger than `size_gt` bytes, sparing profile and
660	/// room-avatar media when `keep_profiles`. Returns the deleted MXCs, empty
661	/// when none match. Quarantine and protection flags are not tracked, so no
662	/// media is spared on those grounds.
663	#[tracing::instrument(level = "debug", skip(self))]
664	pub async fn delete_by_date_size(
665		&self,
666		before_ts: u64,
667		size_gt: u64,
668		keep_profiles: bool,
669	) -> Result<Vec<OwnedMxcUri>> {
670		let spared = keep_profiles
671			.then_async(|| self.avatar_mxcs())
672			.await
673			.unwrap_or_default();
674
675		let candidates = self.get_all_mxcs().await?;
676
677		let deleted: Vec<OwnedMxcUri> = candidates
678			.into_iter()
679			.stream()
680			.ready_filter(|mxc| self.is_local(mxc) && !spared.contains(mxc))
681			.broad_filter_map(async |mxc| {
682				let parts = mxc.parts().ok()?;
683				let Metadata { key, .. } = self.get_metadata(&parts).await?;
684				let object = self.head_meta(&key).await?;
685
686				let eligible = mtime_millis(&object) < before_ts && object.size > size_gt;
687
688				eligible
689					.then_async(|| self.delete(&parts))
690					.await
691					.and_then(Result::ok)
692					.map(|()| mxc)
693			})
694			.collect()
695			.await;
696
697		Ok(deleted)
698	}
699
700	/// The MXCs of every local user's profile avatar and every room's avatar,
701	/// the spare-set honoured by `keep_profiles`.
702	async fn avatar_mxcs(&self) -> HashSet<OwnedMxcUri> {
703		let user_avatars = self
704			.services
705			.users
706			.list_local_users()
707			.map(ToOwned::to_owned)
708			.broad_filter_map(async |user| self.services.profile.avatar_url(&user).await.ok());
709
710		let room_avatars = self
711			.services
712			.metadata
713			.iter_ids()
714			.map(ToOwned::to_owned)
715			.broad_filter_map(async |room_id| {
716				self.services
717					.state_accessor
718					.get_avatar(&room_id)
719					.await
720					.ok()
721					.and_then(|avatar| avatar.url)
722			});
723
724		user_avatars.chain(room_avatars).collect().await
725	}
726
727	fn is_local(&self, mxc: &OwnedMxcUri) -> bool {
728		mxc.server_name()
729			.is_ok_and(|server| self.services.globals.server_is_ours(server))
730	}
731
732	/// First storage provider's object metadata for the media stored under
733	/// `key` (byte length and modification time), or `None` when no provider
734	/// holds it.
735	async fn head_meta(&self, key: &[u8]) -> Option<ObjectMeta> {
736		let path = self.get_media_name_sha256(key);
737
738		let stream = self
739			.storage_providers()
740			.stream()
741			.filter_map(async |provider| provider.head(&path).await.log_debug_err().ok());
742
743		pin_mut!(stream);
744		stream.next().await
745	}
746
747	/// Deletes all media files before or after the given time. Returns a usize
748	/// with the number of media files deleted.
749	pub async fn delete_range(
750		&self,
751		time: SystemTime,
752		older_than: bool,
753		newer_than: bool,
754		yes_i_want_to_delete_local_media: bool,
755	) -> Result<usize> {
756		let all_keys = self.db.get_all_media_keys().await;
757		let mut remote_mxcs = Vec::with_capacity(all_keys.len());
758
759		for key in all_keys {
760			trace!("Full MXC key from database: {key:?}");
761			let mut parts = key.split(|&b| b == 0xFF);
762			let mxc = parts
763				.next()
764				.map(|bytes| {
765					utils::string_from_bytes(bytes).map_err(|e| {
766						err!(Database(error!(
767							"Failed to parse MXC unicode bytes from our database: {e}"
768						)))
769					})
770				})
771				.transpose()?;
772
773			let Some(mxc_s) = mxc else {
774				debug_warn!(
775					?mxc,
776					"Parsed MXC URL unicode bytes from database but is still invalid"
777				);
778				continue;
779			};
780
781			trace!("Parsed MXC key to URL: {mxc_s}");
782			let mxc = OwnedMxcUri::from(mxc_s);
783			if (mxc.server_name() == Ok(self.services.globals.server_name())
784				&& !yes_i_want_to_delete_local_media)
785				|| !mxc.is_valid()
786			{
787				debug!("Ignoring local or broken media MXC: {mxc}");
788				continue;
789			}
790
791			let file_created_at = if let Some(file_metadata) = self
792				.storage_providers()
793				.stream()
794				.filter_map(async |provider| {
795					let path = self.get_media_name_sha256(&key);
796					match provider.head(&path).await {
797						| Ok(file_metadata) => {
798							trace!(%mxc, ?path, "Provider file metadata: {file_metadata:?}");
799							Some(file_metadata)
800						},
801						| Err(e) => {
802							debug_warn!(
803								"Failed to obtain {:?} file metadata for MXC {mxc} at file path \
804								 {path:?}\", skipping: {e}",
805								provider.name,
806							);
807							None
808						},
809					}
810				})
811				.boxed()
812				.next()
813				.await
814			{
815				SystemTime::from(file_metadata.last_modified)
816			} else {
817				continue;
818			};
819
820			debug!("File created at: {file_created_at:?}");
821
822			if file_created_at <= time && older_than {
823				debug!(
824					"File is older than user duration, pushing to list of file paths and keys \
825					 to delete."
826				);
827				remote_mxcs.push(mxc.to_string());
828			} else if file_created_at >= time && newer_than {
829				debug!(
830					"File is newer than user duration, pushing to list of file paths and keys \
831					 to delete."
832				);
833				remote_mxcs.push(mxc.to_string());
834			}
835		}
836
837		debug_info!("Deleting media now in the past {time:?}");
838
839		let mut deletion_count: usize = 0;
840
841		for mxc in remote_mxcs {
842			let Ok(mxc) = mxc.as_str().try_into() else {
843				debug_warn!("Invalid MXC in database, skipping");
844				continue;
845			};
846
847			debug_info!("Deleting MXC {mxc} from database and filesystem");
848
849			match self.delete(&mxc).await {
850				| Ok(()) => {
851					deletion_count = deletion_count.saturating_add(1);
852				},
853				| Err(e) => {
854					warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
855				},
856			}
857		}
858
859		Ok(deletion_count)
860	}
861
862	pub async fn create_media_dir(&self) -> Result {
863		let dir = self.get_media_dir();
864		Ok(fs::create_dir_all(dir).await?)
865	}
866
867	async fn remove_media_file(&self, key: &[u8]) -> Result {
868		let path = self.get_media_name_sha256(key);
869		self.storage_providers()
870			.stream()
871			.filter_map(async |provider| {
872				debug!(
873					?key, ?path, provider = ?provider.name,
874					"Deleting media file from provider",
875				);
876
877				provider
878					.delete_one(&path)
879					.await
880					.log_debug_err()
881					.ok()
882			})
883			.count()
884			.map(|count| {
885				count
886					.ge(&0)
887					.into_option()
888					.ok_or_else(|| err!(Request(NotFound("Failed to remove on any provider."))))
889			})
890			.await
891	}
892
893	async fn create_media_file(&self, key: &[u8], file: &[u8]) -> Result {
894		self.storage_providers()
895			.try_stream()
896			.ready_try_filter(|provider| {
897				let store_media_on_providers = &self.services.config.store_media_on_providers;
898
899				store_media_on_providers.is_empty()
900					|| store_media_on_providers.contains(&provider.name)
901			})
902			.and_then(async |provider| {
903				let path = self.get_media_name_sha256(key);
904				debug!(
905					?key, ?path,
906					len = ?file.len(),
907					provider = ?provider.name,
908					"Creating media file on storage provider."
909				);
910
911				if let Err(e) = provider
912					.put_one(path.as_str(), file.to_vec())
913					.await
914				{
915					return Err!(Database(error!(
916						?path,
917						?provider,
918						"Failed to store media on provider: {e:?}"
919					)));
920				}
921
922				Ok(1)
923			})
924			.ready_try_fold(0_usize, |a, c| Ok(a.saturating_add(c)))
925			.inspect_ok(|&uploads| assert!(uploads > 0, "Successfully saved to nowhere."))
926			.map_ok(|_| ())
927			.await
928	}
929
930	fn storage_providers(&self) -> impl Iterator<Item = &Arc<Provider>> + Send + '_ {
931		let explicit_providers = &self.services.config.media_storage_providers;
932
933		let or_all_providers = explicit_providers
934			.is_empty()
935			.then(|| self.services.storage.providers())
936			.into_iter()
937			.flatten();
938
939		explicit_providers
940			.iter()
941			.filter_map(|id| self.services.storage.provider(id).ok())
942			.chain(or_all_providers)
943	}
944
945	#[inline]
946	pub async fn get_metadata(&self, mxc: &Mxc<'_>) -> Option<Metadata> {
947		self.db
948			.search_file_metadata(mxc, &Dim::default())
949			.await
950			.ok()
951	}
952
953	#[inline]
954	#[must_use]
955	pub fn get_media_path_sha256(&self, key: &[u8]) -> PathBuf {
956		self.get_media_dir()
957			.join(self.get_media_name_sha256(key))
958	}
959
960	/// new SHA256 file name media function. requires database migrated. uses
961	/// SHA256 hash of the base64 key as the file name
962	#[inline]
963	#[must_use]
964	pub fn get_media_name_sha256(&self, key: &[u8]) -> String {
965		// Using the hash of the base64 key as the filename prevents the total
966		// length of the path from exceeding the maximum length in most
967		// filesystems
968		let digest = <sha2::Sha256 as sha2::Digest>::digest(key);
969		encode_key(&digest)
970	}
971
972	/// old base64 file name media function
973	/// This is the old version of `get_media_path_sha256` that uses the full
974	/// base64 key as the filename.
975	#[must_use]
976	pub fn get_media_path_b64(&self, key: &[u8]) -> PathBuf {
977		self.get_media_dir().join(encode_key(key))
978	}
979
980	#[must_use]
981	pub fn get_media_dir(&self) -> PathBuf {
982		self.services
983			.server
984			.config
985			.database_path
986			.join("media")
987	}
988}
989
990#[inline]
991#[must_use]
992pub fn encode_key(key: &[u8]) -> String { general_purpose::URL_SAFE_NO_PAD.encode(key) }
993
994fn mtime_millis(object: &ObjectMeta) -> u64 {
995	u64::try_from(object.last_modified.timestamp_millis()).unwrap_or(0)
996}