appcore_sync_sqlite/
backup.rs1use crate::store::normalize_path;
12use crate::{SqliteSyncError, SqliteSyncResult, SqliteSyncStore, SQLITE_SYNC_SCHEMA_V1};
13use rusqlite::backup::Backup;
14use rusqlite::{Connection, OpenFlags};
15use std::fs::{self, File, OpenOptions};
16use std::path::{Path, PathBuf};
17use std::time::Duration;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct SqliteBackupReport {
22 pub schema_version: u32,
24 pub bytes: u64,
26}
27
28impl SqliteSyncStore {
29 pub fn online_backup(
31 &self,
32 destination: impl AsRef<Path>,
33 ) -> SqliteSyncResult<SqliteBackupReport> {
34 let destination = normalize_backup_destination(destination.as_ref())?;
35 let temporary = reserve_temporary(&destination)?;
36 let result = self.copy_backup(&temporary).and_then(|report| {
37 sync_file(&temporary)?;
38 publish_temporary(&temporary, &destination)?;
39 Ok(report)
40 });
41 if result.is_err() {
42 let _ = fs::remove_file(&temporary);
43 }
44 result
45 }
46
47 fn copy_backup(&self, temporary: &Path) -> SqliteSyncResult<SqliteBackupReport> {
48 self.with_connection(|source| {
49 copy_connection(source, temporary, self.config().backup_pages_per_step)
50 })
51 }
52
53 pub fn restore_backup_to_new(
55 backup_path: impl AsRef<Path>,
56 destination: impl AsRef<Path>,
57 ) -> SqliteSyncResult<SqliteBackupReport> {
58 let original_backup_path = backup_path.as_ref();
59 let backup_path = normalize_path(original_backup_path)?;
60 if !backup_path.is_file() {
61 return Err(SqliteSyncError::UnsafePath);
62 }
63 let destination = normalize_backup_destination(destination.as_ref())?;
64 let source = Connection::open_with_flags(
65 &backup_path,
66 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
67 )
68 .map_err(SqliteSyncError::database)?;
69 validate_backup_connection(&source)?;
70 let temporary = reserve_temporary(&destination)?;
71 let result = copy_connection(&source, &temporary, 128).and_then(|report| {
72 sync_file(&temporary)?;
73 publish_temporary(&temporary, &destination)?;
74 Ok(report)
75 });
76 if result.is_err() {
77 let _ = fs::remove_file(&temporary);
78 }
79 result
80 }
81}
82
83fn copy_connection(
84 source: &Connection,
85 temporary: &Path,
86 pages_per_step: i32,
87) -> SqliteSyncResult<SqliteBackupReport> {
88 let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_FULL_MUTEX;
89 let mut destination =
90 Connection::open_with_flags(temporary, flags).map_err(SqliteSyncError::database)?;
91 let backup = Backup::new(source, &mut destination).map_err(SqliteSyncError::database)?;
92 backup
93 .run_to_completion(pages_per_step, Duration::from_millis(2), None)
94 .map_err(SqliteSyncError::database)?;
95 drop(backup);
96 validate_backup_connection(&destination)?;
97 drop(destination);
98 let bytes = fs::metadata(temporary)
99 .map_err(|_| SqliteSyncError::DatabaseOperation)?
100 .len();
101 Ok(SqliteBackupReport {
102 schema_version: SQLITE_SYNC_SCHEMA_V1,
103 bytes,
104 })
105}
106
107fn normalize_backup_destination(path: &Path) -> SqliteSyncResult<PathBuf> {
108 if path.as_os_str().is_empty() {
109 return Err(SqliteSyncError::UnsafePath);
110 }
111 let normalized = normalize_path(path)?;
112 if normalized.exists() {
113 return Err(SqliteSyncError::UnsafePath);
114 }
115 Ok(normalized)
116}
117
118fn reserve_temporary(destination: &Path) -> SqliteSyncResult<PathBuf> {
119 let parent = destination.parent().unwrap_or_else(|| Path::new("."));
120 let name = destination
121 .file_name()
122 .and_then(|value| value.to_str())
123 .ok_or(SqliteSyncError::UnsafePath)?;
124 for attempt in 0..16u8 {
125 let candidate = parent.join(format!(".{name}.{}-{attempt}.tmp", std::process::id()));
126 match OpenOptions::new()
127 .write(true)
128 .create_new(true)
129 .open(&candidate)
130 {
131 Ok(file) => {
132 drop(file);
133 return Ok(candidate);
134 }
135 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
136 Err(_) => return Err(SqliteSyncError::DatabaseOperation),
137 }
138 }
139 Err(SqliteSyncError::CapacityExceeded("backup temporary"))
140}
141
142fn publish_temporary(temporary: &Path, destination: &Path) -> SqliteSyncResult<()> {
143 fs::hard_link(temporary, destination).map_err(|_| SqliteSyncError::DatabaseOperation)?;
144 if fs::remove_file(temporary).is_err() {
145 let _ = fs::remove_file(destination);
146 return Err(SqliteSyncError::DatabaseOperation);
147 }
148 if sync_parent(destination).is_err() {
149 let _ = fs::remove_file(destination);
150 let _ = sync_parent(destination);
151 return Err(SqliteSyncError::DatabaseOperation);
152 }
153 Ok(())
154}
155
156fn validate_backup_connection(connection: &Connection) -> SqliteSyncResult<()> {
157 let version: u32 = connection
158 .pragma_query_value(None, "user_version", |row| row.get(0))
159 .map_err(SqliteSyncError::database)?;
160 let integrity: String = connection
161 .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
162 .map_err(SqliteSyncError::database)?;
163 if version == SQLITE_SYNC_SCHEMA_V1 && integrity == "ok" {
164 Ok(())
165 } else if version != SQLITE_SYNC_SCHEMA_V1 {
166 Err(SqliteSyncError::UpdateRequired)
167 } else {
168 Err(SqliteSyncError::IntegrityFailed)
169 }
170}
171
172fn sync_file(path: &Path) -> SqliteSyncResult<()> {
173 File::open(path)
174 .and_then(|file| file.sync_all())
175 .map_err(|_| SqliteSyncError::DatabaseOperation)
176}
177
178#[cfg(unix)]
179fn sync_parent(path: &Path) -> SqliteSyncResult<()> {
180 let parent = path.parent().unwrap_or_else(|| Path::new("."));
181 File::open(parent)
182 .and_then(|directory| directory.sync_all())
183 .map_err(|_| SqliteSyncError::DatabaseOperation)
184}
185
186#[cfg(not(unix))]
187fn sync_parent(_path: &Path) -> SqliteSyncResult<()> {
188 Ok(())
189}