Files
matrix-rust-sdk/crates/matrix-sdk-sqlite/src/event_cache_store.rs
T
Kévin Commaille fe143ffbed sqlite: Remove SqliteObjectEventCacheStoreExt trait
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2024-08-26 10:40:12 +02:00

367 lines
12 KiB
Rust

use std::{borrow::Cow, fmt, path::Path, sync::Arc};
use async_trait::async_trait;
use deadpool_sqlite::{Object as SqliteConn, Pool as SqlitePool, Runtime};
use matrix_sdk_base::{
event_cache_store::EventCacheStore,
media::{MediaRequest, UniqueKey},
};
use matrix_sdk_store_encryption::StoreCipher;
use rusqlite::OptionalExtension;
use tokio::fs;
use tracing::debug;
use crate::{
error::{Error, Result},
get_or_create_store_cipher,
utils::{load_db_version, Key, SqliteObjectExt},
OpenStoreError, SqliteObjectStoreExt,
};
mod keys {
// Tables
pub const MEDIA: &str = "media";
}
/// Identifier of the latest database version.
///
/// This is used to figure whether the SQLite database requires a migration.
/// Every new SQL migration should imply a bump of this number, and changes in
/// the [`SqliteEventCacheStore::run_migrations`] function.
const DATABASE_VERSION: u8 = 1;
/// A SQLite-based event cache store.
#[derive(Clone)]
pub struct SqliteEventCacheStore {
store_cipher: Option<Arc<StoreCipher>>,
pool: SqlitePool,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for SqliteEventCacheStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SqliteEventCacheStore").finish_non_exhaustive()
}
}
impl SqliteEventCacheStore {
/// Open the SQLite-based event cache store at the given path using the
/// given passphrase to encrypt private data.
pub async fn open(
path: impl AsRef<Path>,
passphrase: Option<&str>,
) -> Result<Self, OpenStoreError> {
let pool = create_pool(path.as_ref()).await?;
Self::open_with_pool(pool, passphrase).await
}
/// Open an SQLite-based event cache store using the given SQLite database
/// pool. The given passphrase will be used to encrypt private data.
pub async fn open_with_pool(
pool: SqlitePool,
passphrase: Option<&str>,
) -> Result<Self, OpenStoreError> {
let conn = pool.get().await?;
let mut version = load_db_version(&conn).await?;
if version == 0 {
init(&conn).await?;
version = 1;
}
let store_cipher = match passphrase {
Some(p) => Some(Arc::new(get_or_create_store_cipher(p, &conn).await?)),
None => None,
};
let this = Self { store_cipher, pool };
this.run_migrations(&conn, version, None).await?;
Ok(this)
}
/// Run database migrations from the given `from` version to the given `to`
/// version
///
/// If `to` is `None`, the current database version will be used.
async fn run_migrations(&self, conn: &SqliteConn, from: u8, to: Option<u8>) -> Result<()> {
let to = to.unwrap_or(DATABASE_VERSION);
if from < to {
debug!(version = from, new_version = to, "Upgrading database");
} else {
return Ok(());
}
// There is no migration currently since it's the first version of the database.
conn.set_kv("version", vec![to]).await?;
Ok(())
}
fn encode_value(&self, value: Vec<u8>) -> Result<Vec<u8>> {
if let Some(key) = &self.store_cipher {
let encrypted = key.encrypt_value_data(value)?;
Ok(rmp_serde::to_vec_named(&encrypted)?)
} else {
Ok(value)
}
}
fn decode_value<'a>(&self, value: &'a [u8]) -> Result<Cow<'a, [u8]>> {
if let Some(key) = &self.store_cipher {
let encrypted = rmp_serde::from_slice(value)?;
let decrypted = key.decrypt_value_data(encrypted)?;
Ok(Cow::Owned(decrypted))
} else {
Ok(Cow::Borrowed(value))
}
}
fn encode_key(&self, table_name: &str, key: impl AsRef<[u8]>) -> Key {
let bytes = key.as_ref();
if let Some(store_cipher) = &self.store_cipher {
Key::Hashed(store_cipher.hash_key(table_name, bytes))
} else {
Key::Plain(bytes.to_owned())
}
}
async fn acquire(&self) -> Result<SqliteConn> {
Ok(self.pool.get().await?)
}
}
async fn create_pool(path: &Path) -> Result<SqlitePool, OpenStoreError> {
fs::create_dir_all(path).await.map_err(OpenStoreError::CreateDir)?;
let cfg = deadpool_sqlite::Config::new(path.join("matrix-sdk-event-cache.sqlite3"));
Ok(cfg.create_pool(Runtime::Tokio1)?)
}
/// Initialize the database.
async fn init(conn: &SqliteConn) -> Result<()> {
// First turn on WAL mode, this can't be done in the transaction, it fails with
// the error message: "cannot change into wal mode from within a transaction".
conn.execute_batch("PRAGMA journal_mode = wal;").await?;
conn.with_transaction(|txn| {
txn.execute_batch(include_str!("../migrations/event_cache_store/001_init.sql"))
})
.await?;
conn.set_kv("version", vec![1]).await?;
Ok(())
}
#[async_trait]
impl EventCacheStore for SqliteEventCacheStore {
type Error = Error;
async fn add_media_content(&self, request: &MediaRequest, content: Vec<u8>) -> Result<()> {
let uri = self.encode_key(keys::MEDIA, request.source.unique_key());
let format = self.encode_key(keys::MEDIA, request.format.unique_key());
let data = self.encode_value(content)?;
let conn = self.acquire().await?;
conn.execute(
"INSERT OR REPLACE INTO media (uri, format, data, last_access) VALUES (?, ?, ?, CAST(strftime('%s') as INT))",
(uri, format, data),
)
.await?;
Ok(())
}
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
let uri = self.encode_key(keys::MEDIA, request.source.unique_key());
let format = self.encode_key(keys::MEDIA, request.format.unique_key());
let conn = self.acquire().await?;
let data = conn
.with_transaction::<_, rusqlite::Error, _>(move |txn| {
let Some(media) = txn
.query_row::<Vec<u8>, _, _>(
"SELECT data FROM media WHERE uri = ? AND format = ?",
(&uri, &format),
|row| row.get(0),
)
.optional()?
else {
return Ok(None);
};
// Update the last access.
txn.execute(
"UPDATE media SET last_access = CAST(strftime('%s') as INT) \
WHERE uri = ? AND format = ?",
(uri, format),
)?;
Ok(Some(media))
})
.await?;
data.map(|v| self.decode_value(&v).map(Into::into)).transpose()
}
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
let uri = self.encode_key(keys::MEDIA, request.source.unique_key());
let format = self.encode_key(keys::MEDIA, request.format.unique_key());
let conn = self.acquire().await?;
conn.execute("DELETE FROM media WHERE uri = ? AND format = ?", (uri, format)).await?;
Ok(())
}
async fn remove_media_content_for_uri(&self, uri: &ruma::MxcUri) -> Result<()> {
let uri = self.encode_key(keys::MEDIA, uri);
let conn = self.acquire().await?;
conn.execute("DELETE FROM media WHERE uri = ?", (uri,)).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{
sync::atomic::{AtomicU32, Ordering::SeqCst},
time::Duration,
};
use matrix_sdk_base::{
event_cache_store::{EventCacheStore, EventCacheStoreError},
event_cache_store_integration_tests,
media::{MediaFormat, MediaRequest, MediaThumbnailSettings},
};
use matrix_sdk_test::async_test;
use once_cell::sync::Lazy;
use ruma::{events::room::MediaSource, media::Method, mxc_uri, uint};
use tempfile::{tempdir, TempDir};
use super::SqliteEventCacheStore;
use crate::utils::SqliteObjectExt;
static TMP_DIR: Lazy<TempDir> = Lazy::new(|| tempdir().unwrap());
static NUM: AtomicU32 = AtomicU32::new(0);
async fn get_event_cache_store() -> Result<SqliteEventCacheStore, EventCacheStoreError> {
let name = NUM.fetch_add(1, SeqCst).to_string();
let tmpdir_path = TMP_DIR.path().join(name);
tracing::info!("using event cache store @ {}", tmpdir_path.to_str().unwrap());
Ok(SqliteEventCacheStore::open(tmpdir_path.to_str().unwrap(), None).await.unwrap())
}
event_cache_store_integration_tests!();
async fn get_event_cache_store_content_sorted_by_last_access(
event_cache_store: &SqliteEventCacheStore,
) -> Vec<Vec<u8>> {
let sqlite_db = event_cache_store.acquire().await.expect("accessing sqlite db failed");
sqlite_db
.prepare("SELECT data FROM media ORDER BY last_access DESC", |mut stmt| {
stmt.query(())?.mapped(|row| row.get(0)).collect()
})
.await
.expect("querying media cache content by last access failed")
}
#[async_test]
async fn test_last_access() {
let event_cache_store = get_event_cache_store().await.expect("creating media cache failed");
let uri = mxc_uri!("mxc://localhost/media");
let file_request =
MediaRequest { source: MediaSource::Plain(uri.to_owned()), format: MediaFormat::File };
let thumbnail_request = MediaRequest {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::new(
Method::Crop,
uint!(100),
uint!(100),
)),
};
let content: Vec<u8> = "hello world".into();
let thumbnail_content: Vec<u8> = "hello…".into();
// Add the media.
event_cache_store
.add_media_content(&file_request, content.clone())
.await
.expect("adding file failed");
// Since the precision of the timestamp is in seconds, wait so the timestamps
// differ.
tokio::time::sleep(Duration::from_secs(3)).await;
event_cache_store
.add_media_content(&thumbnail_request, thumbnail_content.clone())
.await
.expect("adding thumbnail failed");
// File's last access is older than thumbnail.
let contents =
get_event_cache_store_content_sorted_by_last_access(&event_cache_store).await;
assert_eq!(contents.len(), 2, "media cache contents length is wrong");
assert_eq!(contents[0], thumbnail_content, "thumbnail is not last access");
assert_eq!(contents[1], content, "file is not second-to-last access");
// Since the precision of the timestamp is in seconds, wait so the timestamps
// differ.
tokio::time::sleep(Duration::from_secs(3)).await;
// Access the file so its last access is more recent.
let _ = event_cache_store
.get_media_content(&file_request)
.await
.expect("getting file failed")
.expect("file is missing");
// File's last access is more recent than thumbnail.
let contents =
get_event_cache_store_content_sorted_by_last_access(&event_cache_store).await;
assert_eq!(contents.len(), 2, "media cache contents length is wrong");
assert_eq!(contents[0], content, "file is not last access");
assert_eq!(contents[1], thumbnail_content, "thumbnail is not second-to-last access");
}
}
#[cfg(test)]
mod encrypted_tests {
use std::sync::atomic::{AtomicU32, Ordering::SeqCst};
use matrix_sdk_base::{
event_cache_store::EventCacheStoreError, event_cache_store_integration_tests,
};
use once_cell::sync::Lazy;
use tempfile::{tempdir, TempDir};
use super::SqliteEventCacheStore;
static TMP_DIR: Lazy<TempDir> = Lazy::new(|| tempdir().unwrap());
static NUM: AtomicU32 = AtomicU32::new(0);
async fn get_event_cache_store() -> Result<SqliteEventCacheStore, EventCacheStoreError> {
let name = NUM.fetch_add(1, SeqCst).to_string();
let tmpdir_path = TMP_DIR.path().join(name);
tracing::info!("using event cache store @ {}", tmpdir_path.to_str().unwrap());
Ok(SqliteEventCacheStore::open(
tmpdir_path.to_str().unwrap(),
Some("default_test_password"),
)
.await
.unwrap())
}
event_cache_store_integration_tests!();
}