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