1use crate::{apply_key_first, DbError, DbOptions, Result};
11use rusqlite::backup::{Backup, StepResult};
12use rusqlite::Connection;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::time::Duration;
16
17static COUNTER: AtomicU64 = AtomicU64::new(0);
18
19pub trait BackupProgress {
20 fn step(&mut self, remaining: i32) -> bool;
22}
23
24pub fn online_backup_to(
27 options: &DbOptions,
28 source: &Connection,
29 target: &Path,
30 mut progress: Option<&mut dyn BackupProgress>,
31) -> Result<()> {
32 let key = options.key_pragma_value()?;
33 let temp = temp_path(target)?;
34 let result = (|| -> Result<()> {
35 let mut dst = Connection::open(&temp)?;
36 apply_key_first(&dst, key.as_ref().map(|k| k.as_str()))?;
37 {
38 let backup = Backup::new(source, &mut dst)?;
39 loop {
40 let step = backup.step(512)?;
41 if let Some(sink) = progress.as_deref_mut() {
42 if !sink.step(backup.progress().remaining) {
43 return Err(DbError::Backup("cancelled".into()));
44 }
45 }
46 match step {
47 StepResult::Done => break,
48 StepResult::More => {}
49 _ => std::thread::sleep(Duration::from_millis(25)),
51 }
52 }
53 }
54 let _ = dst.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()));
56 dst.query_row("PRAGMA journal_mode=DELETE", [], |row| {
57 row.get::<_, String>(0)
58 })?;
59 let report: String = dst.query_row("PRAGMA quick_check", [], |row| row.get(0))?;
60 if !report.trim().eq_ignore_ascii_case("ok") {
61 return Err(DbError::Backup(format!(
62 "quick_check on the backup said: {report}"
63 )));
64 }
65 drop(dst);
66 rightkit_fs::restrict_owner_only(&temp)?;
67 std::fs::OpenOptions::new()
69 .write(true)
70 .open(&temp)
71 .and_then(|f| f.sync_all())
72 .map_err(|e| DbError::Backup(format!("fsync backup: {e}")))?;
73 std::fs::rename(&temp, target)
74 .map_err(|e| DbError::Backup(format!("publish backup: {e}")))?;
75 if let Some(parent) = target.parent().filter(|p| !p.as_os_str().is_empty()) {
76 rightkit_fs::atomic::sync_dir(parent);
77 }
78 Ok(())
79 })();
80 if result.is_err() {
81 remove_with_sidecars(&temp);
82 }
83 result
84}
85
86fn temp_path(target: &Path) -> Result<PathBuf> {
87 let name = target
88 .file_name()
89 .ok_or(DbError::InvalidDatabasePath)?
90 .to_string_lossy();
91 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
92 Ok(target.with_file_name(format!(
93 ".{name}{}{}-{n}",
94 rightkit_fs::atomic::TEMP_MARKER,
95 std::process::id()
96 )))
97}
98
99fn remove_with_sidecars(path: &Path) {
100 let _ = std::fs::remove_file(path);
101 for suffix in ["-wal", "-shm", "-journal"] {
102 let mut name = path.as_os_str().to_os_string();
103 name.push(suffix);
104 let _ = std::fs::remove_file(PathBuf::from(name));
105 }
106}