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,
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 integrity: String = connection
168 .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
169 .map_err(SqliteSyncError::database)?;
170 if matches!(version, SQLITE_SYNC_SCHEMA_V1 | SQLITE_SYNC_SCHEMA_V2) && integrity == "ok" {
171 Ok(version)
172 } else if !matches!(version, SQLITE_SYNC_SCHEMA_V1 | SQLITE_SYNC_SCHEMA_V2) {
173 Err(SqliteSyncError::UpdateRequired)
174 } else {
175 Err(SqliteSyncError::IntegrityFailed)
176 }
177}
178
179fn sync_file(path: &Path) -> SqliteSyncResult<()> {
180 File::open(path)
181 .and_then(|file| file.sync_all())
182 .map_err(|_| SqliteSyncError::DatabaseOperation)
183}
184
185#[cfg(unix)]
186fn sync_parent(path: &Path) -> SqliteSyncResult<()> {
187 let parent = path.parent().unwrap_or_else(|| Path::new("."));
188 File::open(parent)
189 .and_then(|directory| directory.sync_all())
190 .map_err(|_| SqliteSyncError::DatabaseOperation)
191}
192
193#[cfg(not(unix))]
194fn sync_parent(_path: &Path) -> SqliteSyncResult<()> {
195 Ok(())
196}