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