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