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