use crate::{apply_key_first, DbError, DbOptions, Result};
use rusqlite::backup::{Backup, StepResult};
use rusqlite::Connection;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
static COUNTER: AtomicU64 = AtomicU64::new(0);
pub trait BackupProgress {
fn step(&mut self, remaining: i32) -> bool;
}
pub fn online_backup_to(
options: &DbOptions,
source: &Connection,
target: &Path,
mut progress: Option<&mut dyn BackupProgress>,
) -> Result<()> {
let key = options.key_pragma_value()?;
let temp = temp_path(target)?;
let result = (|| -> Result<()> {
let mut dst = Connection::open(&temp)?;
dst.busy_timeout(options.busy_timeout)?;
apply_key_first(&dst, key.as_ref().map(|k| k.as_str()))?;
{
let backup = Backup::new(source, &mut dst)?;
loop {
let step = backup.step(512)?;
if let Some(sink) = progress.as_deref_mut() {
if !sink.step(backup.progress().remaining) {
return Err(DbError::Backup("cancelled".into()));
}
}
match step {
StepResult::Done => break,
StepResult::More => {}
_ => std::thread::sleep(Duration::from_millis(25)),
}
}
}
let _ = dst.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()));
dst.query_row("PRAGMA journal_mode=DELETE", [], |row| {
row.get::<_, String>(0)
})?;
let report: String = dst.query_row("PRAGMA quick_check", [], |row| row.get(0))?;
if !report.trim().eq_ignore_ascii_case("ok") {
return Err(DbError::Backup(format!(
"quick_check on the backup said: {report}"
)));
}
drop(dst);
rightkit_fs::restrict_owner_only(&temp)?;
std::fs::OpenOptions::new()
.write(true)
.open(&temp)
.and_then(|f| f.sync_all())
.map_err(|e| DbError::Backup(format!("fsync backup: {e}")))?;
std::fs::rename(&temp, target)
.map_err(|e| DbError::Backup(format!("publish backup: {e}")))?;
if let Some(parent) = target.parent().filter(|p| !p.as_os_str().is_empty()) {
rightkit_fs::atomic::sync_dir(parent);
}
Ok(())
})();
if result.is_err() {
remove_with_sidecars(&temp);
}
result
}
pub fn restore_from(
options: &DbOptions,
destination: &mut Connection,
backup_path: &Path,
) -> Result<()> {
let source = validated_backup(options, backup_path)?;
restore_validated(options, &source, destination)
}
impl DbOptions {
pub fn restore_from(&self, backup_path: impl AsRef<Path>) -> Result<()> {
let source = validated_backup(self, backup_path.as_ref())?;
let mut destination = self.open_existing_writer()?;
restore_validated(self, &source, &mut destination)
}
}
fn validated_backup(options: &DbOptions, path: &Path) -> Result<Connection> {
let source = options.open_existing_at(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
source.execute_batch("BEGIN;")?;
let messages = {
let mut statement = source.prepare("PRAGMA integrity_check")?;
let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
rows.collect::<rusqlite::Result<Vec<_>>>()?
};
if messages.is_empty()
|| messages
.iter()
.any(|message| !message.trim().eq_ignore_ascii_case("ok"))
{
return Err(DbError::Integrity(messages.join("; ")));
}
Ok(source)
}
fn restore_validated(
options: &DbOptions,
source: &Connection,
destination: &mut Connection,
) -> Result<()> {
let backup = Backup::new(source, destination)?;
let started = Instant::now();
loop {
match backup.step(512)? {
StepResult::Done => return Ok(()),
StepResult::More => {}
StepResult::Busy | StepResult::Locked => {
if started.elapsed() >= options.busy_timeout {
return Err(DbError::Backup("restore busy timeout expired".into()));
}
std::thread::sleep(Duration::from_millis(25));
}
_ => return Err(DbError::Backup("unexpected restore step result".into())),
}
}
}
fn temp_path(target: &Path) -> Result<PathBuf> {
let name = target
.file_name()
.ok_or(DbError::InvalidDatabasePath)?
.to_string_lossy();
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
Ok(target.with_file_name(format!(
".{name}{}{}-{n}",
rightkit_fs::atomic::TEMP_MARKER,
std::process::id()
)))
}
fn remove_with_sidecars(path: &Path) {
let _ = std::fs::remove_file(path);
for suffix in ["-wal", "-shm", "-journal"] {
let mut name = path.as_os_str().to_os_string();
name.push(suffix);
let _ = std::fs::remove_file(PathBuf::from(name));
}
}