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