session-open-group-server/src/storage.rs

395 lines
13 KiB
Rust
Raw Normal View History

2021-03-18 05:04:56 +01:00
use std::collections::HashMap;
2021-03-23 03:43:33 +01:00
use std::path::Path;
2021-03-18 05:04:56 +01:00
use std::sync::Mutex;
2021-04-01 00:55:47 +02:00
use log::{error, info};
2021-03-15 00:16:06 +01:00
use r2d2_sqlite::SqliteConnectionManager;
2021-03-25 00:56:16 +01:00
use rusqlite::params;
2021-06-11 02:05:00 +02:00
use rusqlite_migration::{Migrations, M};
2021-03-12 06:40:24 +01:00
2021-03-18 05:35:18 +01:00
use super::errors::Error;
2021-03-10 03:08:34 +01:00
pub type DatabaseConnection = r2d2::PooledConnection<SqliteConnectionManager>;
pub type DatabaseConnectionPool = r2d2::Pool<SqliteConnectionManager>;
2021-03-18 05:24:47 +01:00
// Main
2021-03-19 01:52:18 +01:00
pub const MAIN_TABLE: &str = "main";
2021-03-18 05:24:47 +01:00
lazy_static::lazy_static! {
2021-03-19 01:52:18 +01:00
pub static ref MAIN_POOL: DatabaseConnectionPool = {
2021-03-23 03:43:33 +01:00
let file_name = "database.db";
2021-03-18 05:24:47 +01:00
let db_manager = r2d2_sqlite::SqliteConnectionManager::file(file_name);
return r2d2::Pool::new(db_manager).unwrap();
};
}
pub fn create_main_database_if_needed() {
let pool = &MAIN_POOL;
let conn = pool.get().unwrap();
create_main_tables_if_needed(&conn);
}
fn create_main_tables_if_needed(conn: &DatabaseConnection) {
let main_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
2021-03-18 05:24:47 +01:00
id TEXT PRIMARY KEY,
2021-03-25 03:56:26 +01:00
name TEXT,
image_id TEXT
2021-03-25 00:56:16 +01:00
)",
MAIN_TABLE
);
2021-03-18 05:24:47 +01:00
conn.execute(&main_table_cmd, params![]).expect("Couldn't create main table.");
}
// Rooms
2021-03-17 01:15:06 +01:00
pub const PENDING_TOKEN_EXPIRATION: i64 = 10 * 60;
2021-03-18 00:06:59 +01:00
pub const TOKEN_EXPIRATION: i64 = 7 * 24 * 60 * 60;
2021-06-01 05:31:25 +02:00
pub const FILE_EXPIRATION: i64 = 15 * 24 * 60 * 60;
2021-03-17 01:15:06 +01:00
2021-03-10 06:17:03 +01:00
pub const MESSAGES_TABLE: &str = "messages";
pub const DELETED_MESSAGES_TABLE: &str = "deleted_messages";
2021-03-11 00:06:09 +01:00
pub const MODERATORS_TABLE: &str = "moderators";
2021-03-11 01:02:28 +01:00
pub const BLOCK_LIST_TABLE: &str = "block_list";
2021-03-17 00:10:26 +01:00
pub const PENDING_TOKENS_TABLE: &str = "pending_tokens";
pub const TOKENS_TABLE: &str = "tokens";
2021-03-19 00:09:13 +01:00
pub const FILES_TABLE: &str = "files";
pub const USER_ACTIVITY_TABLE: &str = "user_activity";
2021-03-18 05:04:56 +01:00
lazy_static::lazy_static! {
static ref POOLS: Mutex<HashMap<String, DatabaseConnectionPool>> = Mutex::new(HashMap::new());
}
2021-03-23 05:39:42 +01:00
pub fn pool_by_room_id(room_id: &str) -> DatabaseConnectionPool {
2021-03-18 05:04:56 +01:00
let mut pools = POOLS.lock().unwrap();
2021-03-23 05:39:42 +01:00
if let Some(pool) = pools.get(room_id) {
2021-03-18 05:04:56 +01:00
return pool.clone();
} else {
2021-03-23 05:39:42 +01:00
let raw_path = format!("rooms/{}.db", room_id);
2021-03-23 03:45:17 +01:00
let path = Path::new(&raw_path);
2021-03-23 03:43:33 +01:00
let db_manager = r2d2_sqlite::SqliteConnectionManager::file(path);
2021-03-18 05:04:56 +01:00
let pool = r2d2::Pool::new(db_manager).unwrap();
2021-03-23 05:39:42 +01:00
pools.insert(room_id.to_string(), pool);
return pools[room_id].clone();
2021-03-18 05:04:56 +01:00
}
}
2021-03-23 05:39:42 +01:00
pub fn create_database_if_needed(room_id: &str) {
let pool = pool_by_room_id(room_id);
2021-03-18 05:04:56 +01:00
let conn = pool.get().unwrap();
2021-03-18 05:24:47 +01:00
create_room_tables_if_needed(&conn);
2021-03-18 05:04:56 +01:00
}
pub fn create_room_tables_if_needed(conn: &DatabaseConnection) {
2021-03-10 05:13:58 +01:00
// Messages
// The `id` field is needed to make `rowid` stable, which is important because otherwise
// the `id`s in this table won't correspond to those in the deleted messages table
2021-03-10 06:29:56 +01:00
let messages_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
2021-03-10 06:29:56 +01:00
id INTEGER PRIMARY KEY,
2021-03-17 23:35:51 +01:00
public_key TEXT,
2021-03-26 00:21:08 +01:00
timestamp INTEGER,
2021-03-22 03:25:14 +01:00
data TEXT,
2021-04-29 03:18:05 +02:00
signature TEXT,
is_deleted INTEGER
2021-03-25 00:56:16 +01:00
)",
MESSAGES_TABLE
);
2021-03-10 06:29:56 +01:00
conn.execute(&messages_table_cmd, params![]).expect("Couldn't create messages table.");
// Deleted messages
let deleted_messages_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
id INTEGER PRIMARY KEY,
deleted_message_id INTEGER
2021-03-25 00:56:16 +01:00
)",
DELETED_MESSAGES_TABLE
);
conn.execute(&deleted_messages_table_cmd, params![])
.expect("Couldn't create deleted messages table.");
2021-03-11 00:06:09 +01:00
// Moderators
let moderators_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
2021-03-11 00:38:02 +01:00
public_key TEXT
2021-03-25 00:56:16 +01:00
)",
MODERATORS_TABLE
);
2021-03-11 00:06:09 +01:00
conn.execute(&moderators_table_cmd, params![]).expect("Couldn't create moderators table.");
2021-03-11 00:38:02 +01:00
// Block list
let block_list_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
2021-03-11 00:38:02 +01:00
public_key TEXT
2021-03-25 00:56:16 +01:00
)",
BLOCK_LIST_TABLE
);
2021-03-11 00:38:02 +01:00
conn.execute(&block_list_table_cmd, params![]).expect("Couldn't create block list table.");
2021-03-17 00:10:26 +01:00
// Pending tokens
2021-03-18 00:06:59 +01:00
// Note that a given public key can have multiple pending tokens
2021-03-17 00:10:26 +01:00
let pending_tokens_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
2021-04-27 03:42:00 +02:00
public_key TEXT,
2021-03-17 00:10:26 +01:00
timestamp INTEGER,
token BLOB
2021-03-25 00:56:16 +01:00
)",
PENDING_TOKENS_TABLE
);
conn.execute(&pending_tokens_table_cmd, params![])
.expect("Couldn't create pending tokens table.");
2021-03-17 00:10:26 +01:00
// Tokens
2021-03-18 03:21:10 +01:00
// The token is stored as hex here (rather than as bytes) because it's more convenient for lookup
2021-03-17 00:10:26 +01:00
let tokens_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
2021-06-11 01:47:34 +02:00
public_key TEXT,
2021-03-18 00:06:59 +01:00
timestamp INTEGER,
2021-06-11 01:47:34 +02:00
token TEXT PRIMARY KEY
2021-03-25 00:56:16 +01:00
)",
TOKENS_TABLE
);
2021-03-17 00:10:26 +01:00
conn.execute(&tokens_table_cmd, params![]).expect("Couldn't create tokens table.");
2021-03-19 00:09:13 +01:00
// Files
let files_table_cmd = format!(
2021-03-25 00:56:16 +01:00
"CREATE TABLE IF NOT EXISTS {} (
2021-04-27 03:42:00 +02:00
id TEXT PRIMARY KEY,
2021-03-19 00:09:13 +01:00
timestamp INTEGER
2021-03-25 00:56:16 +01:00
)",
FILES_TABLE
);
2021-03-19 00:09:13 +01:00
conn.execute(&files_table_cmd, params![]).expect("Couldn't create files table.");
2021-06-11 02:05:00 +02:00
// User activity table
let user_activity_table_cmd = format!(
"CREATE TABLE IF NOT EXISTS {} (
public_key TEXT PRIMARY KEY,
last_active INTEGER NOT NULL
)",
USER_ACTIVITY_TABLE,
);
2021-06-04 07:42:10 +02:00
conn.execute(&user_activity_table_cmd, params![])
.expect("Couldn't create user activity table.");
2021-03-10 05:13:58 +01:00
}
2021-03-17 01:15:06 +01:00
2021-03-18 05:24:47 +01:00
// Pruning
2021-03-18 05:04:56 +01:00
pub async fn prune_tokens_periodically() {
2021-03-18 00:06:59 +01:00
let mut timer = tokio::time::interval(chrono::Duration::minutes(10).to_std().unwrap());
loop {
timer.tick().await;
2021-03-25 00:56:16 +01:00
tokio::spawn(async {
prune_tokens().await;
});
2021-03-18 00:06:59 +01:00
}
}
2021-03-18 05:04:56 +01:00
pub async fn prune_pending_tokens_periodically() {
2021-03-17 01:15:06 +01:00
let mut timer = tokio::time::interval(chrono::Duration::minutes(10).to_std().unwrap());
loop {
timer.tick().await;
2021-03-25 00:56:16 +01:00
tokio::spawn(async {
prune_pending_tokens().await;
});
2021-03-17 01:15:06 +01:00
}
}
2021-03-19 00:09:13 +01:00
pub async fn prune_files_periodically() {
let mut timer = tokio::time::interval(chrono::Duration::days(1).to_std().unwrap());
loop {
timer.tick().await;
2021-03-25 00:56:16 +01:00
tokio::spawn(async {
prune_files(FILE_EXPIRATION).await;
});
2021-03-19 00:09:13 +01:00
}
}
2021-03-18 05:04:56 +01:00
async fn prune_tokens() {
2021-03-31 06:12:32 +02:00
let rooms = match get_all_room_ids() {
2021-03-18 05:35:18 +01:00
Ok(rooms) => rooms,
2021-03-25 01:38:06 +01:00
Err(_) => return,
2021-03-18 00:06:59 +01:00
};
2021-03-18 05:35:18 +01:00
for room in rooms {
2021-03-23 05:39:42 +01:00
let pool = pool_by_room_id(&room);
2021-03-18 05:35:18 +01:00
// It's not catastrophic if we fail to prune the database for a given room
2021-03-19 03:26:53 +01:00
let conn = match pool.get() {
2021-03-18 05:35:18 +01:00
Ok(conn) => conn,
2021-04-01 00:55:47 +02:00
Err(e) => return error!("Couldn't prune tokens due to error: {}.", e),
2021-03-18 05:35:18 +01:00
};
let stmt = format!("DELETE FROM {} WHERE timestamp < (?1)", TOKENS_TABLE);
let now = chrono::Utc::now().timestamp();
let expiration = now - TOKEN_EXPIRATION;
2021-03-25 00:56:16 +01:00
match conn.execute(&stmt, params![expiration]) {
2021-03-18 05:35:18 +01:00
Ok(_) => (),
2021-04-01 00:55:47 +02:00
Err(e) => return error!("Couldn't prune tokens due to error: {}.", e),
2021-03-18 05:35:18 +01:00
};
}
2021-04-01 00:55:47 +02:00
info!("Pruned tokens.");
2021-03-18 00:06:59 +01:00
}
2021-03-18 05:04:56 +01:00
async fn prune_pending_tokens() {
2021-03-31 06:12:32 +02:00
let rooms = match get_all_room_ids() {
2021-03-18 05:35:18 +01:00
Ok(rooms) => rooms,
2021-03-25 01:38:06 +01:00
Err(_) => return,
2021-03-17 05:11:48 +01:00
};
2021-03-18 05:35:18 +01:00
for room in rooms {
2021-03-23 05:39:42 +01:00
let pool = pool_by_room_id(&room);
2021-03-18 05:35:18 +01:00
// It's not catastrophic if we fail to prune the database for a given room
2021-03-19 03:26:53 +01:00
let conn = match pool.get() {
2021-03-18 05:35:18 +01:00
Ok(conn) => conn,
2021-04-01 00:55:47 +02:00
Err(e) => return error!("Couldn't prune pending tokens due to error: {}.", e),
2021-03-18 05:35:18 +01:00
};
let stmt = format!("DELETE FROM {} WHERE timestamp < (?1)", PENDING_TOKENS_TABLE);
let now = chrono::Utc::now().timestamp();
let expiration = now - PENDING_TOKEN_EXPIRATION;
2021-03-25 00:56:16 +01:00
match conn.execute(&stmt, params![expiration]) {
2021-03-18 05:35:18 +01:00
Ok(_) => (),
2021-04-01 00:55:47 +02:00
Err(e) => return error!("Couldn't prune pending tokens due to error: {}.", e),
2021-03-18 05:35:18 +01:00
};
}
2021-04-01 00:55:47 +02:00
info!("Pruned pending tokens.");
2021-03-18 05:35:18 +01:00
}
fn get_expired_file_ids(
pool: &DatabaseConnectionPool, file_expiration: i64,
) -> Result<Vec<String>, ()> {
let now = chrono::Utc::now().timestamp();
let expiration = now - file_expiration;
// Get a database connection and open a transaction
let conn = pool.get().map_err(|e| {
error!("Couldn't get database connection to prune files due to error: {}.", e);
})?;
// Get the IDs of the files to delete
let raw_query = format!("SELECT id FROM {} WHERE timestamp < (?1)", FILES_TABLE);
let mut query = conn.prepare(&raw_query).map_err(|e| {
error!("Couldn't prepare query to prune files due to error: {}.", e);
})?;
let rows = query.query_map(params![expiration], |row| row.get(0)).map_err(|e| {
error!("Couldn't prune files due to error: {} (expiration = {}).", e, expiration);
})?;
Ok(rows.filter_map(|result| result.ok()).collect())
}
pub async fn prune_files_for_room(pool: &DatabaseConnectionPool, room: &str, file_expiration: i64) {
let ids = get_expired_file_ids(&pool, file_expiration);
match ids {
Ok(ids) if !ids.is_empty() => {
2021-03-25 00:56:16 +01:00
// Delete the files
2021-06-10 03:42:45 +02:00
let futs = ids.iter().map(|id| async move {
(
tokio::fs::remove_file(format!("files/{}_files/{}", room, id)).await,
id.to_owned(),
)
});
let results = futures::future::join_all(futs).await;
for (res, id) in results {
if let Err(err) = res {
error!(
"Couldn't delete file: {} from room: {} due to error: {}.",
id, room, err
);
2021-03-23 05:43:57 +01:00
}
2021-03-19 00:09:13 +01:00
}
let conn = match pool.get() {
Ok(conn) => conn,
Err(e) => {
return error!(
"Couldn't get database connection to prune files due to error: {}.",
e
)
}
};
2021-06-10 03:42:45 +02:00
// Measure the time it takes to delete all files sequentially
// (this might become a problem since we're not using an async interface)
let now = std::time::Instant::now();
2021-06-03 02:36:55 +02:00
// Remove the file records from the database
// FIXME: It'd be great to do this in a single statement, but apparently this is not supported very well
2021-06-10 03:42:45 +02:00
for id in ids {
2021-06-03 02:36:55 +02:00
let stmt = format!("DELETE FROM {} WHERE id = (?1)", FILES_TABLE);
match conn.execute(&stmt, params![id]) {
Ok(_) => (),
2021-06-03 03:07:52 +02:00
Err(e) => {
return error!("Couldn't prune file with ID: {} due to error: {}.", id, e)
}
2021-06-03 02:36:55 +02:00
};
}
2021-04-28 04:08:38 +02:00
// Log the result
2021-06-10 03:42:45 +02:00
info!("Pruned files for room: {}. Took: {:?}", room, now.elapsed());
2021-03-19 00:09:13 +01:00
}
Ok(_) => {
// empty
}
Err(_) => {
// It's not catastrophic if we fail to prune the database for a given room
}
2021-03-19 00:09:13 +01:00
}
}
pub async fn prune_files(file_expiration: i64) {
// The expiration setting is passed in for testing purposes
let rooms = match get_all_room_ids() {
Ok(rooms) => rooms,
Err(_) => return,
};
let futs = rooms.into_iter().map(|room| async move {
let pool = pool_by_room_id(&room);
prune_files_for_room(&pool, &room, file_expiration).await;
});
futures::future::join_all(futs).await;
}
2021-04-29 03:24:31 +02:00
// Migration
pub fn perform_migration() {
let rooms = match get_all_room_ids() {
Ok(ids) => ids,
Err(_e) => {
2021-06-04 07:42:10 +02:00
return error!("Couldn't get all room IDs.");
}
};
2021-06-11 02:05:00 +02:00
let create_tokens_table_cmd = format!(
"CREATE TABLE IF NOT EXISTS {} (
public_key TEXT,
timestamp INTEGER,
token TEXT PRIMARY KEY
)",
TOKENS_TABLE
);
let migrations =
Migrations::new(vec![M::up("DROP TABLE tokens"), M::up(&create_tokens_table_cmd)]);
for room in rooms {
create_database_if_needed(&room);
2021-06-11 02:05:00 +02:00
let pool = pool_by_room_id(&room);
let mut conn = pool.get().unwrap();
migrations.to_latest(&mut conn).unwrap();
}
2021-04-29 03:24:31 +02:00
}
// Utilities
2021-03-31 06:12:32 +02:00
fn get_all_room_ids() -> Result<Vec<String>, Error> {
2021-03-18 05:35:18 +01:00
// Get a database connection
let conn = MAIN_POOL.get().map_err(|_| Error::DatabaseFailedInternally)?;
// Query the database
2021-03-19 01:52:18 +01:00
let raw_query = format!("SELECT id FROM {}", MAIN_TABLE);
2021-03-18 05:35:18 +01:00
let mut query = conn.prepare(&raw_query).map_err(|_| Error::DatabaseFailedInternally)?;
2021-05-14 06:22:33 +02:00
let rows = match query.query_map(params![], |row| row.get(0)) {
2021-03-18 05:35:18 +01:00
Ok(rows) => rows,
Err(e) => {
2021-04-01 00:55:47 +02:00
error!("Couldn't query database due to error: {}.", e);
2021-03-18 05:35:18 +01:00
return Err(Error::DatabaseFailedInternally);
}
};
2021-03-23 05:39:42 +01:00
let ids: Vec<String> = rows.filter_map(|result| result.ok()).collect();
2021-03-18 05:53:24 +01:00
// Return
2021-03-23 05:39:42 +01:00
return Ok(ids);
2021-03-25 00:56:16 +01:00
}