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