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}