This commit is contained in:
Andras Schmelczer 2026-04-23 20:35:42 +01:00
parent 6a8c7635f1
commit d715d94b6d
26 changed files with 1007 additions and 453 deletions

View file

@ -1,5 +1,9 @@
use core::time::Duration;
use std::{collections::HashMap, sync::Arc, sync::atomic::{AtomicU64, Ordering}};
use std::{
collections::HashMap,
sync::Arc,
sync::atomic::{AtomicU64, Ordering},
};
use anyhow::{Context as _, Result};
use log::info;
@ -96,21 +100,21 @@ pub struct WriteTransaction {
}
impl WriteTransaction {
async fn new(pool: &Pool<Sqlite>, write_guard: tokio::sync::OwnedMutexGuard<()>) -> Result<Self> {
async fn new(
pool: &Pool<Sqlite>,
write_guard: tokio::sync::OwnedMutexGuard<()>,
) -> Result<Self> {
let mut conn = pool
.acquire()
.await
.context("Cannot acquire connection for write transaction")?;
if let Err(e) = sqlx::query("BEGIN IMMEDIATE")
.execute(&mut *conn)
.await
{
if let Err(e) = sqlx::query("BEGIN IMMEDIATE").execute(&mut *conn).await {
let is_busy = match &e {
sqlx::Error::Database(db_err) => {
// SQLITE_BUSY base code is 5. Extended codes share base 5.
let busy_by_code = db_err.code().is_some_and(|c| {
c.parse::<u32>().is_ok_and(|n| n & 0xFF == 5)
});
let busy_by_code = db_err
.code()
.is_some_and(|c| c.parse::<u32>().is_ok_and(|n| n & 0xFF == 5));
busy_by_code || db_err.message().contains("database is locked")
}
_ => false,
@ -120,7 +124,10 @@ impl WriteTransaction {
}
return Err(e).context("Cannot begin immediate transaction");
}
Ok(Self { conn: Some(conn), _write_guard: write_guard })
Ok(Self {
conn: Some(conn),
_write_guard: write_guard,
})
}
pub async fn commit(mut self) -> Result<()> {
@ -215,10 +222,7 @@ impl Database {
Ok(vaults)
}
pub async fn get_vault_stats(
&self,
vault: &VaultId,
) -> Result<models::VaultStats> {
pub async fn get_vault_stats(&self, vault: &VaultId) -> Result<models::VaultStats> {
let pool = self.get_connection_pool(vault).await?;
let row = sqlx::query!(
r#"
@ -295,10 +299,7 @@ impl Database {
Ok(database)
}
async fn create_vault_database(
config: &DatabaseConfig,
vault: &VaultId,
) -> Result<VaultPools> {
async fn create_vault_database(config: &DatabaseConfig, vault: &VaultId) -> Result<VaultPools> {
let file_name = config
.databases_directory_path
.join(format!("{vault}.sqlite"));
@ -384,7 +385,6 @@ impl Database {
Ok(VaultPools { reader, writer })
}
fn validate_vault_id(vault: &VaultId) -> Result<()> {
if vault.is_empty() {
anyhow::bail!("Vault ID must not be empty");
@ -427,12 +427,12 @@ impl Database {
let vault_clone = vault.clone();
let pools = vault_pool
.cell
.get_or_try_init(|| async {
Self::create_vault_database(&config, &vault_clone).await
})
.get_or_try_init(|| async { Self::create_vault_database(&config, &vault_clone).await })
.await?;
vault_pool.last_accessed_ms.store(self.now_ms(), Ordering::Relaxed);
vault_pool
.last_accessed_ms
.store(self.now_ms(), Ordering::Relaxed);
Ok(pools.clone())
}
@ -739,9 +739,6 @@ impl Database {
.await
.context("Failed to commit transaction")?;
// Both sends are synchronous: there's no `.await` between the
// `commit()` above and function return, so a task cancellation
// can't drop the broadcast and leave peers permanently behind.
if broadcast.content_changed {
// Content events are filtered out for the origin device — the
// origin already has the content (or learns about the merge
@ -945,7 +942,11 @@ impl Database {
let closures: Vec<_> = idle_pools
.into_iter()
.filter_map(|(vault_id, vault_pool)| {
vault_pool.cell.get().cloned().map(|pools| (vault_id, pools))
vault_pool
.cell
.get()
.cloned()
.map(|pools| (vault_id, pools))
})
.collect();
@ -958,8 +959,7 @@ impl Database {
let writer_clone = pools.writer.clone();
let ckpt_result = tokio::task::spawn_blocking(move || {
futures::executor::block_on(
sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)")
.execute(&writer_clone),
sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)").execute(&writer_clone),
)
})
.await;

View file

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::app_state::database::models::{
DeviceId, DocumentId, DocumentVersionWithoutContent, VaultUpdateId,
DeviceId, DocumentId, DocumentVersionWithoutContent, UserId, VaultUpdateId,
};
#[derive(TS, Deserialize, Clone, Debug)]
@ -22,6 +22,7 @@ pub struct CursorPositionFromClient {
}
#[derive(TS, Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DocumentWithCursors {
// It's None in case the document is dirty.
// We still want to sync the cursor to mark

View file

@ -7,7 +7,7 @@ use axum_extra::TypedHeader;
use log::{debug, info};
use serde::Deserialize;
use super::{device_id_header::DeviceIdHeader, requests::DeleteDocumentVersion};
use super::device_id_header::DeviceIdHeader;
use crate::{
app_state::{
AppState,
@ -38,7 +38,6 @@ pub async fn delete_document(
Extension(user): Extension<User>,
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
State(state): State<AppState>,
Json(_request): Json<DeleteDocumentVersion>,
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
debug!("Deleting document `{document_id}` in vault `{vault_id}`");

View file

@ -41,5 +41,3 @@ pub struct UpdateTextDocumentVersion {
pub content: Vec<NumberOrText>,
}
#[derive(Debug, Deserialize)]
pub struct DeleteDocumentVersion {}

View file

@ -1,9 +1,20 @@
use anyhow::{Result, ensure};
use crate::consts::MAX_RELATIVE_PATH_LEN;
/// Sanitize the document's path to allow all clients to create the same path in
/// their filesystem. If we didn't do this server-side, client's would need to
/// deal with mapping invalid names to valid ones and then back.
pub fn sanitize_path(path: &str) -> Result<String> {
// Enforce the length cap at the single chokepoint every create/update
// handler goes through, so clients can't blow up axum's JSON/multipart
// parser with a 1 MB `relative_path` before the handler ever runs.
// The WebSocket cursor handler enforces this separately.
ensure!(
path.len() <= MAX_RELATIVE_PATH_LEN,
"Relative path exceeds the maximum length of {MAX_RELATIVE_PATH_LEN} bytes"
);
let options = sanitize_filename::Options {
truncate: true,
windows: true, // Windows is the lowest common denominator