Skip to main content

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, CrossProcessLockConfig, CrossProcessLockError, CrossProcessLockGeneration,
36    CrossProcessLockGuard, CrossProcessLockState, TryLock,
37};
38use matrix_sdk_store_encryption::Error as StoreEncryptionError;
39pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner};
40
41#[cfg(any(test, feature = "testing"))]
42pub use self::integration_tests::{MediaStoreInnerIntegrationTests, MediaStoreIntegrationTests};
43pub use self::{
44    media_retention_policy::MediaRetentionPolicy,
45    media_service::{IgnoreMediaRetentionPolicy, MediaService},
46    memory_store::MemoryMediaStore,
47};
48
49/// Media store specific error type.
50#[derive(Debug, thiserror::Error)]
51pub enum MediaStoreError {
52    /// An error happened in the underlying database backend.
53    #[error(transparent)]
54    Backend(Box<dyn std::error::Error + Send + Sync>),
55
56    /// The store failed to encrypt or decrypt some data.
57    #[error("Error encrypting or decrypting data from the media store: {0}")]
58    Encryption(#[from] StoreEncryptionError),
59
60    /// The store contains invalid data.
61    #[error("The store contains invalid data: {details}")]
62    InvalidData {
63        /// Details why the data contained in the store was invalid.
64        details: String,
65    },
66
67    /// The store failed to serialize or deserialize some data.
68    #[error("Error serializing or deserializing data from the media store: {0}")]
69    Serialization(#[from] serde_json::Error),
70}
71
72impl MediaStoreError {
73    /// Create a new [`Backend`][Self::Backend] error.
74    ///
75    /// Shorthand for `MediaStoreError::Backend(Box::new(error))`.
76    #[inline]
77    pub fn backend<E>(error: E) -> Self
78    where
79        E: std::error::Error + Send + Sync + 'static,
80    {
81        Self::Backend(Box::new(error))
82    }
83}
84
85impl From<MediaStoreError> for CrossProcessLockError {
86    fn from(value: MediaStoreError) -> Self {
87        Self::TryLock(Arc::new(value))
88    }
89}
90
91/// An `MediaStore` specific result type.
92pub type Result<T, E = MediaStoreError> = std::result::Result<T, E>;
93
94/// The high-level public type to represent an `MediaStore` lock.
95#[derive(Clone)]
96pub struct MediaStoreLock {
97    /// The inner cross process lock that is used to lock the `MediaStore`.
98    cross_process_lock: Arc<CrossProcessLock<LockableMediaStore>>,
99
100    /// The store itself.
101    ///
102    /// That's the only place where the store exists.
103    store: Arc<DynMediaStore>,
104}
105
106#[cfg(not(tarpaulin_include))]
107impl fmt::Debug for MediaStoreLock {
108    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109        formatter.debug_struct("MediaStoreLock").finish_non_exhaustive()
110    }
111}
112
113impl MediaStoreLock {
114    /// Create a new lock around the [`MediaStore`].
115    ///
116    /// The `cross_process_lock_config` argument controls whether we need to
117    /// hold the cross process lock or not.
118    pub fn new<S>(store: S, cross_process_lock_config: CrossProcessLockConfig) -> Self
119    where
120        S: IntoMediaStore,
121    {
122        let store = store.into_media_store();
123
124        let cross_process_lock = Arc::new(CrossProcessLock::new(
125            LockableMediaStore(store.clone()),
126            "default".to_owned(),
127            cross_process_lock_config,
128        ));
129        Self { cross_process_lock, store }
130    }
131
132    /// Close the store, releasing database connections and file locks.
133    pub async fn close(&self) -> Result<(), MediaStoreError> {
134        self.store.close().await
135    }
136
137    /// Reopen the store after a close.
138    pub async fn reopen(&self) -> Result<(), MediaStoreError> {
139        self.store.reopen().await
140    }
141
142    /// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
143    pub async fn lock(&self) -> Result<MediaStoreLockGuard<'_>, CrossProcessLockError> {
144        let cross_process_lock_guard = match self.cross_process_lock.spin_lock(None).await?? {
145            // The lock is clean: no other hold acquired it, all good!
146            CrossProcessLockState::Clean(guard) => guard,
147
148            // The lock is dirty: another holder acquired it since the last time we acquired it.
149            // It's not a problem in the case of the `MediaStore` because this API is
150            // “stateless” at the time of writing (2025-11-11). There is nothing
151            // that can be out-of-sync: all the state is in the database,
152            // nothing in memory.
153            CrossProcessLockState::Dirty(guard) => {
154                guard.clear_dirty();
155
156                guard
157            }
158        };
159
160        Ok(MediaStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
161    }
162}
163
164/// An RAII implementation of a “scoped lock” of an [`MediaStoreLock`].
165/// When this structure is dropped (falls out of scope), the lock will be
166/// unlocked.
167pub struct MediaStoreLockGuard<'a> {
168    /// The cross process lock guard.
169    #[allow(unused)]
170    cross_process_lock_guard: CrossProcessLockGuard,
171
172    /// A reference to the store.
173    store: &'a DynMediaStore,
174}
175
176#[cfg(not(tarpaulin_include))]
177impl fmt::Debug for MediaStoreLockGuard<'_> {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        formatter.debug_struct("MediaStoreLockGuard").finish_non_exhaustive()
180    }
181}
182
183impl Deref for MediaStoreLockGuard<'_> {
184    type Target = DynMediaStore;
185
186    fn deref(&self) -> &Self::Target {
187        self.store
188    }
189}
190
191/// A type that wraps the [`MediaStore`] but implements [`TryLock`] to
192/// make it usable inside the cross process lock.
193#[derive(Clone, Debug)]
194struct LockableMediaStore(Arc<DynMediaStore>);
195
196impl TryLock for LockableMediaStore {
197    type LockError = MediaStoreError;
198
199    async fn try_lock(
200        &self,
201        lease_duration_ms: u32,
202        key: &str,
203        holder: &str,
204    ) -> std::result::Result<Option<CrossProcessLockGeneration>, Self::LockError> {
205        self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
206    }
207}