Review pass
This commit is contained in:
parent
2a05930ae7
commit
99fa298b92
1 changed files with 621 additions and 94 deletions
|
|
@ -1,9 +1,14 @@
|
|||
use core::time::Duration;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use log::info;
|
||||
use models::VaultId;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{ConnectOptions, sqlite::SqliteConnectOptions};
|
||||
use sqlx::{Pool, Sqlite, sqlite::SqlitePoolOptions};
|
||||
use tokio::sync::Mutex;
|
||||
|
|
@ -14,7 +19,15 @@ pub mod models;
|
|||
mod mutations;
|
||||
mod queries;
|
||||
|
||||
use crate::config::database_config::DatabaseConfig;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::{
|
||||
config::database_config::DatabaseConfig,
|
||||
utils::normalize_vault_id::{normalize_string, validate_vault_id},
|
||||
};
|
||||
|
||||
const VAULT_DATABASE_DIRECTORY: &str = "vaults";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PoolWithTimestamp {
|
||||
|
|
@ -41,7 +54,10 @@ pub type Transaction<'a> = sqlx::Transaction<'a, Sqlite>;
|
|||
|
||||
impl Database {
|
||||
pub async fn try_new(config: &DatabaseConfig) -> Result<Self> {
|
||||
tokio::fs::create_dir_all(&config.databases_directory_path)
|
||||
let vault_directory = config
|
||||
.databases_directory_path
|
||||
.join(VAULT_DATABASE_DIRECTORY);
|
||||
tokio::fs::create_dir_all(&vault_directory)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
|
|
@ -50,34 +66,25 @@ impl Database {
|
|||
)
|
||||
})?;
|
||||
|
||||
let mut connection_pools = std::collections::HashMap::new();
|
||||
|
||||
info!("Applying pending database migrations");
|
||||
let mut entries = tokio::fs::read_dir(&config.databases_directory_path).await?;
|
||||
|
||||
// Filenames encode only a digest, so their vault names cannot be
|
||||
// recovered here. Check migrations now and open pools lazily by name.
|
||||
let mut entries = tokio::fs::read_dir(&vault_directory).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if !entry.file_name().to_string_lossy().ends_with(".sqlite") {
|
||||
continue;
|
||||
if entry.file_type().await?.is_file()
|
||||
&& entry.path().extension().is_some_and(|ext| ext == "sqlite")
|
||||
{
|
||||
Self::open_database(config, &entry.path())
|
||||
.await?
|
||||
.close()
|
||||
.await;
|
||||
}
|
||||
|
||||
let vault: VaultId = entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.trim_end_matches(".sqlite")
|
||||
.to_owned();
|
||||
|
||||
let pool = Self::create_vault_database(config, &vault).await?;
|
||||
connection_pools.insert(
|
||||
vault.clone(),
|
||||
PoolWithTimestamp {
|
||||
pool,
|
||||
last_accessed: Instant::now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let database = Self {
|
||||
config: config.clone(),
|
||||
connection_pools: Arc::new(Mutex::new(connection_pools)),
|
||||
connection_pools: Arc::default(),
|
||||
};
|
||||
|
||||
// Start background task to cleanup idle connection pools
|
||||
|
|
@ -86,16 +93,21 @@ impl Database {
|
|||
Ok(database)
|
||||
}
|
||||
|
||||
async fn create_vault_database(
|
||||
config: &DatabaseConfig,
|
||||
vault: &VaultId,
|
||||
) -> Result<Pool<Sqlite>> {
|
||||
let file_name = config
|
||||
fn database_path(config: &DatabaseConfig, vault: &str) -> PathBuf {
|
||||
// Hash the normalized vault name into a fixed-length ASCII filename so
|
||||
// filesystem case/Unicode aliasing (e.g. composed vs. decomposed é on
|
||||
// macOS) cannot make separately authorized vaults share a database.
|
||||
// This is only a storage filename; the user-facing vault name is unchanged.
|
||||
let digest = Sha256::digest(vault.as_bytes());
|
||||
config
|
||||
.databases_directory_path
|
||||
.join(format!("{vault}.sqlite"));
|
||||
.join(VAULT_DATABASE_DIRECTORY)
|
||||
.join(format!("{digest:x}.sqlite"))
|
||||
}
|
||||
|
||||
async fn open_database(config: &DatabaseConfig, file_name: &Path) -> Result<Pool<Sqlite>> {
|
||||
let connection_options = SqliteConnectOptions::new()
|
||||
.filename(file_name.clone())
|
||||
.filename(file_name)
|
||||
.create_if_missing(true)
|
||||
.auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Full)
|
||||
.busy_timeout(Duration::from_secs(3600))
|
||||
|
|
@ -126,10 +138,13 @@ impl Database {
|
|||
}
|
||||
|
||||
async fn get_connection_pool(&self, vault: &VaultId) -> Result<Pool<Sqlite>> {
|
||||
let vault = normalize_string(vault);
|
||||
validate_vault_id(&vault)?;
|
||||
let mut pools = self.connection_pools.lock().await;
|
||||
|
||||
if !pools.contains_key(vault) {
|
||||
let pool = Self::create_vault_database(&self.config, vault).await?;
|
||||
if !pools.contains_key(&vault) {
|
||||
let file_name = Self::database_path(&self.config, &vault);
|
||||
let pool = Self::open_database(&self.config, &file_name).await?;
|
||||
pools.insert(
|
||||
vault.clone(),
|
||||
PoolWithTimestamp {
|
||||
|
|
@ -140,7 +155,7 @@ impl Database {
|
|||
}
|
||||
|
||||
let pool_with_timestamp = pools
|
||||
.get_mut(vault)
|
||||
.get_mut(&vault)
|
||||
.expect("Pool was just inserted or already exists");
|
||||
|
||||
// Update last accessed time
|
||||
|
|
|
|||
Loading…
Reference in a new issue