Review pass

This commit is contained in:
Andras Schmelczer 2026-09-19 19:51:08 +01:00
commit 99fa298b92

View file

@ -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