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, cross_process_lock::CrossProcessLockGeneration};
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<Option<CrossProcessLockGeneration>, 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    /// Perform database optimizations if any are available, i.e. vacuuming in
170    /// SQLite.
171    ///
172    /// **Warning:** this was added to check if SQLite fragmentation was the
173    /// source of performance issues, **DO NOT use in production**.
174    #[doc(hidden)]
175    async fn optimize(&self) -> Result<(), Self::Error>;
176
177    /// Returns the size of the store in bytes, if known.
178    async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
179}
180
181/// An abstract trait that can be used to implement different store backends
182/// for the media cache of the SDK.
183///
184/// The main purposes of this trait are to be able to centralize where we handle
185/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
186/// simplify the implementation of tests by being able to have complete control
187/// over the `SystemTime`s provided to the store.
188#[cfg_attr(target_family = "wasm", async_trait(?Send))]
189#[cfg_attr(not(target_family = "wasm"), async_trait)]
190pub trait MediaStoreInner: AsyncTraitDeps + Clone {
191    /// The error type used by this media cache store.
192    type Error: fmt::Debug + fmt::Display + Into<MediaStoreError>;
193
194    /// The persisted media retention policy in the media cache.
195    async fn media_retention_policy_inner(
196        &self,
197    ) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
198
199    /// Persist the media retention policy in the media cache.
200    ///
201    /// # Arguments
202    ///
203    /// * `policy` - The `MediaRetentionPolicy` to persist.
204    async fn set_media_retention_policy_inner(
205        &self,
206        policy: MediaRetentionPolicy,
207    ) -> Result<(), Self::Error>;
208
209    /// Add a media file's content in the media cache.
210    ///
211    /// # Arguments
212    ///
213    /// * `request` - The `MediaRequestParameters` of the file.
214    ///
215    /// * `content` - The content of the file.
216    ///
217    /// * `current_time` - The current time, to set the last access time of the
218    ///   media.
219    ///
220    /// * `policy` - The media retention policy, to check whether the media is
221    ///   too big to be cached.
222    ///
223    /// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
224    ///   for this media. This setting should be persisted alongside the media
225    ///   and taken into account whenever the policy is used.
226    async fn add_media_content_inner(
227        &self,
228        request: &MediaRequestParameters,
229        content: Vec<u8>,
230        current_time: SystemTime,
231        policy: MediaRetentionPolicy,
232        ignore_policy: IgnoreMediaRetentionPolicy,
233    ) -> Result<(), Self::Error>;
234
235    /// Set whether the current [`MediaRetentionPolicy`] should be ignored for
236    /// the media.
237    ///
238    /// If the media of the given request is not found, this should be a noop.
239    ///
240    /// The change will be taken into account in the next cleanup.
241    ///
242    /// # Arguments
243    ///
244    /// * `request` - The `MediaRequestParameters` of the file.
245    ///
246    /// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
247    ///   ignored.
248    async fn set_ignore_media_retention_policy_inner(
249        &self,
250        request: &MediaRequestParameters,
251        ignore_policy: IgnoreMediaRetentionPolicy,
252    ) -> Result<(), Self::Error>;
253
254    /// Get a media file's content out of the media cache.
255    ///
256    /// # Arguments
257    ///
258    /// * `request` - The `MediaRequestParameters` of the file.
259    ///
260    /// * `current_time` - The current time, to update the last access time of
261    ///   the media.
262    async fn get_media_content_inner(
263        &self,
264        request: &MediaRequestParameters,
265        current_time: SystemTime,
266    ) -> Result<Option<Vec<u8>>, Self::Error>;
267
268    /// Get a media file's content associated to an `MxcUri` from the
269    /// media store.
270    ///
271    /// # Arguments
272    ///
273    /// * `uri` - The `MxcUri` of the media file.
274    ///
275    /// * `current_time` - The current time, to update the last access time of
276    ///   the media.
277    async fn get_media_content_for_uri_inner(
278        &self,
279        uri: &MxcUri,
280        current_time: SystemTime,
281    ) -> Result<Option<Vec<u8>>, Self::Error>;
282
283    /// Clean up the media cache with the given policy.
284    ///
285    /// For the integration tests, it is expected that content that does not
286    /// pass the last access expiry and max file size criteria will be
287    /// removed first. After that, the remaining cache size should be
288    /// computed to compare against the max cache size criteria.
289    ///
290    /// # Arguments
291    ///
292    /// * `policy` - The media retention policy to use for the cleanup. The
293    ///   `cleanup_frequency` will be ignored.
294    ///
295    /// * `current_time` - The current time, to be used to check for expired
296    ///   content and to be stored as the time of the last media cache cleanup.
297    async fn clean_inner(
298        &self,
299        policy: MediaRetentionPolicy,
300        current_time: SystemTime,
301    ) -> Result<(), Self::Error>;
302
303    /// The time of the last media cache cleanup.
304    async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
305}
306
307#[repr(transparent)]
308struct EraseMediaStoreError<T>(T);
309
310#[cfg(not(tarpaulin_include))]
311impl<T: fmt::Debug> fmt::Debug for EraseMediaStoreError<T> {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        self.0.fmt(f)
314    }
315}
316
317#[cfg_attr(target_family = "wasm", async_trait(?Send))]
318#[cfg_attr(not(target_family = "wasm"), async_trait)]
319impl<T: MediaStore> MediaStore for EraseMediaStoreError<T> {
320    type Error = MediaStoreError;
321
322    async fn try_take_leased_lock(
323        &self,
324        lease_duration_ms: u32,
325        key: &str,
326        holder: &str,
327    ) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
328        self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
329    }
330
331    async fn add_media_content(
332        &self,
333        request: &MediaRequestParameters,
334        content: Vec<u8>,
335        ignore_policy: IgnoreMediaRetentionPolicy,
336    ) -> Result<(), Self::Error> {
337        self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
338    }
339
340    async fn replace_media_key(
341        &self,
342        from: &MediaRequestParameters,
343        to: &MediaRequestParameters,
344    ) -> Result<(), Self::Error> {
345        self.0.replace_media_key(from, to).await.map_err(Into::into)
346    }
347
348    async fn get_media_content(
349        &self,
350        request: &MediaRequestParameters,
351    ) -> Result<Option<Vec<u8>>, Self::Error> {
352        self.0.get_media_content(request).await.map_err(Into::into)
353    }
354
355    async fn remove_media_content(
356        &self,
357        request: &MediaRequestParameters,
358    ) -> Result<(), Self::Error> {
359        self.0.remove_media_content(request).await.map_err(Into::into)
360    }
361
362    async fn get_media_content_for_uri(
363        &self,
364        uri: &MxcUri,
365    ) -> Result<Option<Vec<u8>>, Self::Error> {
366        self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
367    }
368
369    async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
370        self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
371    }
372
373    async fn set_media_retention_policy(
374        &self,
375        policy: MediaRetentionPolicy,
376    ) -> Result<(), Self::Error> {
377        self.0.set_media_retention_policy(policy).await.map_err(Into::into)
378    }
379
380    fn media_retention_policy(&self) -> MediaRetentionPolicy {
381        self.0.media_retention_policy()
382    }
383
384    async fn set_ignore_media_retention_policy(
385        &self,
386        request: &MediaRequestParameters,
387        ignore_policy: IgnoreMediaRetentionPolicy,
388    ) -> Result<(), Self::Error> {
389        self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
390    }
391
392    async fn clean(&self) -> Result<(), Self::Error> {
393        self.0.clean().await.map_err(Into::into)
394    }
395
396    async fn optimize(&self) -> Result<(), Self::Error> {
397        self.0.optimize().await.map_err(Into::into)
398    }
399
400    async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
401        self.0.get_size().await.map_err(Into::into)
402    }
403}
404
405/// A type-erased [`MediaStore`].
406pub type DynMediaStore = dyn MediaStore<Error = MediaStoreError>;
407
408/// A type that can be type-erased into `Arc<dyn MediaStore>`.
409///
410/// This trait is not meant to be implemented directly outside
411/// `matrix-sdk-base`, but it is automatically implemented for everything that
412/// implements `MediaStore`.
413pub trait IntoMediaStore {
414    #[doc(hidden)]
415    fn into_media_store(self) -> Arc<DynMediaStore>;
416}
417
418impl IntoMediaStore for Arc<DynMediaStore> {
419    fn into_media_store(self) -> Arc<DynMediaStore> {
420        self
421    }
422}
423
424impl<T> IntoMediaStore for T
425where
426    T: MediaStore + Sized + 'static,
427{
428    fn into_media_store(self) -> Arc<DynMediaStore> {
429        Arc::new(EraseMediaStoreError(self))
430    }
431}
432
433// Turns a given `Arc<T>` into `Arc<DynMediaStore>` by attaching the
434// `MediaStore` impl vtable of `EraseMediaStoreError<T>`.
435impl<T> IntoMediaStore for Arc<T>
436where
437    T: MediaStore + 'static,
438{
439    fn into_media_store(self) -> Arc<DynMediaStore> {
440        let ptr: *const T = Arc::into_raw(self);
441        let ptr_erased = ptr as *const EraseMediaStoreError<T>;
442        // SAFETY: EraseMediaStoreError is repr(transparent) so T and
443        //         EraseMediaStoreError<T> have the same layout and ABI
444        unsafe { Arc::from_raw(ptr_erased) }
445    }
446}