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, Instant};
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(
36 options: &DbOptions,
37 source: &Connection,
38 target: &Path,
39 mut progress: Option<&mut dyn BackupProgress>,
40) -> Result<()> {
41 let started = Instant::now();
42 let deadline = started.checked_add(options.busy_timeout).unwrap_or(started);
43 let check_deadline = || -> Result<()> {
44 if Instant::now() >= deadline {
45 Err(DbError::Backup("backup operation timeout expired".into()))
46 } else {
47 Ok(())
48 }
49 };
50 let key = options.key_pragma_value()?;
52 check_deadline()?;
53 let (temp, created) = create_private_staging(target)?;
54 let result = (|| -> Result<()> {
55 if let Some(want) = created {
57 if rightkit_fs::FileKey::of_path(&temp).ok() != Some(want) {
58 return Err(DbError::Backup(
59 "backup staging was replaced before it was opened".into(),
60 ));
61 }
62 }
63 let mut dst = Connection::open(&temp)?;
64 dst.busy_timeout(Duration::ZERO)?;
66 apply_key_first(&dst, key.as_ref().map(|k| k.as_str()))?;
67 {
68 let backup = Backup::new(source, &mut dst)?;
69 loop {
70 check_deadline()?;
71 let step = backup.step(512)?;
72 check_deadline()?;
73 if let Some(sink) = progress.as_deref_mut() {
74 if !sink.step(backup.progress().remaining) {
75 return Err(DbError::Backup("cancelled".into()));
76 }
77 }
78 check_deadline()?;
79 match step {
80 StepResult::Done => break,
81 StepResult::More => {}
82 _ => {
84 let now = Instant::now();
85 let remaining = deadline.saturating_duration_since(now);
86 if remaining.is_zero() {
87 return Err(DbError::Backup("backup busy timeout expired".into()));
88 }
89 std::thread::sleep(remaining.min(Duration::from_millis(25)));
90 }
91 }
92 }
93 }
94 check_deadline()?;
95 let _ = dst.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(()));
97 dst.query_row("PRAGMA journal_mode=DELETE", [], |row| {
98 row.get::<_, String>(0)
99 })?;
100 let report: String = dst.query_row("PRAGMA quick_check", [], |row| row.get(0))?;
101 if !report.trim().eq_ignore_ascii_case("ok") {
102 return Err(DbError::Backup(format!(
103 "quick_check on the backup said: {report}"
104 )));
105 }
106 drop(dst);
107 rightkit_fs::restrict_owner_only(&temp)?;
108 std::fs::OpenOptions::new()
110 .write(true)
111 .open(&temp)
112 .and_then(|f| f.sync_all())
113 .map_err(|e| DbError::Backup(format!("fsync backup: {e}")))?;
114 check_deadline()?;
115 std::fs::rename(&temp, target)
116 .map_err(|e| DbError::Backup(format!("publish backup: {e}")))?;
117 if let Some(parent) = target.parent().filter(|p| !p.as_os_str().is_empty()) {
118 rightkit_fs::atomic::sync_dir(parent);
119 }
120 Ok(())
121 })();
122 if result.is_err() {
123 remove_owned_staging(&temp, created);
124 }
125 result
126}
127
128fn create_private_staging(target: &Path) -> Result<(PathBuf, Option<rightkit_fs::FileKey>)> {
132 for _ in 0..32 {
133 let temp = temp_path(target)?;
134 match rightkit_fs::create_private_new(&temp) {
135 Ok(file) => {
136 let key = rightkit_fs::FileKey::of_file(&file, &temp).ok();
137 drop(file);
138 return Ok((temp, key));
139 }
140 Err(error) if error.kind() == Some(std::io::ErrorKind::AlreadyExists) => {}
141 Err(error) => return Err(error.into()),
142 }
143 }
144 Err(DbError::Backup(
145 "could not create a unique backup staging file".into(),
146 ))
147}
148
149fn remove_owned_staging(temp: &Path, created: Option<rightkit_fs::FileKey>) {
153 if let Some(want) = created {
154 match rightkit_fs::FileKey::of_path(temp) {
155 Ok(now) if now != want => return,
156 _ => {}
157 }
158 }
159 remove_with_sidecars(temp);
160}
161
162pub fn restore_from(
167 options: &DbOptions,
168 destination: &mut Connection,
169 backup_path: &Path,
170) -> Result<()> {
171 let source = validated_backup(options, backup_path)?;
172 restore_validated(options, &source, destination)
173}
174
175impl DbOptions {
176 pub fn restore_from(&self, backup_path: impl AsRef<Path>) -> Result<()> {
180 let source = validated_backup(self, backup_path.as_ref())?;
181 let mut destination = self.open_existing_writer()?;
182 restore_validated(self, &source, &mut destination)
183 }
184}
185
186pub(crate) fn validated_backup(options: &DbOptions, path: &Path) -> Result<Connection> {
187 let source = options.open_existing_at(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
188 source.execute_batch("BEGIN;")?;
190 let messages = {
191 let mut statement = source.prepare("PRAGMA integrity_check")?;
192 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
193 rows.collect::<rusqlite::Result<Vec<_>>>()?
194 };
195 if messages.is_empty()
196 || messages
197 .iter()
198 .any(|message| !message.trim().eq_ignore_ascii_case("ok"))
199 {
200 return Err(DbError::Integrity(messages.join("; ")));
201 }
202 Ok(source)
203}
204
205fn restore_validated(
206 options: &DbOptions,
207 source: &Connection,
208 destination: &mut Connection,
209) -> Result<()> {
210 let backup = Backup::new(source, destination)?;
211 let started = Instant::now();
212 let deadline = started.checked_add(options.busy_timeout);
213 loop {
214 match backup.step(512)? {
215 StepResult::Done => return Ok(()),
216 StepResult::More => {}
217 StepResult::Busy | StepResult::Locked => {
218 let Some(deadline) = deadline else {
219 std::thread::sleep(Duration::from_millis(25));
220 continue;
221 };
222 let remaining = deadline.saturating_duration_since(Instant::now());
223 if remaining.is_zero() {
224 return Err(DbError::Backup("restore busy timeout expired".into()));
225 }
226 std::thread::sleep(remaining.min(Duration::from_millis(25)));
227 }
228 _ => return Err(DbError::Backup("unexpected restore step result".into())),
229 }
230 }
231}
232
233fn temp_path(target: &Path) -> Result<PathBuf> {
234 let name = target
235 .file_name()
236 .ok_or(DbError::InvalidDatabasePath)?
237 .to_string_lossy();
238 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
239 Ok(target.with_file_name(format!(
240 ".{name}{}{}-{n}",
241 rightkit_fs::atomic::TEMP_MARKER,
242 std::process::id()
243 )))
244}
245
246fn remove_with_sidecars(path: &Path) {
247 let _ = remove_database_files(path);
248}
249
250pub fn remove_database_files(path: &Path) -> std::io::Result<()> {
257 let mut first_error = None;
258 let mut remove = |p: &Path| {
259 if let Err(e) = std::fs::remove_file(p) {
260 if e.kind() != std::io::ErrorKind::NotFound && first_error.is_none() {
261 first_error = Some(e);
262 }
263 }
264 };
265 remove(path);
266 for suffix in ["-wal", "-shm", "-journal"] {
267 let mut name = path.as_os_str().to_os_string();
268 name.push(suffix);
269 remove(&PathBuf::from(name));
270 }
271 first_error.map_or(Ok(()), Err)
272}
273
274pub fn store_lock_path(db_path: &Path) -> PathBuf {
277 let mut name = db_path.as_os_str().to_os_string();
278 name.push(".store-lock");
279 PathBuf::from(name)
280}
281
282#[derive(Debug)]
289pub struct StoreLock {
290 lock: rightkit_fs::FileLock,
291}
292
293impl StoreLock {
294 pub fn shared(db_path: &Path, options: &rightkit_fs::LockOptions) -> Result<Self> {
297 let lock = rightkit_fs::FileLock::acquire_shared(&store_lock_path(db_path), options)?;
298 Ok(Self { lock })
299 }
300
301 pub fn exclusive(db_path: &Path, options: &rightkit_fs::LockOptions) -> Result<Self> {
303 let lock = rightkit_fs::FileLock::acquire(&store_lock_path(db_path), options)?;
304 Ok(Self { lock })
305 }
306
307 pub fn path(&self) -> &Path {
309 self.lock.path()
310 }
311}
312
313pub fn replace_database(
334 options: &DbOptions,
335 replacement: &Path,
336 lock: &rightkit_fs::LockOptions,
337) -> Result<()> {
338 let _guard = StoreLock::exclusive(&options.path, lock)?;
339 replace_database_locked(options, replacement)
340}
341
342pub fn replace_database_locked(options: &DbOptions, replacement: &Path) -> Result<()> {
345 let target = options.path.as_path();
346 let source = validated_backup(options, replacement)?;
347 let staging = temp_path(target)?;
348 let staged = (|| -> Result<()> {
349 online_backup_to(options, &source, &staging, None)?;
350 drop(source);
351 remove_database_files_except_main(target)
352 .map_err(|e| DbError::Backup(format!("clear live sidecars: {e}")))?;
353 std::fs::rename(&staging, target)
354 .map_err(|e| DbError::Backup(format!("publish replacement: {e}")))?;
355 if let Some(parent) = target.parent().filter(|p| !p.as_os_str().is_empty()) {
356 rightkit_fs::atomic::sync_dir(parent);
357 }
358 Ok(())
359 })();
360 if staged.is_err() {
361 let _ = std::fs::remove_file(&staging);
362 }
363 staged
364}
365
366fn remove_database_files_except_main(path: &Path) -> std::io::Result<()> {
367 let mut first_error = None;
368 for suffix in ["-wal", "-shm", "-journal"] {
369 let mut name = path.as_os_str().to_os_string();
370 name.push(suffix);
371 if let Err(e) = std::fs::remove_file(PathBuf::from(name)) {
372 if e.kind() != std::io::ErrorKind::NotFound && first_error.is_none() {
373 first_error = Some(e);
374 }
375 }
376 }
377 first_error.map_or(Ok(()), Err)
378}