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}