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