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