1use std::sync::Arc;
16
17use matrix_sdk_common::{
18 SendOutsideWasm, SyncOutsideWasm,
19 executor::{JoinHandle, spawn},
20 locks::Mutex,
21};
22use ruma::time::SystemTime;
23use tokio::sync::Mutex as AsyncMutex;
24use tracing::error;
25
26use super::{MediaRetentionPolicy, MediaStoreInner};
27use crate::media::MediaRequestParameters;
28
29#[derive(Debug)]
34pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
35 inner: Arc<MediaServiceInner<Time>>,
36}
37
38#[derive(Debug)]
39struct MediaServiceInner<Time: TimeProvider = DefaultTimeProvider> {
40 time_provider: Time,
42
43 policy: Mutex<MediaRetentionPolicy>,
45
46 cleanup_guard: AsyncMutex<()>,
48
49 last_media_cleanup_time: Mutex<Option<SystemTime>>,
51
52 automatic_media_cleanup_join_handle: Mutex<Option<JoinHandle<()>>>,
57}
58
59impl MediaService {
60 pub fn new() -> Self {
65 Self::default()
66 }
67}
68
69impl Default for MediaService {
70 fn default() -> Self {
71 Self::with_time_provider(DefaultTimeProvider)
72 }
73}
74
75impl<Time> MediaService<Time>
76where
77 Time: TimeProvider + 'static,
78{
79 fn with_time_provider(time_provider: Time) -> Self {
82 let inner = MediaServiceInner {
83 time_provider,
84 policy: Mutex::new(MediaRetentionPolicy::empty()),
85 cleanup_guard: AsyncMutex::new(()),
86 last_media_cleanup_time: Mutex::new(None),
87 automatic_media_cleanup_join_handle: Mutex::new(None),
88 };
89
90 Self { inner: Arc::new(inner) }
91 }
92
93 pub fn restore(
102 &self,
103 policy: Option<MediaRetentionPolicy>,
104 last_media_cleanup_time: Option<SystemTime>,
105 ) {
106 if let Some(policy) = policy {
107 *self.inner.policy.lock() = policy;
108 }
109
110 if let Some(time) = last_media_cleanup_time {
111 *self.inner.last_media_cleanup_time.lock() = Some(time);
112 }
113 }
114
115 fn now(&self) -> SystemTime {
117 self.inner.time_provider.now()
118 }
119
120 pub async fn set_media_retention_policy<Store: MediaStoreInner + 'static>(
128 &self,
129 store: &Store,
130 policy: MediaRetentionPolicy,
131 ) -> Result<(), Store::Error> {
132 store.set_media_retention_policy_inner(policy).await?;
133
134 *self.inner.policy.lock() = policy;
135
136 self.maybe_spawn_automatic_media_cache_cleanup(store, self.now());
137
138 Ok(())
139 }
140
141 pub fn media_retention_policy(&self) -> MediaRetentionPolicy {
143 *self.inner.policy.lock()
144 }
145
146 pub async fn add_media_content<Store: MediaStoreInner + 'static>(
156 &self,
157 store: &Store,
158 request: &MediaRequestParameters,
159 content: Vec<u8>,
160 ignore_policy: IgnoreMediaRetentionPolicy,
161 ) -> Result<(), Store::Error> {
162 let policy = self.media_retention_policy();
163
164 if ignore_policy == IgnoreMediaRetentionPolicy::No
165 && policy.exceeds_max_file_size(content.len() as u64)
166 {
167 return Ok(());
169 }
170
171 let current_time = self.now();
172 store
173 .add_media_content_inner(request, content, current_time, policy, ignore_policy)
174 .await?;
175
176 self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
177
178 Ok(())
179 }
180
181 pub async fn set_ignore_media_retention_policy<Store: MediaStoreInner>(
193 &self,
194 store: &Store,
195 request: &MediaRequestParameters,
196 ignore_policy: IgnoreMediaRetentionPolicy,
197 ) -> Result<(), Store::Error> {
198 store.set_ignore_media_retention_policy_inner(request, ignore_policy).await
199 }
200
201 pub async fn get_media_content<Store: MediaStoreInner + 'static>(
209 &self,
210 store: &Store,
211 request: &MediaRequestParameters,
212 ) -> Result<Option<Vec<u8>>, Store::Error> {
213 let current_time = self.now();
214 let content = store.get_media_content_inner(request, current_time).await?;
215
216 self.maybe_spawn_automatic_media_cache_cleanup(store, current_time);
217
218 Ok(content)
219 }
220
221 pub async fn clean<Store: MediaStoreInner>(&self, store: &Store) -> Result<(), Store::Error> {
229 self.clean_inner(store, self.now()).await
230 }
231
232 async fn clean_inner<Store: MediaStoreInner>(
233 &self,
234 store: &Store,
235 current_time: SystemTime,
236 ) -> Result<(), Store::Error> {
237 let Ok(_guard) = self.inner.cleanup_guard.try_lock() else {
238 return Ok(());
240 };
241
242 let policy = self.media_retention_policy();
243
244 if !policy.has_limitations() {
245 return Ok(());
247 }
248
249 store.clean_inner(policy, current_time).await?;
250
251 *self.inner.last_media_cleanup_time.lock() = Some(current_time);
252
253 Ok(())
254 }
255
256 fn maybe_spawn_automatic_media_cache_cleanup<Store: MediaStoreInner + 'static>(
265 &self,
266 store: &Store,
267 current_time: SystemTime,
268 ) {
269 let mut join_handle = self.inner.automatic_media_cleanup_join_handle.lock();
270
271 if join_handle.as_ref().is_some_and(|join_handle| !join_handle.is_finished()) {
272 return;
274 }
275
276 let policy = self.media_retention_policy();
277 if policy.cleanup_frequency.is_none() || !policy.has_limitations() {
278 return;
280 }
281
282 let last_media_cleanup_time = *self.inner.last_media_cleanup_time.lock();
283 if last_media_cleanup_time.is_some_and(|last_cleanup_time| {
284 !policy.should_clean_up(current_time, last_cleanup_time)
285 }) {
286 return;
288 }
289
290 let this = self.clone();
291 let store = store.clone();
292
293 let handle = spawn(async move {
294 if let Err(error) = this.clean_inner(&store, current_time).await {
295 error!("Failed to run automatic media cache cleanup: {error}");
296 }
297 });
298
299 *join_handle = Some(handle);
300 }
301}
302
303impl<Time> Clone for MediaService<Time>
304where
305 Time: TimeProvider,
306{
307 fn clone(&self) -> Self {
308 Self { inner: self.inner.clone() }
309 }
310}
311
312impl<Time> Drop for MediaServiceInner<Time>
313where
314 Time: TimeProvider,
315{
316 fn drop(&mut self) {
317 if let Some(join_handle) = self.automatic_media_cleanup_join_handle.lock().take() {
318 join_handle.abort();
319 }
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub enum IgnoreMediaRetentionPolicy {
335 Yes,
345
346 No,
349}
350
351impl IgnoreMediaRetentionPolicy {
352 pub fn is_yes(self) -> bool {
354 matches!(self, Self::Yes)
355 }
356}
357
358pub trait TimeProvider: SendOutsideWasm + SyncOutsideWasm {
361 fn now(&self) -> SystemTime;
363}
364
365#[derive(Debug)]
367pub struct DefaultTimeProvider;
368
369impl TimeProvider for DefaultTimeProvider {
370 fn now(&self) -> SystemTime {
371 SystemTime::now()
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use std::{
378 fmt,
379 sync::{Arc, MutexGuard},
380 };
381
382 use async_trait::async_trait;
383 use matrix_sdk_common::locks::Mutex;
384 use matrix_sdk_test::async_test;
385 use ruma::{
386 OwnedMxcUri,
387 events::room::MediaSource,
388 mxc_uri,
389 time::{Duration, SystemTime},
390 };
391
392 use super::{
393 IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStoreInner,
394 TimeProvider,
395 };
396 use crate::media::{MediaFormat, MediaRequestParameters, UniqueKey, store::MediaStoreError};
397
398 #[derive(Debug, Default, Clone)]
399 struct MockMediaStoreInner {
400 inner: Arc<Mutex<MockMediaStoreInnerInner>>,
401 }
402
403 impl MockMediaStoreInner {
404 fn accessed(&self) -> bool {
406 self.inner.lock().accessed
407 }
408
409 fn reset_accessed(&self) {
411 self.inner.lock().accessed = false;
412 }
413
414 fn inner(&self) -> MutexGuard<'_, MockMediaStoreInnerInner> {
419 let mut inner = self.inner.lock();
420 inner.accessed = true;
421 inner
422 }
423 }
424
425 #[derive(Debug, Default)]
426 struct MockMediaStoreInnerInner {
427 accessed: bool,
431
432 media_retention_policy: Option<MediaRetentionPolicy>,
434
435 media_list: Vec<MediaContent>,
437
438 cleanup_time: Option<SystemTime>,
440 }
441
442 #[derive(Debug, Clone)]
443 struct MediaContent {
444 key: String,
446
447 uri: OwnedMxcUri,
449
450 content: Vec<u8>,
452
453 ignore_policy: bool,
456
457 last_access: SystemTime,
459 }
460
461 #[derive(Debug)]
462 struct MockMediaStoreInnerError;
463
464 impl fmt::Display for MockMediaStoreInnerError {
465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466 write!(f, "MockMediaStoreInnerError")
467 }
468 }
469
470 impl std::error::Error for MockMediaStoreInnerError {}
471
472 impl From<MockMediaStoreInnerError> for MediaStoreError {
473 fn from(value: MockMediaStoreInnerError) -> Self {
474 Self::backend(value)
475 }
476 }
477
478 #[cfg_attr(target_family = "wasm", async_trait(?Send))]
479 #[cfg_attr(not(target_family = "wasm"), async_trait)]
480 impl MediaStoreInner for MockMediaStoreInner {
481 type Error = MockMediaStoreInnerError;
482
483 async fn media_retention_policy_inner(
484 &self,
485 ) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
486 Ok(self.inner().media_retention_policy)
487 }
488
489 async fn set_media_retention_policy_inner(
490 &self,
491 policy: MediaRetentionPolicy,
492 ) -> Result<(), Self::Error> {
493 self.inner().media_retention_policy = Some(policy);
494 Ok(())
495 }
496
497 async fn add_media_content_inner(
498 &self,
499 request: &MediaRequestParameters,
500 content: Vec<u8>,
501 current_time: SystemTime,
502 policy: MediaRetentionPolicy,
503 ignore_policy: IgnoreMediaRetentionPolicy,
504 ) -> Result<(), Self::Error> {
505 let ignore_policy = ignore_policy.is_yes();
506
507 if !ignore_policy && policy.exceeds_max_file_size(content.len() as u64) {
508 return Ok(());
509 }
510
511 let mut inner = self.inner();
512 let key = request.unique_key();
513
514 if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
515 let media_content = &mut inner.media_list[pos];
516 media_content.content = content;
517 media_content.last_access = current_time;
518 media_content.ignore_policy = ignore_policy;
519 } else {
520 inner.media_list.push(MediaContent {
521 key,
522 uri: request.uri().to_owned(),
523 content,
524 ignore_policy,
525 last_access: current_time,
526 });
527 }
528
529 Ok(())
530 }
531
532 async fn set_ignore_media_retention_policy_inner(
533 &self,
534 request: &MediaRequestParameters,
535 ignore_policy: IgnoreMediaRetentionPolicy,
536 ) -> Result<(), Self::Error> {
537 let key = request.unique_key();
538 let mut inner = self.inner();
539
540 if let Some(pos) = inner.media_list.iter().position(|content| content.key == key) {
541 inner.media_list[pos].ignore_policy = ignore_policy.is_yes();
542 }
543
544 Ok(())
545 }
546
547 async fn get_media_content_inner(
548 &self,
549 request: &MediaRequestParameters,
550 current_time: SystemTime,
551 ) -> Result<Option<Vec<u8>>, Self::Error> {
552 let key = request.unique_key();
553 let mut inner = self.inner();
554
555 let Some(media_content) =
556 inner.media_list.iter_mut().find(|content| content.key == key)
557 else {
558 return Ok(None);
559 };
560
561 media_content.last_access = current_time;
562
563 Ok(Some(media_content.content.clone()))
564 }
565
566 async fn clean_inner(
567 &self,
568 _policy: MediaRetentionPolicy,
569 current_time: SystemTime,
570 ) -> Result<(), Self::Error> {
571 self.inner().cleanup_time = Some(current_time);
575
576 Ok(())
577 }
578
579 async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
580 Ok(self.inner().cleanup_time)
581 }
582 }
583
584 #[derive(Debug)]
585 struct MockTimeProvider {
586 now: Mutex<SystemTime>,
587 }
588
589 impl MockTimeProvider {
590 fn new(now: SystemTime) -> Self {
592 Self { now: Mutex::new(now) }
593 }
594
595 fn set_now(&self, now: SystemTime) {
597 *self.now.lock() = now;
598 }
599 }
600
601 impl TimeProvider for MockTimeProvider {
602 fn now(&self) -> SystemTime {
603 *self.now.lock()
604 }
605 }
606
607 #[async_test]
608 async fn test_media_service_empty_policy() {
609 let content = b"some text content";
610 let uri = mxc_uri!("mxc://server.local/AbcDe1234");
611 let request = MediaRequestParameters {
612 source: MediaSource::Plain(uri.to_owned()),
613 format: MediaFormat::File,
614 };
615
616 let now = SystemTime::UNIX_EPOCH;
617
618 let store = MockMediaStoreInner::default();
619 let service = MediaService::with_time_provider(MockTimeProvider::new(now));
620
621 assert!(!service.media_retention_policy().has_limitations());
623 service.restore(None, None);
624 assert!(!service.media_retention_policy().has_limitations());
625 assert!(!store.accessed());
626
627 service
629 .add_media_content(&store, &request, content.to_vec(), IgnoreMediaRetentionPolicy::No)
630 .await
631 .unwrap();
632 assert!(store.accessed());
633
634 let media_content = store.inner().media_list[0].clone();
635 assert_eq!(media_content.uri, uri);
636 assert_eq!(media_content.content, content);
637 assert!(!media_content.ignore_policy);
638 assert_eq!(media_content.last_access, now);
639
640 let now = now + Duration::from_secs(60);
641 service.inner.time_provider.set_now(now);
642 store.reset_accessed();
643
644 let loaded_content = service.get_media_content(&store, &request).await.unwrap();
646 assert!(store.accessed());
647 assert_eq!(loaded_content.as_deref(), Some(content.as_slice()));
648
649 let media = store.inner().media_list[0].clone();
651 assert_eq!(media.last_access, now);
652
653 let now = now + Duration::from_secs(60);
654 service.inner.time_provider.set_now(now);
655 store.reset_accessed();
656
657 service
659 .set_ignore_media_retention_policy(&store, &request, IgnoreMediaRetentionPolicy::Yes)
660 .await
661 .unwrap();
662 assert!(store.accessed());
663
664 let media_content = store.inner().media_list[0].clone();
665 assert!(media_content.ignore_policy);
666
667 assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
670 store.reset_accessed();
671
672 service.clean(&store).await.unwrap();
673 assert!(!store.accessed());
674 assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
675 }
676
677 #[async_test]
678 async fn test_media_service_non_empty_policy() {
679 let small_content = b"some text content";
681 let small_uri = mxc_uri!("mxc://server.local/small");
682 let small_request = MediaRequestParameters {
683 source: MediaSource::Plain(small_uri.to_owned()),
684 format: MediaFormat::File,
685 };
686
687 let big_content = b"some much much larger text content";
689 let big_uri = mxc_uri!("mxc://server.local/big");
690 let big_request = MediaRequestParameters {
691 source: MediaSource::Plain(big_uri.to_owned()),
692 format: MediaFormat::File,
693 };
694
695 let policy = MediaRetentionPolicy { max_file_size: Some(32), ..Default::default() };
697
698 let now = SystemTime::UNIX_EPOCH;
699
700 let store = MockMediaStoreInner::default();
701 let service = MediaService::with_time_provider(MockTimeProvider::new(now));
702
703 service.restore(Some(MediaRetentionPolicy::default()), None);
705 assert_eq!(service.media_retention_policy(), MediaRetentionPolicy::default());
706 assert!(!store.accessed());
707
708 service.set_media_retention_policy(&store, policy).await.unwrap();
710 assert!(store.accessed());
711 assert_eq!(service.media_retention_policy(), policy);
712 assert_eq!(store.inner().media_retention_policy, Some(policy));
713
714 store.reset_accessed();
715
716 service
719 .add_media_content(
720 &store,
721 &small_request,
722 small_content.to_vec(),
723 IgnoreMediaRetentionPolicy::No,
724 )
725 .await
726 .unwrap();
727 assert!(store.accessed());
728
729 let media_content = store.inner().media_list[0].clone();
730 assert_eq!(media_content.uri, small_uri);
731 assert_eq!(media_content.content, small_content);
732 assert!(!media_content.ignore_policy);
733 assert_eq!(media_content.last_access, now);
734
735 let now = now + Duration::from_secs(60);
736 service.inner.time_provider.set_now(now);
737 store.reset_accessed();
738
739 let loaded_content = service.get_media_content(&store, &small_request).await.unwrap();
741 assert!(store.accessed());
742 assert_eq!(loaded_content.as_deref(), Some(small_content.as_slice()));
743
744 let media = store.inner().media_list[0].clone();
746 assert_eq!(media.last_access, now);
747
748 let now = now + Duration::from_secs(60);
749 service.inner.time_provider.set_now(now);
750 store.reset_accessed();
751
752 let now = now + Duration::from_secs(60);
753 service.inner.time_provider.set_now(now);
754 store.reset_accessed();
755
756 service
759 .add_media_content(
760 &store,
761 &big_request,
762 big_content.to_vec(),
763 IgnoreMediaRetentionPolicy::No,
764 )
765 .await
766 .unwrap();
767 assert!(!store.accessed());
768 assert_eq!(store.inner().media_list.len(), 1);
769
770 store.reset_accessed();
771
772 let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
773 assert!(store.accessed());
774 assert_eq!(loaded_content, None);
775
776 store.reset_accessed();
777
778 service
780 .add_media_content(
781 &store,
782 &big_request,
783 big_content.to_vec(),
784 IgnoreMediaRetentionPolicy::Yes,
785 )
786 .await
787 .unwrap();
788 assert!(store.accessed());
789 assert_eq!(store.inner().media_list.len(), 2);
790
791 store.reset_accessed();
792
793 let loaded_content = service.get_media_content(&store, &big_request).await.unwrap();
795 assert!(store.accessed());
796 assert_eq!(loaded_content.as_deref(), Some(big_content.as_slice()));
797
798 let media = store.inner().media_list[1].clone();
800 assert_eq!(media.last_access, now);
801
802 assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
804
805 let now = now + Duration::from_secs(60);
806 service.inner.time_provider.set_now(now);
807 store.reset_accessed();
808
809 service.clean(&store).await.unwrap();
810 assert!(store.accessed());
811 assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
812 }
813
814 #[async_test]
815 async fn test_media_service_automatic_cleanup() {
816 let content = vec![0; 64];
818
819 let uri_1 = mxc_uri!("mxc://localhost/media-1");
820 let request_1 = MediaRequestParameters {
821 source: MediaSource::Plain(uri_1.to_owned()),
822 format: MediaFormat::File,
823 };
824 let uri_2 = mxc_uri!("mxc://localhost/media-2");
825 let request_2 = MediaRequestParameters {
826 source: MediaSource::Plain(uri_2.to_owned()),
827 format: MediaFormat::File,
828 };
829
830 let now = SystemTime::UNIX_EPOCH;
831
832 let store = MockMediaStoreInner::default();
833 let service = MediaService::with_time_provider(MockTimeProvider::new(now));
834
835 let policy = MediaRetentionPolicy::empty();
837 service.set_media_retention_policy(&store, policy).await.unwrap();
838
839 service
841 .add_media_content(&store, &request_1, content.clone(), IgnoreMediaRetentionPolicy::No)
842 .await
843 .unwrap();
844 service
845 .add_media_content(&store, &request_2, content, IgnoreMediaRetentionPolicy::No)
846 .await
847 .unwrap();
848 assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
849
850 let now = now + Duration::from_secs(60);
852 service.inner.time_provider.set_now(now);
853 service.maybe_spawn_automatic_media_cache_cleanup(&store, now);
854
855 assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
857
858 let policy = MediaRetentionPolicy::empty()
860 .with_cleanup_frequency(Some(Duration::from_secs(60 * 60)));
861 let now = now + Duration::from_secs(60);
862 service.inner.time_provider.set_now(now);
863 service.set_media_retention_policy(&store, policy).await.unwrap();
864
865 assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
867
868 let policy = MediaRetentionPolicy::empty()
870 .with_cleanup_frequency(Some(Duration::from_secs(60 * 60)))
871 .with_max_file_size(Some(512));
872 let now = now + Duration::from_secs(60);
873 service.inner.time_provider.set_now(now);
874 service.set_media_retention_policy(&store, policy).await.unwrap();
875
876 let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
878 join_handle.await.unwrap();
879
880 assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
881
882 let now = now + Duration::from_secs(60);
885 service.inner.time_provider.set_now(now);
886 service.get_media_content(&store, &request_1).await.unwrap();
887
888 assert!(service.inner.automatic_media_cleanup_join_handle.lock().is_none());
889
890 let now = now + Duration::from_secs(2 * 60 * 60);
892 service.inner.time_provider.set_now(now);
893 service.get_media_content(&store, &request_1).await.unwrap();
894
895 let join_handle = service.inner.automatic_media_cleanup_join_handle.lock().take().unwrap();
896 join_handle.await.unwrap();
897
898 assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
899 }
900}