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 /// Remove all the media files' content associated to an `MxcUri` from the
106 /// media store.
107 ///
108 /// This should not raise an error when the `uri` parameter points to an
109 /// unknown media, and it should return an Ok result in this case.
110 ///
111 /// # Arguments
112 ///
113 /// * `uri` - The `MxcUri` of the media files.
114 async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
115
116 /// Set the `MediaRetentionPolicy` to use for deciding whether to store or
117 /// keep media content.
118 ///
119 /// # Arguments
120 ///
121 /// * `policy` - The `MediaRetentionPolicy` to use.
122 async fn set_media_retention_policy(
123 &self,
124 policy: MediaRetentionPolicy,
125 ) -> Result<(), Self::Error>;
126
127 /// Get the current `MediaRetentionPolicy`.
128 fn media_retention_policy(&self) -> MediaRetentionPolicy;
129
130 /// Set whether the current [`MediaRetentionPolicy`] should be ignored for
131 /// the media.
132 ///
133 /// The change will be taken into account in the next cleanup.
134 ///
135 /// # Arguments
136 ///
137 /// * `request` - The `MediaRequestParameters` of the file.
138 ///
139 /// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
140 /// ignored.
141 async fn set_ignore_media_retention_policy(
142 &self,
143 request: &MediaRequestParameters,
144 ignore_policy: IgnoreMediaRetentionPolicy,
145 ) -> Result<(), Self::Error>;
146
147 /// Clean up the media cache with the current `MediaRetentionPolicy`.
148 ///
149 /// If there is already an ongoing cleanup, this is a noop.
150 async fn clean(&self) -> Result<(), Self::Error>;
151
152 /// Close the store, releasing all held resources (database connections,
153 /// file descriptors, file locks).
154 ///
155 /// In-flight operations complete before this method returns. After it
156 /// returns, operations will fail until [`Self::reopen()`] is called.
157 async fn close(&self) -> Result<(), Self::Error>;
158
159 /// Reopen the store after a [`Self::close()`], re-acquiring database
160 /// connections.
161 async fn reopen(&self) -> Result<(), Self::Error>;
162
163 /// Perform database optimizations if any are available, i.e. vacuuming in
164 /// SQLite.
165 ///
166 /// **Warning:** this was added to check if SQLite fragmentation was the
167 /// source of performance issues, **DO NOT use in production**.
168 #[doc(hidden)]
169 async fn optimize(&self) -> Result<(), Self::Error>;
170
171 /// Returns the size of the store in bytes, if known.
172 async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
173}
174
175/// An abstract trait that can be used to implement different store backends
176/// for the media cache of the SDK.
177///
178/// The main purposes of this trait are to be able to centralize where we handle
179/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
180/// simplify the implementation of tests by being able to have complete control
181/// over the `SystemTime`s provided to the store.
182#[cfg_attr(target_family = "wasm", async_trait(?Send))]
183#[cfg_attr(not(target_family = "wasm"), async_trait)]
184pub trait MediaStoreInner: AsyncTraitDeps + Clone {
185 /// The error type used by this media cache store.
186 type Error: fmt::Debug + fmt::Display + Into<MediaStoreError>;
187
188 /// The persisted media retention policy in the media cache.
189 async fn media_retention_policy_inner(
190 &self,
191 ) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
192
193 /// Persist the media retention policy in the media cache.
194 ///
195 /// # Arguments
196 ///
197 /// * `policy` - The `MediaRetentionPolicy` to persist.
198 async fn set_media_retention_policy_inner(
199 &self,
200 policy: MediaRetentionPolicy,
201 ) -> Result<(), Self::Error>;
202
203 /// Add a media file's content in the media cache.
204 ///
205 /// # Arguments
206 ///
207 /// * `request` - The `MediaRequestParameters` of the file.
208 ///
209 /// * `content` - The content of the file.
210 ///
211 /// * `current_time` - The current time, to set the last access time of the
212 /// media.
213 ///
214 /// * `policy` - The media retention policy, to check whether the media is
215 /// too big to be cached.
216 ///
217 /// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
218 /// for this media. This setting should be persisted alongside the media
219 /// and taken into account whenever the policy is used.
220 async fn add_media_content_inner(
221 &self,
222 request: &MediaRequestParameters,
223 content: Vec<u8>,
224 current_time: SystemTime,
225 policy: MediaRetentionPolicy,
226 ignore_policy: IgnoreMediaRetentionPolicy,
227 ) -> Result<(), Self::Error>;
228
229 /// Set whether the current [`MediaRetentionPolicy`] should be ignored for
230 /// the media.
231 ///
232 /// If the media of the given request is not found, this should be a noop.
233 ///
234 /// The change will be taken into account in the next cleanup.
235 ///
236 /// # Arguments
237 ///
238 /// * `request` - The `MediaRequestParameters` of the file.
239 ///
240 /// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
241 /// ignored.
242 async fn set_ignore_media_retention_policy_inner(
243 &self,
244 request: &MediaRequestParameters,
245 ignore_policy: IgnoreMediaRetentionPolicy,
246 ) -> Result<(), Self::Error>;
247
248 /// Get a media file's content out of the media cache.
249 ///
250 /// # Arguments
251 ///
252 /// * `request` - The `MediaRequestParameters` of the file.
253 ///
254 /// * `current_time` - The current time, to update the last access time of
255 /// the media.
256 async fn get_media_content_inner(
257 &self,
258 request: &MediaRequestParameters,
259 current_time: SystemTime,
260 ) -> Result<Option<Vec<u8>>, Self::Error>;
261
262 /// Clean up the media cache with the given policy.
263 ///
264 /// For the integration tests, it is expected that content that does not
265 /// pass the last access expiry and max file size criteria will be
266 /// removed first. After that, the remaining cache size should be
267 /// computed to compare against the max cache size criteria.
268 ///
269 /// # Arguments
270 ///
271 /// * `policy` - The media retention policy to use for the cleanup. The
272 /// `cleanup_frequency` will be ignored.
273 ///
274 /// * `current_time` - The current time, to be used to check for expired
275 /// content and to be stored as the time of the last media cache cleanup.
276 async fn clean_inner(
277 &self,
278 policy: MediaRetentionPolicy,
279 current_time: SystemTime,
280 ) -> Result<(), Self::Error>;
281
282 /// The time of the last media cache cleanup.
283 async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
284}
285
286#[repr(transparent)]
287struct EraseMediaStoreError<T>(T);
288
289#[cfg(not(tarpaulin_include))]
290impl<T: fmt::Debug> fmt::Debug for EraseMediaStoreError<T> {
291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 self.0.fmt(f)
293 }
294}
295
296#[cfg_attr(target_family = "wasm", async_trait(?Send))]
297#[cfg_attr(not(target_family = "wasm"), async_trait)]
298impl<T: MediaStore> MediaStore for EraseMediaStoreError<T> {
299 type Error = MediaStoreError;
300
301 async fn try_take_leased_lock(
302 &self,
303 lease_duration_ms: u32,
304 key: &str,
305 holder: &str,
306 ) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
307 self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
308 }
309
310 async fn add_media_content(
311 &self,
312 request: &MediaRequestParameters,
313 content: Vec<u8>,
314 ignore_policy: IgnoreMediaRetentionPolicy,
315 ) -> Result<(), Self::Error> {
316 self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
317 }
318
319 async fn replace_media_key(
320 &self,
321 from: &MediaRequestParameters,
322 to: &MediaRequestParameters,
323 ) -> Result<(), Self::Error> {
324 self.0.replace_media_key(from, to).await.map_err(Into::into)
325 }
326
327 async fn get_media_content(
328 &self,
329 request: &MediaRequestParameters,
330 ) -> Result<Option<Vec<u8>>, Self::Error> {
331 self.0.get_media_content(request).await.map_err(Into::into)
332 }
333
334 async fn remove_media_content(
335 &self,
336 request: &MediaRequestParameters,
337 ) -> Result<(), Self::Error> {
338 self.0.remove_media_content(request).await.map_err(Into::into)
339 }
340
341 async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
342 self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
343 }
344
345 async fn set_media_retention_policy(
346 &self,
347 policy: MediaRetentionPolicy,
348 ) -> Result<(), Self::Error> {
349 self.0.set_media_retention_policy(policy).await.map_err(Into::into)
350 }
351
352 fn media_retention_policy(&self) -> MediaRetentionPolicy {
353 self.0.media_retention_policy()
354 }
355
356 async fn set_ignore_media_retention_policy(
357 &self,
358 request: &MediaRequestParameters,
359 ignore_policy: IgnoreMediaRetentionPolicy,
360 ) -> Result<(), Self::Error> {
361 self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
362 }
363
364 async fn clean(&self) -> Result<(), Self::Error> {
365 self.0.clean().await.map_err(Into::into)
366 }
367
368 async fn close(&self) -> Result<(), Self::Error> {
369 self.0.close().await.map_err(Into::into)
370 }
371
372 async fn reopen(&self) -> Result<(), Self::Error> {
373 self.0.reopen().await.map_err(Into::into)
374 }
375
376 async fn optimize(&self) -> Result<(), Self::Error> {
377 self.0.optimize().await.map_err(Into::into)
378 }
379
380 async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
381 self.0.get_size().await.map_err(Into::into)
382 }
383}
384
385/// A type-erased [`MediaStore`].
386pub type DynMediaStore = dyn MediaStore<Error = MediaStoreError>;
387
388/// A type that can be type-erased into `Arc<dyn MediaStore>`.
389///
390/// This trait is not meant to be implemented directly outside
391/// `matrix-sdk-base`, but it is automatically implemented for everything that
392/// implements `MediaStore`.
393pub trait IntoMediaStore {
394 #[doc(hidden)]
395 fn into_media_store(self) -> Arc<DynMediaStore>;
396}
397
398impl IntoMediaStore for Arc<DynMediaStore> {
399 fn into_media_store(self) -> Arc<DynMediaStore> {
400 self
401 }
402}
403
404impl<T> IntoMediaStore for T
405where
406 T: MediaStore + Sized + 'static,
407{
408 fn into_media_store(self) -> Arc<DynMediaStore> {
409 Arc::new(EraseMediaStoreError(self))
410 }
411}
412
413// Turns a given `Arc<T>` into `Arc<DynMediaStore>` by attaching the
414// `MediaStore` impl vtable of `EraseMediaStoreError<T>`.
415impl<T> IntoMediaStore for Arc<T>
416where
417 T: MediaStore + 'static,
418{
419 fn into_media_store(self) -> Arc<DynMediaStore> {
420 let ptr: *const T = Arc::into_raw(self);
421 let ptr_erased = ptr as *const EraseMediaStoreError<T>;
422 // SAFETY: EraseMediaStoreError is repr(transparent) so T and
423 // EraseMediaStoreError<T> have the same layout and ABI
424 unsafe { Arc::from_raw(ptr_erased) }
425 }
426}