matrix_sdk_base/media/store/
traits.rs

1// Copyright 2025 Kévin Commaille
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types and traits regarding media caching of the media store.
16
17use std::{fmt, sync::Arc};
18
19use async_trait::async_trait;
20use matrix_sdk_common::AsyncTraitDeps;
21use ruma::{MxcUri, time::SystemTime};
22
23#[cfg(doc)]
24use crate::media::store::MediaService;
25use crate::media::{
26    MediaRequestParameters,
27    store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaStoreError},
28};
29
30/// An abstract trait that can be used to implement different store backends
31/// for the media of the SDK.
32#[cfg_attr(target_family = "wasm", async_trait(?Send))]
33#[cfg_attr(not(target_family = "wasm"), async_trait)]
34pub trait MediaStore: AsyncTraitDeps {
35    /// The error type used by this media store.
36    type Error: fmt::Debug + Into<MediaStoreError>;
37
38    /// Try to take a lock using the given store.
39    async fn try_take_leased_lock(
40        &self,
41        lease_duration_ms: u32,
42        key: &str,
43        holder: &str,
44    ) -> Result<bool, Self::Error>;
45
46    /// Add a media file's content in the media store.
47    ///
48    /// # Arguments
49    ///
50    /// * `request` - The `MediaRequest` of the file.
51    ///
52    /// * `content` - The content of the file.
53    async fn add_media_content(
54        &self,
55        request: &MediaRequestParameters,
56        content: Vec<u8>,
57        ignore_policy: IgnoreMediaRetentionPolicy,
58    ) -> Result<(), Self::Error>;
59
60    /// Replaces the given media's content key with another one.
61    ///
62    /// This should be used whenever a temporary (local) MXID has been used, and
63    /// it must now be replaced with its actual remote counterpart (after
64    /// uploading some content, or creating an empty MXC URI).
65    ///
66    /// ⚠ No check is performed to ensure that the media formats are consistent,
67    /// i.e. it's possible to update with a thumbnail key a media that was
68    /// keyed as a file before. The caller is responsible of ensuring that
69    /// the replacement makes sense, according to their use case.
70    ///
71    /// This should not raise an error when the `from` parameter points to an
72    /// unknown media, and it should silently continue in this case.
73    ///
74    /// # Arguments
75    ///
76    /// * `from` - The previous `MediaRequest` of the file.
77    ///
78    /// * `to` - The new `MediaRequest` of the file.
79    async fn replace_media_key(
80        &self,
81        from: &MediaRequestParameters,
82        to: &MediaRequestParameters,
83    ) -> Result<(), Self::Error>;
84
85    /// Get a media file's content out of the media store.
86    ///
87    /// # Arguments
88    ///
89    /// * `request` - The `MediaRequest` of the file.
90    async fn get_media_content(
91        &self,
92        request: &MediaRequestParameters,
93    ) -> Result<Option<Vec<u8>>, Self::Error>;
94
95    /// Remove a media file's content from the media store.
96    ///
97    /// # Arguments
98    ///
99    /// * `request` - The `MediaRequest` of the file.
100    async fn remove_media_content(
101        &self,
102        request: &MediaRequestParameters,
103    ) -> Result<(), Self::Error>;
104
105    /// Get a media file's content associated to an `MxcUri` from the
106    /// media store.
107    ///
108    /// In theory, there could be several files stored using the same URI and a
109    /// different `MediaFormat`. This API is meant to be used with a media file
110    /// that has only been stored with a single format.
111    ///
112    /// If there are several media files for a given URI in different formats,
113    /// this API will only return one of them. Which one is left as an
114    /// implementation detail.
115    ///
116    /// # Arguments
117    ///
118    /// * `uri` - The `MxcUri` of the media file.
119    async fn get_media_content_for_uri(&self, uri: &MxcUri)
120    -> Result<Option<Vec<u8>>, Self::Error>;
121
122    /// Remove all the media files' content associated to an `MxcUri` from the
123    /// media store.
124    ///
125    /// This should not raise an error when the `uri` parameter points to an
126    /// unknown media, and it should return an Ok result in this case.
127    ///
128    /// # Arguments
129    ///
130    /// * `uri` - The `MxcUri` of the media files.
131    async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
132
133    /// Set the `MediaRetentionPolicy` to use for deciding whether to store or
134    /// keep media content.
135    ///
136    /// # Arguments
137    ///
138    /// * `policy` - The `MediaRetentionPolicy` to use.
139    async fn set_media_retention_policy(
140        &self,
141        policy: MediaRetentionPolicy,
142    ) -> Result<(), Self::Error>;
143
144    /// Get the current `MediaRetentionPolicy`.
145    fn media_retention_policy(&self) -> MediaRetentionPolicy;
146
147    /// Set whether the current [`MediaRetentionPolicy`] should be ignored for
148    /// the media.
149    ///
150    /// The change will be taken into account in the next cleanup.
151    ///
152    /// # Arguments
153    ///
154    /// * `request` - The `MediaRequestParameters` of the file.
155    ///
156    /// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
157    ///   ignored.
158    async fn set_ignore_media_retention_policy(
159        &self,
160        request: &MediaRequestParameters,
161        ignore_policy: IgnoreMediaRetentionPolicy,
162    ) -> Result<(), Self::Error>;
163
164    /// Clean up the media cache with the current `MediaRetentionPolicy`.
165    ///
166    /// If there is already an ongoing cleanup, this is a noop.
167    async fn clean(&self) -> Result<(), Self::Error>;
168}
169
170/// An abstract trait that can be used to implement different store backends
171/// for the media cache of the SDK.
172///
173/// The main purposes of this trait are to be able to centralize where we handle
174/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
175/// simplify the implementation of tests by being able to have complete control
176/// over the `SystemTime`s provided to the store.
177#[cfg_attr(target_family = "wasm", async_trait(?Send))]
178#[cfg_attr(not(target_family = "wasm"), async_trait)]
179pub trait MediaStoreInner: AsyncTraitDeps + Clone {
180    /// The error type used by this media cache store.
181    type Error: fmt::Debug + fmt::Display + Into<MediaStoreError>;
182
183    /// The persisted media retention policy in the media cache.
184    async fn media_retention_policy_inner(
185        &self,
186    ) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
187
188    /// Persist the media retention policy in the media cache.
189    ///
190    /// # Arguments
191    ///
192    /// * `policy` - The `MediaRetentionPolicy` to persist.
193    async fn set_media_retention_policy_inner(
194        &self,
195        policy: MediaRetentionPolicy,
196    ) -> Result<(), Self::Error>;
197
198    /// Add a media file's content in the media cache.
199    ///
200    /// # Arguments
201    ///
202    /// * `request` - The `MediaRequestParameters` of the file.
203    ///
204    /// * `content` - The content of the file.
205    ///
206    /// * `current_time` - The current time, to set the last access time of the
207    ///   media.
208    ///
209    /// * `policy` - The media retention policy, to check whether the media is
210    ///   too big to be cached.
211    ///
212    /// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
213    ///   for this media. This setting should be persisted alongside the media
214    ///   and taken into account whenever the policy is used.
215    async fn add_media_content_inner(
216        &self,
217        request: &MediaRequestParameters,
218        content: Vec<u8>,
219        current_time: SystemTime,
220        policy: MediaRetentionPolicy,
221        ignore_policy: IgnoreMediaRetentionPolicy,
222    ) -> Result<(), Self::Error>;
223
224    /// Set whether the current [`MediaRetentionPolicy`] should be ignored for
225    /// the media.
226    ///
227    /// If the media of the given request is not found, this should be a noop.
228    ///
229    /// The change will be taken into account in the next cleanup.
230    ///
231    /// # Arguments
232    ///
233    /// * `request` - The `MediaRequestParameters` of the file.
234    ///
235    /// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
236    ///   ignored.
237    async fn set_ignore_media_retention_policy_inner(
238        &self,
239        request: &MediaRequestParameters,
240        ignore_policy: IgnoreMediaRetentionPolicy,
241    ) -> Result<(), Self::Error>;
242
243    /// Get a media file's content out of the media cache.
244    ///
245    /// # Arguments
246    ///
247    /// * `request` - The `MediaRequestParameters` of the file.
248    ///
249    /// * `current_time` - The current time, to update the last access time of
250    ///   the media.
251    async fn get_media_content_inner(
252        &self,
253        request: &MediaRequestParameters,
254        current_time: SystemTime,
255    ) -> Result<Option<Vec<u8>>, Self::Error>;
256
257    /// Get a media file's content associated to an `MxcUri` from the
258    /// media store.
259    ///
260    /// # Arguments
261    ///
262    /// * `uri` - The `MxcUri` of the media file.
263    ///
264    /// * `current_time` - The current time, to update the last access time of
265    ///   the media.
266    async fn get_media_content_for_uri_inner(
267        &self,
268        uri: &MxcUri,
269        current_time: SystemTime,
270    ) -> Result<Option<Vec<u8>>, Self::Error>;
271
272    /// Clean up the media cache with the given policy.
273    ///
274    /// For the integration tests, it is expected that content that does not
275    /// pass the last access expiry and max file size criteria will be
276    /// removed first. After that, the remaining cache size should be
277    /// computed to compare against the max cache size criteria.
278    ///
279    /// # Arguments
280    ///
281    /// * `policy` - The media retention policy to use for the cleanup. The
282    ///   `cleanup_frequency` will be ignored.
283    ///
284    /// * `current_time` - The current time, to be used to check for expired
285    ///   content and to be stored as the time of the last media cache cleanup.
286    async fn clean_inner(
287        &self,
288        policy: MediaRetentionPolicy,
289        current_time: SystemTime,
290    ) -> Result<(), Self::Error>;
291
292    /// The time of the last media cache cleanup.
293    async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
294}
295
296#[repr(transparent)]
297struct EraseMediaStoreError<T>(T);
298
299#[cfg(not(tarpaulin_include))]
300impl<T: fmt::Debug> fmt::Debug for EraseMediaStoreError<T> {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        self.0.fmt(f)
303    }
304}
305
306#[cfg_attr(target_family = "wasm", async_trait(?Send))]
307#[cfg_attr(not(target_family = "wasm"), async_trait)]
308impl<T: MediaStore> MediaStore for EraseMediaStoreError<T> {
309    type Error = MediaStoreError;
310
311    async fn try_take_leased_lock(
312        &self,
313        lease_duration_ms: u32,
314        key: &str,
315        holder: &str,
316    ) -> Result<bool, Self::Error> {
317        self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
318    }
319
320    async fn add_media_content(
321        &self,
322        request: &MediaRequestParameters,
323        content: Vec<u8>,
324        ignore_policy: IgnoreMediaRetentionPolicy,
325    ) -> Result<(), Self::Error> {
326        self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
327    }
328
329    async fn replace_media_key(
330        &self,
331        from: &MediaRequestParameters,
332        to: &MediaRequestParameters,
333    ) -> Result<(), Self::Error> {
334        self.0.replace_media_key(from, to).await.map_err(Into::into)
335    }
336
337    async fn get_media_content(
338        &self,
339        request: &MediaRequestParameters,
340    ) -> Result<Option<Vec<u8>>, Self::Error> {
341        self.0.get_media_content(request).await.map_err(Into::into)
342    }
343
344    async fn remove_media_content(
345        &self,
346        request: &MediaRequestParameters,
347    ) -> Result<(), Self::Error> {
348        self.0.remove_media_content(request).await.map_err(Into::into)
349    }
350
351    async fn get_media_content_for_uri(
352        &self,
353        uri: &MxcUri,
354    ) -> Result<Option<Vec<u8>>, Self::Error> {
355        self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
356    }
357
358    async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
359        self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
360    }
361
362    async fn set_media_retention_policy(
363        &self,
364        policy: MediaRetentionPolicy,
365    ) -> Result<(), Self::Error> {
366        self.0.set_media_retention_policy(policy).await.map_err(Into::into)
367    }
368
369    fn media_retention_policy(&self) -> MediaRetentionPolicy {
370        self.0.media_retention_policy()
371    }
372
373    async fn set_ignore_media_retention_policy(
374        &self,
375        request: &MediaRequestParameters,
376        ignore_policy: IgnoreMediaRetentionPolicy,
377    ) -> Result<(), Self::Error> {
378        self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
379    }
380
381    async fn clean(&self) -> Result<(), Self::Error> {
382        self.0.clean().await.map_err(Into::into)
383    }
384}
385
386/// A type-erased [`MediaStore`].
387pub type DynMediaStore = dyn MediaStore<Error = MediaStoreError>;
388
389/// A type that can be type-erased into `Arc<dyn MediaStore>`.
390///
391/// This trait is not meant to be implemented directly outside
392/// `matrix-sdk-base`, but it is automatically implemented for everything that
393/// implements `MediaStore`.
394pub trait IntoMediaStore {
395    #[doc(hidden)]
396    fn into_media_store(self) -> Arc<DynMediaStore>;
397}
398
399impl IntoMediaStore for Arc<DynMediaStore> {
400    fn into_media_store(self) -> Arc<DynMediaStore> {
401        self
402    }
403}
404
405impl<T> IntoMediaStore for T
406where
407    T: MediaStore + Sized + 'static,
408{
409    fn into_media_store(self) -> Arc<DynMediaStore> {
410        Arc::new(EraseMediaStoreError(self))
411    }
412}
413
414// Turns a given `Arc<T>` into `Arc<DynMediaStore>` by attaching the
415// `MediaStore` impl vtable of `EraseMediaStoreError<T>`.
416impl<T> IntoMediaStore for Arc<T>
417where
418    T: MediaStore + 'static,
419{
420    fn into_media_store(self) -> Arc<DynMediaStore> {
421        let ptr: *const T = Arc::into_raw(self);
422        let ptr_erased = ptr as *const EraseMediaStoreError<T>;
423        // SAFETY: EraseMediaStoreError is repr(transparent) so T and
424        //         EraseMediaStoreError<T> have the same layout and ABI
425        unsafe { Arc::from_raw(ptr_erased) }
426    }
427}