From 2ffa7187ea1fb55f92d7407aede737e7098df01c Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Wed, 14 Oct 2026 16:54:27 +0100 Subject: [PATCH] fix: nonfatal app db backup fail --- packages/app-lib/src/state/db.rs | 8 +++- packages/app-lib/src/state/db_backup.rs | 52 +++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index 1f2a99f52b..5318c1f21b 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -22,7 +22,13 @@ pub(crate) async fn connect( } async fn connect_app_db(db_path: &Path) -> crate::Result> { - super::db_backup::maybe_backup_existing_app_db(db_path).await?; + if let Err(err) = + super::db_backup::maybe_backup_existing_app_db(db_path).await + { + tracing::warn!( + "Continuing without a pre-migration app database backup: {err}" + ); + } open_migrated_app_db(db_path).await } diff --git a/packages/app-lib/src/state/db_backup.rs b/packages/app-lib/src/state/db_backup.rs index 9bff835fb2..29bf3af6f4 100644 --- a/packages/app-lib/src/state/db_backup.rs +++ b/packages/app-lib/src/state/db_backup.rs @@ -1,5 +1,5 @@ -use sqlx::ConnectOptions; use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection}; +use sqlx::{ConnectOptions, Connection}; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -99,10 +99,20 @@ pub(crate) async fn maybe_backup_existing_app_db( if let Err(err) = create_sqlite_snapshot(&mut conn, &backup_path).await { tracing::error!( - "Failed to create pre-migration app database backup at {}: {err}", + "Failed to create pre-migration app database backup at {}, falling back to copying the database files: {err}", backup_path.display() ); - return Err(err); + remove_partial_backup(&backup_path).await; + let _ = conn.close().await; + + if let Err(copy_err) = copy_db_files(db_path, &backup_path).await { + tracing::error!( + "Failed to copy app database files to {}: {copy_err}", + backup_path.display() + ); + remove_partial_backup(&backup_path).await; + return Err(copy_err); + } } tracing::info!( @@ -113,6 +123,42 @@ pub(crate) async fn maybe_backup_existing_app_db( Ok(()) } +/// Copies the database file and its WAL byte-for-byte. Used when `VACUUM INTO` +/// fails, which usually means the database is corrupt and the raw files are +/// what's needed to recover it. +async fn copy_db_files( + db_path: &Path, + backup_path: &Path, +) -> crate::Result<()> { + crate::util::io::copy(db_path, backup_path).await?; + + let db_wal_path = wal_path(db_path); + if db_wal_path.try_exists()? { + crate::util::io::copy(&db_wal_path, wal_path(backup_path)).await?; + } + + Ok(()) +} + +async fn remove_partial_backup(backup_path: &Path) { + for path in [backup_path.to_path_buf(), wal_path(backup_path)] { + if let Err(err) = crate::util::io::remove_file(&path).await + && err.kind() != std::io::ErrorKind::NotFound + { + tracing::warn!( + "Failed to remove partial app database backup at {}: {err}", + path.display() + ); + } + } +} + +fn wal_path(db_path: &Path) -> PathBuf { + let mut path = db_path.as_os_str().to_owned(); + path.push("-wal"); + PathBuf::from(path) +} + async fn open_read_only_db(db_path: &Path) -> crate::Result { let conn_options = SqliteConnectOptions::new() .filename(db_path)