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#[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#[derive(Clone, Debug)]
70pub struct UploadStat {
71 pub user_id: OwnedUserId,
72 pub media_length: u64,
73 pub created_ts: u64,
74}
75
76struct MXCState {
78 notifiers: Mutex<HashMap<OwnedMxcUri, Arc<Notify>>>,
80 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
96pub const MXC_LENGTH: usize = 32;
98
99pub const CACHE_CONTROL_IMMUTABLE: &str = "private,max-age=31536000,immutable";
101
102pub const CORP_CROSS_ORIGIN: &str = "cross-origin";
104
105const 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 #[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 let rate = f64::from(config.media_rc_create_per_second);
153 let burst = f64::from(config.media_rc_create_burst_count);
154
155 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 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 #[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 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 let key = self.db.create_file_metadata(
256 mxc,
257 user,
258 &Dim::default(),
259 content_disposition,
260 content_type,
261 )?;
262
263 self.create_media_file(&key, file).await
265 }
266
267 #[tracing::instrument(level = "trace", skip(self))]
269 pub async fn delete(&self, mxc: &Mxc<'_>) -> Result {
270 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 #[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 #[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 #[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 #[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 #[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 let _lock = self.federation_mutex.lock(&key).await;
459
460 if self
462 .db
463 .search_file_metadata(mxc, &Dim::default())
464 .await
465 .is_ok()
466 {
467 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 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 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 #[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 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 #[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 #[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 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 #[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 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 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 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 #[inline]
963 #[must_use]
964 pub fn get_media_name_sha256(&self, key: &[u8]) -> String {
965 let digest = <sha2::Sha256 as sha2::Digest>::digest(key);
969 encode_key(&digest)
970 }
971
972 #[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}