1use 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 pub const MEDIA_RETENTION_POLICY: &str = "media_retention_policy";
61 pub const LAST_MEDIA_CLEANUP_TIME: &str = "last_media_cleanup_time";
62
63 pub const MEDIA: &str = "media";
65}
66
67const DATABASE_NAME: &str = "matrix-sdk-media.sqlite3";
69
70#[derive(Clone)]
72pub struct SqliteMediaStore {
73 store_cipher: Option<Arc<StoreCipher>>,
74
75 connections: Arc<SqliteConnections>,
77
78 db_path: PathBuf,
80
81 pool_config: PoolConfig,
83
84 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 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 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 #[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 this.write().await?.apply_runtime_config(runtime_config).await?;
143
144 Ok(this)
145 }
146
147 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 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 #[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 connection.execute_batch("PRAGMA foreign_keys = ON;").await?;
205
206 Ok(connection)
207 }
208
209 #[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 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 #[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
271async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> {
273 conn.execute_batch("PRAGMA foreign_keys = ON;").await?;
275
276 if version < 1 {
277 debug!("Creating database");
278 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 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 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 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 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 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 if let Some(max_cache_size) = policy.max_cache_size {
617 let cache_size = txn
620 .query_row(
621 "SELECT sum(length(data)) FROM media WHERE ignore_policy IS FALSE",
622 (),
623 |row| {
624 row.get::<_, Option<u64>>(0)
626 },
627 )?
628 .unwrap_or_default();
629
630 if cache_size > max_cache_size {
633 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 limit_reached = true;
664 rows_to_remove.push(row_id);
665 }
666 Some(acc) => accumulated_items_size = acc,
667 None => {
668 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 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 media_store
801 .add_media_content(&file_request, content.clone(), IgnoreMediaRetentionPolicy::No)
802 .await
803 .expect("adding file failed");
804
805 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 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 tokio::time::sleep(Duration::from_secs(3)).await;
828
829 let _ = media_store
831 .get_media_content(&file_request)
832 .await
833 .expect("getting file failed")
834 .expect("file is missing");
835
836 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 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 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 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 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 store
971 .add_media_content(
972 &test_request(),
973 b"hello world".to_vec(),
974 IgnoreMediaRetentionPolicy::Yes,
975 )
976 .await
977 .unwrap();
978
979 let content = store.get_media_content(&test_request()).await.unwrap();
981 assert_eq!(content.as_deref(), Some(b"hello world".as_slice()));
982
983 store.close().await.unwrap();
985 store.reopen().await.unwrap();
986
987 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 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 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 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 let result = store.get_media_content(&test_request()).await;
1035 assert!(result.is_ok(), "read should succeed immediately after reopen");
1036
1037 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 let held_conn = store.read().await.unwrap();
1054
1055 let store_clone = store.clone();
1058 let close_handle = tokio::spawn(async move {
1059 store_clone.close().await.unwrap();
1060 });
1061
1062 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1064
1065 assert!(!close_handle.is_finished(), "close should be waiting for the held connection");
1067
1068 drop(held_conn);
1070
1071 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 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}