matrix_sdk_base/media/store/
mod.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//! The media store holds downloaded media when the cache was
16//! activated to save bandwidth at the cost of increased storage space usage.
17//!
18//! Implementing the `MediaStore` trait, you can plug any storage backend
19//! into the media store for the actual storage. By default this brings an
20//! in-memory store.
21
22mod media_retention_policy;
23mod media_service;
24mod memory_store;
25mod traits;
26#[cfg(any(test, feature = "testing"))]
27#[macro_use]
28pub mod integration_tests;
29
30#[cfg(not(tarpaulin_include))]
31use std::fmt;
32use std::{ops::Deref, sync::Arc};
33
34use matrix_sdk_common::cross_process_lock::{
35    CrossProcessLock, CrossProcessLockError, CrossProcessLockGuard, TryLock,
36};
37use matrix_sdk_store_encryption::Error as StoreEncryptionError;
38pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner};
39
40#[cfg(any(test, feature = "testing"))]
41pub use self::integration_tests::{MediaStoreInnerIntegrationTests, MediaStoreIntegrationTests};
42pub use self::{
43    media_retention_policy::MediaRetentionPolicy,
44    media_service::{IgnoreMediaRetentionPolicy, MediaService},
45    memory_store::MemoryMediaStore,
46};
47
48/// Media store specific error type.
49#[derive(Debug, thiserror::Error)]
50pub enum MediaStoreError {
51    /// An error happened in the underlying database backend.
52    #[error(transparent)]
53    Backend(Box<dyn std::error::Error + Send + Sync>),
54
55    /// The store failed to encrypt or decrypt some data.
56    #[error("Error encrypting or decrypting data from the media store: {0}")]
57    Encryption(#[from] StoreEncryptionError),
58
59    /// The store contains invalid data.
60    #[error("The store contains invalid data: {details}")]
61    InvalidData {
62        /// Details why the data contained in the store was invalid.
63        details: String,
64    },
65
66    /// The store failed to serialize or deserialize some data.
67    #[error("Error serializing or deserializing data from the media store: {0}")]
68    Serialization(#[from] serde_json::Error),
69}
70
71impl MediaStoreError {
72    /// Create a new [`Backend`][Self::Backend] error.
73    ///
74    /// Shorthand for `MediaStoreError::Backend(Box::new(error))`.
75    #[inline]
76    pub fn backend<E>(error: E) -> Self
77    where
78        E: std::error::Error + Send + Sync + 'static,
79    {
80        Self::Backend(Box::new(error))
81    }
82}
83
84/// An `MediaStore` specific result type.
85pub type Result<T, E = MediaStoreError> = std::result::Result<T, E>;
86
87/// The high-level public type to represent an `MediaStore` lock.
88#[derive(Clone)]
89pub struct MediaStoreLock {
90    /// The inner cross process lock that is used to lock the `MediaStore`.
91    cross_process_lock: Arc<CrossProcessLock<LockableMediaStore>>,
92
93    /// The store itself.
94    ///
95    /// That's the only place where the store exists.
96    store: Arc<DynMediaStore>,
97}
98
99#[cfg(not(tarpaulin_include))]
100impl fmt::Debug for MediaStoreLock {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        formatter.debug_struct("MediaStoreLock").finish_non_exhaustive()
103    }
104}
105
106impl MediaStoreLock {
107    /// Create a new lock around the [`MediaStore`].
108    ///
109    /// The `holder` argument represents the holder inside the
110    /// [`CrossProcessLock::new`].
111    pub fn new<S>(store: S, holder: String) -> Self
112    where
113        S: IntoMediaStore,
114    {
115        let store = store.into_media_store();
116
117        Self {
118            cross_process_lock: Arc::new(CrossProcessLock::new(
119                LockableMediaStore(store.clone()),
120                "default".to_owned(),
121                holder,
122            )),
123            store,
124        }
125    }
126
127    /// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
128    pub async fn lock(&self) -> Result<MediaStoreLockGuard<'_>, CrossProcessLockError> {
129        let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?;
130
131        Ok(MediaStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
132    }
133}
134
135/// An RAII implementation of a “scoped lock” of an [`MediaStoreLock`].
136/// When this structure is dropped (falls out of scope), the lock will be
137/// unlocked.
138pub struct MediaStoreLockGuard<'a> {
139    /// The cross process lock guard.
140    #[allow(unused)]
141    cross_process_lock_guard: CrossProcessLockGuard,
142
143    /// A reference to the store.
144    store: &'a DynMediaStore,
145}
146
147#[cfg(not(tarpaulin_include))]
148impl fmt::Debug for MediaStoreLockGuard<'_> {
149    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150        formatter.debug_struct("MediaStoreLockGuard").finish_non_exhaustive()
151    }
152}
153
154impl Deref for MediaStoreLockGuard<'_> {
155    type Target = DynMediaStore;
156
157    fn deref(&self) -> &Self::Target {
158        self.store
159    }
160}
161
162/// A type that wraps the [`MediaStore`] but implements [`TryLock`] to
163/// make it usable inside the cross process lock.
164#[derive(Clone, Debug)]
165struct LockableMediaStore(Arc<DynMediaStore>);
166
167impl TryLock for LockableMediaStore {
168    type LockError = MediaStoreError;
169
170    async fn try_lock(
171        &self,
172        lease_duration_ms: u32,
173        key: &str,
174        holder: &str,
175    ) -> std::result::Result<bool, Self::LockError> {
176        self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
177    }
178}