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