Skip to main content

matrix_sdk_sqlite/
media_store.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
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//! An SQLite-based backend for the [`MediaStore`].
16
17use std::{
18    fmt,
19    path::{Path, PathBuf},
20    sync::Arc,
21};
22
23use async_trait::async_trait;
24use deadpool::managed::PoolConfig;
25use matrix_sdk_base::{
26    cross_process_lock::CrossProcessLockGeneration,
27    media::{
28        MediaRequestParameters, UniqueKey,
29        store::{
30            IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore,
31            MediaStoreInner,
32        },
33    },
34    timer,
35};
36use matrix_sdk_store_encryption::StoreCipher;
37use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, time::SystemTime};
38use rusqlite::{OptionalExtension, params_from_iter};
39use tokio::{
40    fs,
41    sync::{Mutex, OwnedMutexGuard},
42};
43use tracing::{debug, instrument};
44
45use crate::{
46    OpenStoreError, RuntimeConfig, Secret, SqliteStoreConfig,
47    connection::{
48        self, Connection as SqliteAsyncConn, Pool as SqlitePool, SqliteConnections,
49        SqliteConnectionsInner,
50    },
51    error::{Error, Result},
52    utils::{
53        EncryptableStore, SqliteAsyncConnExt, SqliteKeyValueStoreAsyncConnExt,
54        SqliteKeyValueStoreConnExt, SqliteTransactionExt, time_to_timestamp,
55    },
56};
57
58mod keys {
59    // Entries in Key-value store
60    pub const MEDIA_RETENTION_POLICY: &str = "media_retention_policy";
61    pub const LAST_MEDIA_CLEANUP_TIME: &str = "last_media_cleanup_time";
62
63    // Tables
64    pub const MEDIA: &str = "media";
65}
66
67/// The database name.
68const DATABASE_NAME: &str = "matrix-sdk-media.sqlite3";
69
70/// An SQLite-based media store.
71#[derive(Clone)]
72pub struct SqliteMediaStore {
73    store_cipher: Option<Arc<StoreCipher>>,
74
75    /// `Some` when active, `None` when closed.
76    connections: Arc<SqliteConnections>,
77
78    /// Retained so we can rebuild the pool on reopen.
79    db_path: PathBuf,
80
81    /// Retained so we can rebuild the pool on reopen.
82    pool_config: PoolConfig,
83
84    /// Retained so we can re-apply runtime config on reopen.
85    runtime_config: RuntimeConfig,
86
87    media_service: MediaService,
88}
89
90#[cfg(not(tarpaulin_include))]
91impl fmt::Debug for SqliteMediaStore {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.debug_struct("SqliteMediaStore").finish_non_exhaustive()
94    }
95}
96
97impl EncryptableStore for SqliteMediaStore {
98    fn get_cypher(&self) -> Option<&StoreCipher> {
99        self.store_cipher.as_deref()
100    }
101}
102
103impl SqliteMediaStore {
104    /// Open the SQLite-based media store at the given path using the given
105    /// passphrase to encrypt private data.
106    pub async fn open(
107        path: impl AsRef<Path>,
108        passphrase: Option<&str>,
109    ) -> Result<Self, OpenStoreError> {
110        Self::open_with_config(&SqliteStoreConfig::new(path).passphrase(passphrase)).await
111    }
112
113    /// Open the SQLite-based media store at the given path using the given key
114    /// to encrypt private data.
115    pub async fn open_with_key(
116        path: impl AsRef<Path>,
117        key: Option<&[u8]>,
118    ) -> Result<Self, OpenStoreError> {
119        Self::open_with_config(&SqliteStoreConfig::new(path).key(key)).await
120    }
121
122    /// Open the SQLite-based media store with the config open config.
123    #[instrument(skip(config), fields(path = ?config.path))]
124    pub async fn open_with_config(config: &SqliteStoreConfig) -> Result<Self, OpenStoreError> {
125        debug!(?config);
126
127        let _timer = timer!("open_with_config");
128
129        fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir)?;
130
131        let db_path = config.path.join(DATABASE_NAME);
132        let pool_config = config.pool_config();
133        let runtime_config = config.runtime_config();
134
135        let pool = config.build_pool_of_connections(DATABASE_NAME)?;
136
137        let this =
138            Self::open_with_pool(pool, db_path, pool_config, runtime_config, config.secret.clone())
139                .await?;
140
141        // Apply runtime config on the write connection.
142        this.write().await?.apply_runtime_config(runtime_config).await?;
143
144        Ok(this)
145    }
146
147    /// Open an SQLite-based media store using the given SQLite database pool.
148    /// The given passphrase will be used to encrypt private data.
149    async fn open_with_pool(
150        pool: SqlitePool,
151        db_path: PathBuf,
152        pool_config: PoolConfig,
153        runtime_config: RuntimeConfig,
154        secret: Option<Secret>,
155    ) -> Result<Self, OpenStoreError> {
156        let conn = pool.get().await?;
157
158        let version = conn.db_version().await?;
159        run_migrations(&conn, version).await?;
160
161        conn.wal_checkpoint().await;
162
163        let store_cipher = match &secret {
164            Some(s) => Some(Arc::new(conn.get_or_create_store_cipher(s.clone()).await?)),
165            None => None,
166        };
167
168        let media_service = MediaService::new();
169        let media_retention_policy = conn.get_serialized_kv(keys::MEDIA_RETENTION_POLICY).await?;
170        let last_media_cleanup_time = conn.get_serialized_kv(keys::LAST_MEDIA_CLEANUP_TIME).await?;
171        media_service.restore(media_retention_policy, last_media_cleanup_time);
172
173        let connections = SqliteConnectionsInner {
174            pool,
175            // Use `conn` as our selected write connection.
176            write_connection: Arc::new(Mutex::new(conn)),
177        };
178
179        Ok(Self {
180            store_cipher,
181            connections: Arc::new(SqliteConnections::new(connections).await),
182            db_path,
183            pool_config,
184            runtime_config,
185            media_service,
186        })
187    }
188
189    // Acquire a connection for executing read operations.
190    #[instrument(skip_all)]
191    async fn read(&self) -> Result<SqliteAsyncConn> {
192        let pool = {
193            let guard = self.connections.lock().await;
194            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
195            conns.pool.clone()
196        };
197
198        let connection = pool.get().await?;
199
200        // Per https://www.sqlite.org/foreignkeys.html#fk_enable, foreign key
201        // support must be enabled on a per-connection basis. Execute it every
202        // time we try to get a connection, since we can't guarantee a previous
203        // connection did enable it before.
204        connection.execute_batch("PRAGMA foreign_keys = ON;").await?;
205
206        Ok(connection)
207    }
208
209    // Acquire a connection for executing write operations.
210    #[instrument(skip_all)]
211    async fn write(&self) -> Result<OwnedMutexGuard<SqliteAsyncConn>> {
212        let write_connection = {
213            let guard = self.connections.lock().await;
214            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
215            conns.write_connection.clone()
216        };
217
218        let connection = write_connection.lock_owned().await;
219
220        // Per https://www.sqlite.org/foreignkeys.html#fk_enable, foreign key
221        // support must be enabled on a per-connection basis. Execute it every
222        // time we try to get a connection, since we can't guarantee a previous
223        // connection did enable it before.
224        connection.execute_batch("PRAGMA foreign_keys = ON;").await?;
225
226        Ok(connection)
227    }
228
229    pub async fn vacuum(&self) -> Result<()> {
230        let write_connection = {
231            let guard = self.connections.lock().await;
232            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
233            conns.write_connection.clone()
234        };
235        write_connection.lock().await.vacuum().await
236    }
237
238    async fn get_db_size(&self) -> Result<Option<usize>> {
239        let pool = {
240            let guard = self.connections.lock().await;
241            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
242            conns.pool.clone()
243        };
244        Ok(Some(pool.get().await?.get_db_size().await?))
245    }
246
247    pub async fn close(&self) -> Result<()> {
248        connection::close_connections(&self.connections, "Media store").await;
249        Ok(())
250    }
251
252    pub async fn reopen(&self) -> Result<()> {
253        connection::reopen_connections(
254            &self.connections,
255            self.db_path.clone(),
256            self.pool_config,
257            self.runtime_config,
258        )
259        .await?;
260        Ok(())
261    }
262
263    /// Returns the pool size status, for testing purposes.
264    #[cfg(test)]
265    async fn pool_max_size(&self) -> Option<usize> {
266        let guard = self.connections.lock().await;
267        guard.as_ref().map(|conns| conns.pool.status().max_size)
268    }
269}
270
271/// Run migrations for the given version of the database.
272async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> {
273    // Always enable foreign keys for the current connection.
274    conn.execute_batch("PRAGMA foreign_keys = ON;").await?;
275
276    if version < 1 {
277        debug!("Creating database");
278        // First turn on WAL mode, this can't be done in the transaction, it
279        // fails with the error message: "cannot change into wal mode from
280        // within a transaction".
281        conn.execute_batch("PRAGMA journal_mode = wal;").await?;
282        conn.with_transaction(|txn| {
283            txn.execute_batch(include_str!("../migrations/media_store/001_init.sql"))?;
284            txn.set_db_version(1)
285        })
286        .await?;
287    }
288
289    if version < 2 {
290        debug!("Upgrading database to version 2");
291        conn.with_transaction(|txn| {
292            txn.execute_batch(include_str!(
293                "../migrations/media_store/002_lease_locks_with_generation.sql"
294            ))?;
295            txn.set_db_version(2)
296        })
297        .await?;
298    }
299
300    Ok(())
301}
302
303#[async_trait]
304impl MediaStore for SqliteMediaStore {
305    type Error = Error;
306
307    #[instrument(skip(self))]
308    async fn try_take_leased_lock(
309        &self,
310        lease_duration_ms: u32,
311        key: &str,
312        holder: &str,
313    ) -> Result<Option<CrossProcessLockGeneration>> {
314        let key = key.to_owned();
315        let holder = holder.to_owned();
316
317        let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
318        let expiration = now + lease_duration_ms as u64;
319
320        // Learn about the `excluded` keyword in https://sqlite.org/lang_upsert.html.
321        let generation = self
322            .write()
323            .await?
324            .with_transaction(move |txn| {
325                txn.query_one(
326                    "INSERT INTO lease_locks (key, holder, expiration)
327                    VALUES (?1, ?2, ?3)
328                    ON CONFLICT (key)
329                    DO
330                        UPDATE SET
331                            holder = excluded.holder,
332                            expiration = excluded.expiration,
333                            generation =
334                                CASE holder
335                                    WHEN excluded.holder THEN generation
336                                    ELSE generation + 1
337                                END
338                        WHERE
339                            holder = excluded.holder
340                            OR expiration < ?4
341                    RETURNING generation
342                    ",
343                    (key, holder, expiration, now),
344                    |row| row.get(0),
345                )
346                .optional()
347            })
348            .await?;
349
350        Ok(generation)
351    }
352
353    async fn add_media_content(
354        &self,
355        request: &MediaRequestParameters,
356        content: Vec<u8>,
357        ignore_policy: IgnoreMediaRetentionPolicy,
358    ) -> Result<()> {
359        let _timer = timer!("method");
360
361        self.media_service.add_media_content(self, request, content, ignore_policy).await
362    }
363
364    #[instrument(skip_all)]
365    async fn replace_media_key(
366        &self,
367        from: &MediaRequestParameters,
368        to: &MediaRequestParameters,
369    ) -> Result<(), Self::Error> {
370        let _timer = timer!("method");
371
372        let prev_uri = self.encode_key(keys::MEDIA, from.source.unique_key());
373        let prev_format = self.encode_key(keys::MEDIA, from.format.unique_key());
374
375        let new_uri = self.encode_key(keys::MEDIA, to.source.unique_key());
376        let new_format = self.encode_key(keys::MEDIA, to.format.unique_key());
377
378        let conn = self.write().await?;
379        conn.execute(
380            r#"UPDATE media SET uri = ?, format = ? WHERE uri = ? AND format = ?"#,
381            (new_uri, new_format, prev_uri, prev_format),
382        )
383        .await?;
384
385        Ok(())
386    }
387
388    #[instrument(skip_all)]
389    async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
390        let _timer = timer!("method");
391
392        self.media_service.get_media_content(self, request).await
393    }
394
395    #[instrument(skip_all)]
396    async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
397        let _timer = timer!("method");
398
399        let uri = self.encode_key(keys::MEDIA, request.source.unique_key());
400        let format = self.encode_key(keys::MEDIA, request.format.unique_key());
401
402        let conn = self.write().await?;
403        conn.execute("DELETE FROM media WHERE uri = ? AND format = ?", (uri, format)).await?;
404
405        Ok(())
406    }
407
408    #[instrument(skip(self))]
409    async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
410        let _timer = timer!("method");
411
412        let uri = self.encode_key(keys::MEDIA, uri);
413
414        let conn = self.write().await?;
415        conn.execute("DELETE FROM media WHERE uri = ?", (uri,)).await?;
416
417        Ok(())
418    }
419
420    #[instrument(skip_all)]
421    async fn set_media_retention_policy(
422        &self,
423        policy: MediaRetentionPolicy,
424    ) -> Result<(), Self::Error> {
425        let _timer = timer!("method");
426
427        self.media_service.set_media_retention_policy(self, policy).await
428    }
429
430    #[instrument(skip_all)]
431    fn media_retention_policy(&self) -> MediaRetentionPolicy {
432        let _timer = timer!("method");
433
434        self.media_service.media_retention_policy()
435    }
436
437    #[instrument(skip_all)]
438    async fn set_ignore_media_retention_policy(
439        &self,
440        request: &MediaRequestParameters,
441        ignore_policy: IgnoreMediaRetentionPolicy,
442    ) -> Result<(), Self::Error> {
443        let _timer = timer!("method");
444
445        self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
446    }
447
448    #[instrument(skip_all)]
449    async fn clean(&self) -> Result<(), Self::Error> {
450        let _timer = timer!("method");
451
452        self.media_service.clean(self).await
453    }
454
455    async fn close(&self) -> Result<(), Self::Error> {
456        SqliteMediaStore::close(self).await
457    }
458
459    async fn reopen(&self) -> Result<(), Self::Error> {
460        SqliteMediaStore::reopen(self).await
461    }
462
463    async fn optimize(&self) -> Result<(), Self::Error> {
464        Ok(self.vacuum().await?)
465    }
466
467    async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
468        self.get_db_size().await
469    }
470}
471
472#[cfg_attr(target_family = "wasm", async_trait(?Send))]
473#[cfg_attr(not(target_family = "wasm"), async_trait)]
474impl MediaStoreInner for SqliteMediaStore {
475    type Error = Error;
476
477    async fn media_retention_policy_inner(
478        &self,
479    ) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
480        let conn = self.read().await?;
481        conn.get_serialized_kv(keys::MEDIA_RETENTION_POLICY).await
482    }
483
484    async fn set_media_retention_policy_inner(
485        &self,
486        policy: MediaRetentionPolicy,
487    ) -> Result<(), Self::Error> {
488        let conn = self.write().await?;
489        conn.set_serialized_kv(keys::MEDIA_RETENTION_POLICY, policy).await?;
490        Ok(())
491    }
492
493    async fn add_media_content_inner(
494        &self,
495        request: &MediaRequestParameters,
496        data: Vec<u8>,
497        last_access: SystemTime,
498        policy: MediaRetentionPolicy,
499        ignore_policy: IgnoreMediaRetentionPolicy,
500    ) -> Result<(), Self::Error> {
501        let ignore_policy = ignore_policy.is_yes();
502        let data = self.encode_value(data)?;
503
504        if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
505            return Ok(());
506        }
507
508        let uri = self.encode_key(keys::MEDIA, request.source.unique_key());
509        let format = self.encode_key(keys::MEDIA, request.format.unique_key());
510        let timestamp = time_to_timestamp(last_access);
511
512        let conn = self.write().await?;
513        conn.execute(
514            "INSERT OR REPLACE INTO media (uri, format, data, last_access, ignore_policy) VALUES (?, ?, ?, ?, ?)",
515            (uri, format, data, timestamp, ignore_policy),
516        )
517        .await?;
518
519        Ok(())
520    }
521
522    async fn set_ignore_media_retention_policy_inner(
523        &self,
524        request: &MediaRequestParameters,
525        ignore_policy: IgnoreMediaRetentionPolicy,
526    ) -> Result<(), Self::Error> {
527        let uri = self.encode_key(keys::MEDIA, request.source.unique_key());
528        let format = self.encode_key(keys::MEDIA, request.format.unique_key());
529        let ignore_policy = ignore_policy.is_yes();
530
531        let conn = self.write().await?;
532        conn.execute(
533            r#"UPDATE media SET ignore_policy = ? WHERE uri = ? AND format = ?"#,
534            (ignore_policy, uri, format),
535        )
536        .await?;
537
538        Ok(())
539    }
540
541    async fn get_media_content_inner(
542        &self,
543        request: &MediaRequestParameters,
544        current_time: SystemTime,
545    ) -> Result<Option<Vec<u8>>, Self::Error> {
546        let uri = self.encode_key(keys::MEDIA, request.source.unique_key());
547        let format = self.encode_key(keys::MEDIA, request.format.unique_key());
548        let timestamp = time_to_timestamp(current_time);
549
550        let conn = self.write().await?;
551        let data = conn
552            .with_transaction::<_, rusqlite::Error, _>(move |txn| {
553                // Update the last access. We need to do this first so the
554                // transaction is in write mode right away. See:
555                // https://sqlite.org/lang_transaction.html#read_transactions_versus_write_transactions
556                txn.execute(
557                    "UPDATE media SET last_access = ? WHERE uri = ? AND format = ?",
558                    (timestamp, &uri, &format),
559                )?;
560
561                txn.query_row::<Vec<u8>, _, _>(
562                    "SELECT data FROM media WHERE uri = ? AND format = ?",
563                    (&uri, &format),
564                    |row| row.get(0),
565                )
566                .optional()
567            })
568            .await?;
569
570        data.map(|v| self.decode_value(&v).map(Into::into)).transpose()
571    }
572
573    async fn clean_inner(
574        &self,
575        policy: MediaRetentionPolicy,
576        current_time: SystemTime,
577    ) -> Result<(), Self::Error> {
578        if !policy.has_limitations() {
579            // We can safely skip all the checks.
580            return Ok(());
581        }
582
583        let conn = self.write().await?;
584        let removed = conn
585            .with_transaction::<_, Error, _>(move |txn| {
586                let mut removed = false;
587
588                // First, check media content that exceed the max filesize.
589                if let Some(max_file_size) = policy.computed_max_file_size() {
590                    let count = txn.execute(
591                        "DELETE FROM media WHERE ignore_policy IS FALSE AND length(data) > ?",
592                        (max_file_size,),
593                    )?;
594
595                    if count > 0 {
596                        removed = true;
597                    }
598                }
599
600                // Then, clean up expired media content.
601                if let Some(last_access_expiry) = policy.last_access_expiry {
602                    let current_timestamp = time_to_timestamp(current_time);
603                    let expiry_secs = last_access_expiry.as_secs();
604                    let count = txn.execute(
605                        "DELETE FROM media WHERE ignore_policy IS FALSE AND (? - last_access) >= ?",
606                        (current_timestamp, expiry_secs),
607                    )?;
608
609                    if count > 0 {
610                        removed = true;
611                    }
612                }
613
614                // Finally, if the cache size is too big, remove old items until
615                // it fits.
616                if let Some(max_cache_size) = policy.max_cache_size {
617                    // i64 is the integer type used by SQLite, use it here to
618                    // avoid usize overflow during the conversion of the result.
619                    let cache_size = txn
620                        .query_row(
621                            "SELECT sum(length(data)) FROM media WHERE ignore_policy IS FALSE",
622                            (),
623                            |row| {
624                                // `sum()` returns `NULL` if there are no rows.
625                                row.get::<_, Option<u64>>(0)
626                            },
627                        )?
628                        .unwrap_or_default();
629
630                    // If the cache size is overflowing or bigger than max cache
631                    // size, clean up.
632                    if cache_size > max_cache_size {
633                        // Get the sizes of the media contents ordered by last
634                        // access.
635                        let mut cached_stmt = txn.prepare_cached(
636                            "SELECT rowid, length(data) FROM media \
637                             WHERE ignore_policy IS FALSE ORDER BY last_access DESC",
638                        )?;
639                        let content_sizes = cached_stmt
640                            .query(())?
641                            .mapped(|row| Ok((row.get::<_, i64>(0)?, row.get::<_, u64>(1)?)));
642
643                        let mut accumulated_items_size = 0u64;
644                        let mut limit_reached = false;
645                        let mut rows_to_remove = Vec::new();
646
647                        for result in content_sizes {
648                            let (row_id, size) = match result {
649                                Ok(content_size) => content_size,
650                                Err(error) => {
651                                    return Err(error.into());
652                                }
653                            };
654
655                            if limit_reached {
656                                rows_to_remove.push(row_id);
657                                continue;
658                            }
659
660                            match accumulated_items_size.checked_add(size) {
661                                Some(acc) if acc > max_cache_size => {
662                                    // We can stop accumulating.
663                                    limit_reached = true;
664                                    rows_to_remove.push(row_id);
665                                }
666                                Some(acc) => accumulated_items_size = acc,
667                                None => {
668                                    // The accumulated size is overflowing but
669                                    // the setting cannot be bigger than
670                                    // usize::MAX, we can stop accumulating.
671                                    limit_reached = true;
672                                    rows_to_remove.push(row_id);
673                                }
674                            }
675                        }
676
677                        if !rows_to_remove.is_empty() {
678                            removed = true;
679                        }
680
681                        txn.chunk_large_query_over(rows_to_remove, None, |txn, row_ids| {
682                            let sql_params = row_ids.host_parameters();
683                            let query = format!("DELETE FROM media WHERE rowid IN ({sql_params})");
684                            txn.prepare(&query)?.execute(params_from_iter(row_ids))?;
685                            Ok(Vec::<()>::new())
686                        })?;
687                    }
688                }
689
690                txn.set_serialized_kv(keys::LAST_MEDIA_CLEANUP_TIME, current_time)?;
691
692                Ok(removed)
693            })
694            .await?;
695
696        // If we removed media, defragment the database and free space on the
697        // filesystem.
698        if removed {
699            conn.vacuum().await?;
700        }
701
702        Ok(())
703    }
704
705    async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
706        let conn = self.read().await?;
707        conn.get_serialized_kv(keys::LAST_MEDIA_CLEANUP_TIME).await
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use std::{
714        path::PathBuf,
715        sync::{
716            LazyLock,
717            atomic::{AtomicU32, Ordering::SeqCst},
718        },
719        time::Duration,
720    };
721
722    use matrix_sdk_base::{
723        media::{
724            MediaFormat, MediaRequestParameters, MediaThumbnailSettings,
725            store::{IgnoreMediaRetentionPolicy, MediaStore, MediaStoreError},
726        },
727        media_store_inner_integration_tests, media_store_integration_tests,
728        media_store_integration_tests_time,
729    };
730    use matrix_sdk_test::async_test;
731    use ruma::{events::room::MediaSource, media::Method, mxc_uri, uint};
732    use tempfile::{TempDir, tempdir};
733
734    use super::SqliteMediaStore;
735    use crate::{SqliteStoreConfig, utils::SqliteAsyncConnExt};
736
737    static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
738    static NUM: AtomicU32 = AtomicU32::new(0);
739
740    fn new_media_store_workspace() -> PathBuf {
741        let name = NUM.fetch_add(1, SeqCst).to_string();
742        TMP_DIR.path().join(name)
743    }
744
745    async fn get_media_store() -> Result<SqliteMediaStore, MediaStoreError> {
746        let tmpdir_path = new_media_store_workspace();
747
748        tracing::info!("using media store @ {}", tmpdir_path.to_str().unwrap());
749
750        Ok(SqliteMediaStore::open(tmpdir_path.to_str().unwrap(), None).await.unwrap())
751    }
752
753    media_store_integration_tests!();
754    media_store_integration_tests_time!();
755    media_store_inner_integration_tests!();
756
757    async fn get_media_store_content_sorted_by_last_access(
758        media_store: &SqliteMediaStore,
759    ) -> Vec<Vec<u8>> {
760        let sqlite_db = media_store.read().await.expect("accessing sqlite db failed");
761        sqlite_db
762            .prepare("SELECT data FROM media ORDER BY last_access DESC", |mut stmt| {
763                stmt.query(())?.mapped(|row| row.get(0)).collect()
764            })
765            .await
766            .expect("querying media cache content by last access failed")
767    }
768
769    #[async_test]
770    async fn test_pool_size() {
771        let tmpdir_path = new_media_store_workspace();
772        let store_open_config = SqliteStoreConfig::new(tmpdir_path).pool_max_size(42);
773
774        let store = SqliteMediaStore::open_with_config(&store_open_config).await.unwrap();
775
776        assert_eq!(store.pool_max_size().await.unwrap(), 42);
777    }
778
779    #[async_test]
780    async fn test_last_access() {
781        let media_store = get_media_store().await.expect("creating media cache failed");
782        let uri = mxc_uri!("mxc://localhost/media");
783        let file_request = MediaRequestParameters {
784            source: MediaSource::Plain(uri.to_owned()),
785            format: MediaFormat::File,
786        };
787        let thumbnail_request = MediaRequestParameters {
788            source: MediaSource::Plain(uri.to_owned()),
789            format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
790                Method::Crop,
791                uint!(100),
792                uint!(100),
793            )),
794        };
795
796        let content: Vec<u8> = "hello world".into();
797        let thumbnail_content: Vec<u8> = "hello…".into();
798
799        // Add the media.
800        media_store
801            .add_media_content(&file_request, content.clone(), IgnoreMediaRetentionPolicy::No)
802            .await
803            .expect("adding file failed");
804
805        // Since the precision of the timestamp is in seconds, wait so the
806        // timestamps differ.
807        tokio::time::sleep(Duration::from_secs(3)).await;
808
809        media_store
810            .add_media_content(
811                &thumbnail_request,
812                thumbnail_content.clone(),
813                IgnoreMediaRetentionPolicy::No,
814            )
815            .await
816            .expect("adding thumbnail failed");
817
818        // File's last access is older than thumbnail.
819        let contents = get_media_store_content_sorted_by_last_access(&media_store).await;
820
821        assert_eq!(contents.len(), 2, "media cache contents length is wrong");
822        assert_eq!(contents[0], thumbnail_content, "thumbnail is not last access");
823        assert_eq!(contents[1], content, "file is not second-to-last access");
824
825        // Since the precision of the timestamp is in seconds, wait so the
826        // timestamps differ.
827        tokio::time::sleep(Duration::from_secs(3)).await;
828
829        // Access the file so its last access is more recent.
830        let _ = media_store
831            .get_media_content(&file_request)
832            .await
833            .expect("getting file failed")
834            .expect("file is missing");
835
836        // File's last access is more recent than thumbnail.
837        let contents = get_media_store_content_sorted_by_last_access(&media_store).await;
838
839        assert_eq!(contents.len(), 2, "media cache contents length is wrong");
840        assert_eq!(contents[0], content, "file is not last access");
841        assert_eq!(contents[1], thumbnail_content, "thumbnail is not second-to-last access");
842    }
843}
844
845#[cfg(test)]
846mod close_reopen_tests {
847    use std::sync::{
848        LazyLock,
849        atomic::{AtomicU32, Ordering::SeqCst},
850    };
851
852    use matrix_sdk_base::media::{
853        MediaFormat, MediaRequestParameters,
854        store::{IgnoreMediaRetentionPolicy, MediaStore},
855    };
856    use matrix_sdk_test::async_test;
857    use ruma::{events::room::MediaSource, mxc_uri};
858    use tempfile::{TempDir, tempdir};
859
860    use super::SqliteMediaStore;
861
862    static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
863    static NUM: AtomicU32 = AtomicU32::new(0);
864
865    async fn new_store() -> SqliteMediaStore {
866        let name = NUM.fetch_add(1, SeqCst).to_string();
867        let tmpdir_path = TMP_DIR.path().join(name);
868        SqliteMediaStore::open(tmpdir_path, None).await.unwrap()
869    }
870
871    fn test_request() -> MediaRequestParameters {
872        MediaRequestParameters {
873            source: MediaSource::Plain(mxc_uri!("mxc://localhost/test_media").to_owned()),
874            format: MediaFormat::File,
875        }
876    }
877
878    #[async_test]
879    async fn test_close_completes_without_timeout() {
880        let store = new_store().await;
881
882        // Close should complete quickly without hitting any timeout.
883        let start = std::time::Instant::now();
884        store.close().await.unwrap();
885        let elapsed = start.elapsed();
886
887        assert!(
888            elapsed < std::time::Duration::from_secs(2),
889            "close() took {elapsed:?}, expected < 2s (no timeout)"
890        );
891
892        // Connections should be None after close.
893        let guard = store.connections.lock().await;
894        assert!(guard.is_none(), "connections should be None after close");
895    }
896
897    #[async_test]
898    async fn test_reopen_restores_connections() {
899        let store = new_store().await;
900
901        store.close().await.unwrap();
902
903        {
904            let guard = store.connections.lock().await;
905            assert!(guard.is_none());
906        }
907
908        store.reopen().await.unwrap();
909
910        {
911            let guard = store.connections.lock().await;
912            assert!(guard.is_some(), "connections should be Some after reopen");
913        }
914    }
915
916    #[async_test]
917    async fn test_close_is_idempotent() {
918        let store = new_store().await;
919
920        store.close().await.unwrap();
921        // Second close should be a no-op.
922        store.close().await.unwrap();
923
924        let guard = store.connections.lock().await;
925        assert!(guard.is_none());
926    }
927
928    #[async_test]
929    async fn test_reopen_is_idempotent() {
930        let store = new_store().await;
931
932        // Reopen on an active store should be a no-op.
933        store.reopen().await.unwrap();
934
935        let guard = store.connections.lock().await;
936        assert!(guard.is_some());
937    }
938
939    #[async_test]
940    async fn test_read_fails_when_closed() {
941        let store = new_store().await;
942        store.close().await.unwrap();
943
944        let err = store.get_media_content(&test_request()).await;
945        assert!(err.is_err(), "read should fail when closed");
946
947        let err_msg = err.unwrap_err().to_string();
948        assert!(err_msg.contains("closed"), "error should mention 'closed', got: {err_msg}");
949    }
950
951    #[async_test]
952    async fn test_write_fails_when_closed() {
953        let store = new_store().await;
954        store.close().await.unwrap();
955
956        let err = store
957            .add_media_content(&test_request(), b"data".to_vec(), IgnoreMediaRetentionPolicy::No)
958            .await;
959        assert!(err.is_err(), "write should fail when closed");
960
961        let err_msg = err.unwrap_err().to_string();
962        assert!(err_msg.contains("closed"), "error should mention 'closed', got: {err_msg}");
963    }
964
965    #[async_test]
966    async fn test_data_persists_across_close_reopen() {
967        let store = new_store().await;
968
969        // Write some media content.
970        store
971            .add_media_content(
972                &test_request(),
973                b"hello world".to_vec(),
974                IgnoreMediaRetentionPolicy::Yes,
975            )
976            .await
977            .unwrap();
978
979        // Verify it's there.
980        let content = store.get_media_content(&test_request()).await.unwrap();
981        assert_eq!(content.as_deref(), Some(b"hello world".as_slice()));
982
983        // Close and reopen.
984        store.close().await.unwrap();
985        store.reopen().await.unwrap();
986
987        // Content should still be there after reopen.
988        let content = store.get_media_content(&test_request()).await.unwrap();
989        assert_eq!(
990            content.as_deref(),
991            Some(b"hello world".as_slice()),
992            "media content should persist across close/reopen"
993        );
994    }
995
996    #[async_test]
997    async fn test_multiple_close_reopen_cycles() {
998        let store = new_store().await;
999
1000        for _ in 0..5 {
1001            store.close().await.unwrap();
1002            store.reopen().await.unwrap();
1003
1004            // After each cycle, the store should be fully operational.
1005            let result = store.get_media_content(&test_request()).await;
1006            assert!(result.is_ok(), "store should work after close/reopen cycle");
1007        }
1008    }
1009
1010    #[async_test]
1011    async fn test_pool_is_fully_drained_after_close() {
1012        let store = new_store().await;
1013
1014        // Do a few reads to exercise the pool.
1015        let _ = store.get_media_content(&test_request()).await;
1016        let _ = store.get_media_content(&test_request()).await;
1017
1018        store.close().await.unwrap();
1019
1020        // After close, the connections field should be None (pool and write
1021        // connection have been fully torn down).
1022        let guard = store.connections.lock().await;
1023        assert!(guard.is_none(), "all connections should be released after close");
1024    }
1025
1026    #[async_test]
1027    async fn test_operations_work_immediately_after_reopen() {
1028        let store = new_store().await;
1029
1030        store.close().await.unwrap();
1031        store.reopen().await.unwrap();
1032
1033        // Read should work immediately after reopen.
1034        let result = store.get_media_content(&test_request()).await;
1035        assert!(result.is_ok(), "read should succeed immediately after reopen");
1036
1037        // Write should work immediately after reopen.
1038        let result = store
1039            .add_media_content(
1040                &test_request(),
1041                b"after_reopen".to_vec(),
1042                IgnoreMediaRetentionPolicy::No,
1043            )
1044            .await;
1045        assert!(result.is_ok(), "write should succeed immediately after reopen");
1046    }
1047
1048    #[async_test]
1049    async fn test_close_waits_for_held_read_connection_to_drain() {
1050        let store = new_store().await;
1051
1052        // Acquire a read connection and hold it, simulating an in-flight read.
1053        let held_conn = store.read().await.unwrap();
1054
1055        // Spawn close in a background task — it will close the pool and then
1056        // poll-wait for pool.status().size == 0 in the drain loop.
1057        let store_clone = store.clone();
1058        let close_handle = tokio::spawn(async move {
1059            store_clone.close().await.unwrap();
1060        });
1061
1062        // Give close() a moment to close the pool and enter the drain loop.
1063        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1064
1065        // The close task should still be running because we hold a connection.
1066        assert!(!close_handle.is_finished(), "close should be waiting for the held connection");
1067
1068        // Release the held connection — this lets pool.status().size drop to 0.
1069        drop(held_conn);
1070
1071        // Now close should complete promptly (well within the 5s timeout).
1072        let timeout = tokio::time::timeout(std::time::Duration::from_secs(3), close_handle).await;
1073        assert!(timeout.is_ok(), "close should complete after the held connection is released");
1074        timeout.unwrap().unwrap();
1075
1076        // Verify the store is fully closed.
1077        let guard = store.connections.lock().await;
1078        assert!(guard.is_none(), "connections should be None after close");
1079    }
1080}
1081
1082#[cfg(test)]
1083mod encrypted_tests {
1084    use std::sync::{
1085        LazyLock,
1086        atomic::{AtomicU32, Ordering::SeqCst},
1087    };
1088
1089    use matrix_sdk_base::{
1090        media::store::MediaStoreError, media_store_inner_integration_tests,
1091        media_store_integration_tests, media_store_integration_tests_time,
1092    };
1093    use tempfile::{TempDir, tempdir};
1094
1095    use super::SqliteMediaStore;
1096
1097    static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
1098    static NUM: AtomicU32 = AtomicU32::new(0);
1099
1100    async fn get_media_store() -> Result<SqliteMediaStore, MediaStoreError> {
1101        let name = NUM.fetch_add(1, SeqCst).to_string();
1102        let tmpdir_path = TMP_DIR.path().join(name);
1103
1104        tracing::info!("using media store @ {}", tmpdir_path.to_str().unwrap());
1105
1106        Ok(SqliteMediaStore::open(tmpdir_path.to_str().unwrap(), Some("default_test_password"))
1107            .await
1108            .unwrap())
1109    }
1110
1111    media_store_integration_tests!();
1112    media_store_integration_tests_time!();
1113    media_store_inner_integration_tests!();
1114}