Skip to main content

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