Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/app-lib/src/state/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ pub(crate) async fn connect(
}

async fn connect_app_db(db_path: &Path) -> crate::Result<Pool<Sqlite>> {
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
}

Expand Down
52 changes: 49 additions & 3 deletions packages/app-lib/src/state/db_backup.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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!(
Expand All @@ -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<SqliteConnection> {
let conn_options = SqliteConnectOptions::new()
.filename(db_path)
Expand Down
Loading