Skip to main content

appcore_sync_sqlite/
backup.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: backup.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/26 00:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/26 00:00:00 by dnettoRaw
8//      ###########      S: 2.0.0
9// =============================================================================
10
11//! Defines bounded backup contracts and behavior for this crate.
12
13use 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, SQLITE_SYNC_SCHEMA_V5,
17};
18use rusqlite::backup::Backup;
19use rusqlite::{Connection, OpenFlags, TransactionBehavior};
20use sha2::{Digest, Sha256};
21use std::fs::{self, File, OpenOptions};
22use std::io::Read;
23use std::path::{Path, PathBuf};
24use std::time::Duration;
25
26/// Evidence returned after an integrity-checked online backup.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct SqliteBackupReport {
29    /// Internal schema version copied to the backup.
30    pub schema_version: u32,
31    /// Final backup file size in bytes.
32    pub bytes: u64,
33}
34
35/// Evidence returned after an integrity-checked backup of both attached databases.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SqlitePairedBackupReport {
38    /// Backup evidence for complete local state.
39    pub local: SqliteBackupReport,
40    /// Backup evidence for the pending-only sync queue.
41    pub queue: SqliteBackupReport,
42    /// Number of unconfirmed operations captured in the same snapshot.
43    pub queue_entries: u64,
44    /// Combined size of the two SQLite files.
45    pub total_bytes: u64,
46}
47
48impl SqliteSyncStore {
49    /// Creates an integrity-checked online backup and atomically publishes it.
50    pub fn online_backup(
51        &self,
52        destination: impl AsRef<Path>,
53    ) -> SqliteSyncResult<SqliteBackupReport> {
54        if self.config().separate_outbox_path().is_some() {
55            return Err(SqliteSyncError::InvalidConfiguration(
56                "separate outbox requires a paired backup",
57            ));
58        }
59        let destination = normalize_backup_destination(destination.as_ref())?;
60        let temporary = reserve_temporary(&destination)?;
61        let result = self.copy_backup(&temporary).and_then(|report| {
62            sync_file(&temporary)?;
63            publish_temporary(&temporary, &destination)?;
64            Ok(report)
65        });
66        if result.is_err() {
67            let _ = fs::remove_file(&temporary);
68        }
69        result
70    }
71
72    /// Publishes a consistent bundle containing the local provider database and the
73    /// pending-only queue database. The destination directory must not exist.
74    pub fn paired_online_backup(
75        &self,
76        destination: impl AsRef<Path>,
77    ) -> SqliteSyncResult<SqlitePairedBackupReport> {
78        if self.config().separate_outbox_path().is_none() {
79            return Err(SqliteSyncError::InvalidConfiguration(
80                "paired backup requires a separate outbox",
81            ));
82        }
83        let destination = normalize_backup_destination(destination.as_ref())?;
84        let temporary = reserve_temporary_directory(&destination)?;
85        let local_name = safe_manifest_name(
86            self.config()
87                .path
88                .file_name()
89                .and_then(|name| name.to_str()),
90        )?;
91        let queue_name = safe_manifest_name(
92            self.config()
93                .separate_outbox_path()
94                .and_then(Path::file_name)
95                .and_then(|name| name.to_str()),
96        )?;
97        let result = self.with_connection(|connection| {
98            let transaction = connection
99                .transaction_with_behavior(TransactionBehavior::Exclusive)
100                .map_err(SqliteSyncError::database)?;
101            // Establish a read snapshot on both attached files before either copy.
102            let _: i64 = transaction
103                .query_row("SELECT count(*) FROM main.appcore_object_head", [], |row| row.get(0))
104                .map_err(SqliteSyncError::database)?;
105            let queue_entries: u64 = transaction
106                .query_row("SELECT count(*) FROM sync_queue.appcore_sync_outbox", [], |row| row.get(0))
107                .map_err(SqliteSyncError::database)?;
108            let local_path = temporary.join("local.sqlite3");
109            let queue_path = temporary.join("sync-queue.sqlite3");
110            fs::copy(self.config().path(), &local_path)
111                .map_err(|_| SqliteSyncError::DatabaseOperation)?;
112            fs::copy(
113                self.config().separate_outbox_path().ok_or(
114                    SqliteSyncError::InvalidConfiguration("separate outbox required"),
115                )?,
116                &queue_path,
117            )
118            .map_err(|_| SqliteSyncError::DatabaseOperation)?;
119            let local = validate_pair_database(&local_path, false)?;
120            let queue = validate_pair_database(&queue_path, true)?;
121            drop(transaction);
122            let manifest = format!(
123                "appcore-sync-pair-v1\nlocal_file={local_name}\nqueue_file={queue_name}\nlocal_schema={}\nlocal_bytes={}\nlocal_sha256={}\nqueue_schema={}\nqueue_bytes={}\nqueue_sha256={}\nqueue_entries={}\n",
124                local.schema_version, local.bytes, file_sha256(&local_path)?, queue.schema_version, queue.bytes, file_sha256(&queue_path)?, queue_entries
125            );
126            let manifest_path = temporary.join("manifest.txt");
127            fs::write(&manifest_path, manifest).map_err(|_| SqliteSyncError::DatabaseOperation)?;
128            sync_file(&local_path)?;
129            sync_file(&queue_path)?;
130            sync_file(&manifest_path)?;
131            sync_parent(&local_path)?;
132            let total_bytes = local.bytes.saturating_add(queue.bytes);
133            Ok(SqlitePairedBackupReport { local, queue, queue_entries, total_bytes })
134        });
135        let result = result.and_then(|report| {
136            publish_directory(&temporary, &destination)?;
137            Ok(report)
138        });
139        if result.is_err() {
140            let _ = fs::remove_dir_all(&temporary);
141        }
142        result
143    }
144
145    /// Restores a validated paired bundle into a new directory and publishes it atomically.
146    pub fn restore_paired_backup_to_new(
147        bundle: impl AsRef<Path>,
148        destination_directory: impl AsRef<Path>,
149    ) -> SqliteSyncResult<SqlitePairedBackupReport> {
150        let bundle = normalize_bundle_directory(bundle.as_ref())?;
151        let report = validate_paired_bundle(&bundle)?;
152        let destination = normalize_backup_destination(destination_directory.as_ref())?;
153        let temporary = reserve_temporary_directory(&destination)?;
154        let result = (|| {
155            let manifest = fs::read_to_string(bundle.join("manifest.txt"))
156                .map_err(|_| SqliteSyncError::IntegrityFailed)?;
157            let (local_name, queue_name) = manifest_names(&manifest)?;
158            for (source, name) in [
159                ("local.sqlite3", local_name),
160                ("sync-queue.sqlite3", queue_name),
161            ] {
162                fs::copy(bundle.join(source), temporary.join(&name))
163                    .map_err(|_| SqliteSyncError::DatabaseOperation)?;
164                sync_file(&temporary.join(name))?;
165            }
166            fs::copy(bundle.join("manifest.txt"), temporary.join("manifest.txt"))
167                .map_err(|_| SqliteSyncError::DatabaseOperation)?;
168            sync_file(&temporary.join("manifest.txt"))?;
169            validate_paired_bundle(&temporary)?;
170            sync_parent(&temporary.join("local.sqlite3"))?;
171            publish_directory(&temporary, &destination)?;
172            Ok(report)
173        })();
174        if result.is_err() {
175            let _ = fs::remove_dir_all(&temporary);
176        }
177        result
178    }
179
180    fn copy_backup(&self, temporary: &Path) -> SqliteSyncResult<SqliteBackupReport> {
181        self.with_connection(|source| {
182            copy_connection(source, temporary, self.config().backup_pages_per_step)
183        })
184    }
185
186    /// Restores a verified backup into a new, previously absent database path.
187    pub fn restore_backup_to_new(
188        backup_path: impl AsRef<Path>,
189        destination: impl AsRef<Path>,
190    ) -> SqliteSyncResult<SqliteBackupReport> {
191        let original_backup_path = backup_path.as_ref();
192        let backup_path = normalize_path(original_backup_path)?;
193        if !backup_path.is_file() {
194            return Err(SqliteSyncError::UnsafePath);
195        }
196        let destination = normalize_backup_destination(destination.as_ref())?;
197        let source = Connection::open_with_flags(
198            &backup_path,
199            OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
200        )
201        .map_err(SqliteSyncError::database)?;
202        configure_memory(&source)?;
203        validate_backup_connection(&source)?;
204        let temporary = reserve_temporary(&destination)?;
205        let result = copy_connection(&source, &temporary, 128).and_then(|report| {
206            sync_file(&temporary)?;
207            publish_temporary(&temporary, &destination)?;
208            Ok(report)
209        });
210        if result.is_err() {
211            let _ = fs::remove_file(&temporary);
212        }
213        result
214    }
215}
216
217fn copy_connection(
218    source: &Connection,
219    temporary: &Path,
220    pages_per_step: i32,
221) -> SqliteSyncResult<SqliteBackupReport> {
222    let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_FULL_MUTEX;
223    let mut destination =
224        Connection::open_with_flags(temporary, flags).map_err(SqliteSyncError::database)?;
225    configure_memory(&destination)?;
226    let backup = Backup::new(source, &mut destination).map_err(SqliteSyncError::database)?;
227    backup
228        .run_to_completion(pages_per_step, Duration::from_millis(2), None)
229        .map_err(SqliteSyncError::database)?;
230    drop(backup);
231    let schema_version = validate_backup_connection(&destination)?;
232    drop(destination);
233    let bytes = fs::metadata(temporary)
234        .map_err(|_| SqliteSyncError::DatabaseOperation)?
235        .len();
236    Ok(SqliteBackupReport {
237        schema_version,
238        bytes,
239    })
240}
241
242fn reserve_temporary_directory(destination: &Path) -> SqliteSyncResult<PathBuf> {
243    let parent = destination.parent().unwrap_or_else(|| Path::new("."));
244    let name = destination
245        .file_name()
246        .and_then(|value| value.to_str())
247        .ok_or(SqliteSyncError::UnsafePath)?;
248    for attempt in 0..16u8 {
249        let candidate = parent.join(format!(".{name}.{}-{attempt}.tmp", std::process::id()));
250        let mut builder = fs::DirBuilder::new();
251        #[cfg(unix)]
252        {
253            use std::os::unix::fs::DirBuilderExt;
254            builder.mode(0o700);
255        }
256        match builder.create(&candidate) {
257            Ok(()) => return Ok(candidate),
258            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
259            Err(_) => return Err(SqliteSyncError::DatabaseOperation),
260        }
261    }
262    Err(SqliteSyncError::CapacityExceeded("backup temporary"))
263}
264
265fn publish_directory(temporary: &Path, destination: &Path) -> SqliteSyncResult<()> {
266    fs::rename(temporary, destination).map_err(|_| SqliteSyncError::DatabaseOperation)?;
267    if sync_parent(destination).is_err() {
268        let _ = fs::rename(destination, temporary);
269        return Err(SqliteSyncError::DatabaseOperation);
270    }
271    Ok(())
272}
273
274fn validate_paired_bundle(bundle: &Path) -> SqliteSyncResult<SqlitePairedBackupReport> {
275    let manifest_path = bundle.join("manifest.txt");
276    let manifest =
277        fs::read_to_string(&manifest_path).map_err(|_| SqliteSyncError::IntegrityFailed)?;
278    let (local_name, queue_name) = manifest_names(&manifest)?;
279    let local_path = if bundle.join("local.sqlite3").is_file() {
280        bundle.join("local.sqlite3")
281    } else {
282        bundle.join(&local_name)
283    };
284    let queue_path = if bundle.join("sync-queue.sqlite3").is_file() {
285        bundle.join("sync-queue.sqlite3")
286    } else {
287        bundle.join(&queue_name)
288    };
289    let local = validate_pair_database(&local_path, false)?;
290    let queue = validate_pair_database(&queue_path, true)?;
291    let local_connection = Connection::open_with_flags(
292        &local_path,
293        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
294    )
295    .map_err(SqliteSyncError::database)?;
296    let recorded_queue_name: String = local_connection
297        .query_row(
298            "SELECT outbox_file FROM appcore_sync_storage_mode WHERE singleton=1",
299            [],
300            |row| row.get(0),
301        )
302        .map_err(SqliteSyncError::database)?;
303    if recorded_queue_name != queue_name {
304        return Err(SqliteSyncError::IntegrityFailed);
305    }
306    let connection = Connection::open_with_flags(
307        &queue_path,
308        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
309    )
310    .map_err(SqliteSyncError::database)?;
311    let queue_entries: u64 = connection
312        .query_row("SELECT count(*) FROM appcore_sync_outbox", [], |row| {
313            row.get(0)
314        })
315        .map_err(SqliteSyncError::database)?;
316    let expected = format!(
317        "appcore-sync-pair-v1\nlocal_file={local_name}\nqueue_file={queue_name}\nlocal_schema={}\nlocal_bytes={}\nlocal_sha256={}\nqueue_schema={}\nqueue_bytes={}\nqueue_sha256={}\nqueue_entries={}\n",
318        local.schema_version, local.bytes, file_sha256(&local_path)?, queue.schema_version, queue.bytes, file_sha256(&queue_path)?, queue_entries
319    );
320    if manifest != expected {
321        return Err(SqliteSyncError::IntegrityFailed);
322    }
323    Ok(SqlitePairedBackupReport {
324        total_bytes: local.bytes.saturating_add(queue.bytes),
325        local,
326        queue,
327        queue_entries,
328    })
329}
330
331fn manifest_names(manifest: &str) -> SqliteSyncResult<(String, String)> {
332    let mut lines = manifest.lines();
333    if lines.next() != Some("appcore-sync-pair-v1") {
334        return Err(SqliteSyncError::UpdateRequired);
335    }
336    let mut local = None;
337    let mut queue = None;
338    for line in lines {
339        if let Some(value) = line.strip_prefix("local_file=") {
340            local = Some(safe_manifest_name(Some(value))?);
341        }
342        if let Some(value) = line.strip_prefix("queue_file=") {
343            queue = Some(safe_manifest_name(Some(value))?);
344        }
345    }
346    Ok((
347        local.ok_or(SqliteSyncError::IntegrityFailed)?,
348        queue.ok_or(SqliteSyncError::IntegrityFailed)?,
349    ))
350}
351
352fn safe_manifest_name(value: Option<&str>) -> SqliteSyncResult<String> {
353    let value = value.ok_or(SqliteSyncError::UnsafePath)?;
354    if value.is_empty()
355        || value == "."
356        || value == ".."
357        || value.contains('/')
358        || value.contains('\\')
359        || value.bytes().any(|byte| byte.is_ascii_control())
360    {
361        return Err(SqliteSyncError::UnsafePath);
362    }
363    Ok(value.to_owned())
364}
365
366fn validate_pair_database(path: &Path, queue: bool) -> SqliteSyncResult<SqliteBackupReport> {
367    if !path.is_file() {
368        return Err(SqliteSyncError::UnsafePath);
369    }
370    let connection = Connection::open_with_flags(
371        path,
372        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_FULL_MUTEX,
373    )
374    .map_err(SqliteSyncError::database)?;
375    let schema_version: u32 = connection
376        .pragma_query_value(None, "user_version", |row| row.get(0))
377        .map_err(SqliteSyncError::database)?;
378    let application_id: i64 = connection
379        .pragma_query_value(None, "application_id", |row| row.get(0))
380        .map_err(SqliteSyncError::database)?;
381    let expected_id = if queue { 0x4150_4351 } else { 0x4150_4353 };
382    if application_id != expected_id {
383        return Err(SqliteSyncError::UpdateRequired);
384    }
385    if queue {
386        let integrity: String = connection
387            .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
388            .map_err(SqliteSyncError::database)?;
389        if integrity != "ok" {
390            return Err(SqliteSyncError::IntegrityFailed);
391        }
392    } else if validate_backup_connection(&connection).is_err()
393        && !validate_local_backup_connection(&connection)?
394    {
395        return Err(SqliteSyncError::IntegrityFailed);
396    }
397    if queue {
398        let has_queue: bool = connection
399            .query_row("SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type='table' AND name='appcore_sync_outbox')", [], |row| row.get(0))
400            .map_err(SqliteSyncError::database)?;
401        if !has_queue || schema_version != 1 {
402            return Err(SqliteSyncError::UpdateRequired);
403        }
404    }
405    let bytes = fs::metadata(path)
406        .map_err(|_| SqliteSyncError::DatabaseOperation)?
407        .len();
408    Ok(SqliteBackupReport {
409        schema_version,
410        bytes,
411    })
412}
413
414fn validate_local_backup_connection(connection: &Connection) -> SqliteSyncResult<bool> {
415    let version: u32 = connection
416        .pragma_query_value(None, "user_version", |row| row.get(0))
417        .map_err(SqliteSyncError::database)?;
418    if !(SQLITE_SYNC_SCHEMA_V1..=SQLITE_SYNC_SCHEMA_V5).contains(&version) {
419        return Ok(false);
420    }
421    let integrity: String = connection
422        .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
423        .map_err(SqliteSyncError::database)?;
424    let mode: String = connection
425        .query_row(
426            "SELECT outbox_mode FROM appcore_sync_storage_mode WHERE singleton=1",
427            [],
428            |row| row.get(0),
429        )
430        .map_err(SqliteSyncError::database)?;
431    Ok(integrity == "ok" && mode == "separate")
432}
433
434fn file_sha256(path: &Path) -> SqliteSyncResult<String> {
435    let mut file = File::open(path).map_err(|_| SqliteSyncError::DatabaseOperation)?;
436    let mut hasher = Sha256::new();
437    let mut buffer = [0u8; 64 * 1024];
438    loop {
439        let read = file
440            .read(&mut buffer)
441            .map_err(|_| SqliteSyncError::DatabaseOperation)?;
442        if read == 0 {
443            break;
444        }
445        hasher.update(&buffer[..read]);
446    }
447    Ok(hasher
448        .finalize()
449        .iter()
450        .map(|byte| format!("{byte:02x}"))
451        .collect())
452}
453
454fn normalize_backup_destination(path: &Path) -> SqliteSyncResult<PathBuf> {
455    if path.as_os_str().is_empty() {
456        return Err(SqliteSyncError::UnsafePath);
457    }
458    let normalized = normalize_path(path)?;
459    if normalized.exists() {
460        return Err(SqliteSyncError::UnsafePath);
461    }
462    Ok(normalized)
463}
464
465fn normalize_bundle_directory(path: &Path) -> SqliteSyncResult<PathBuf> {
466    let metadata = fs::symlink_metadata(path).map_err(|_| SqliteSyncError::UnsafePath)?;
467    if !metadata.is_dir() || metadata.file_type().is_symlink() {
468        return Err(SqliteSyncError::UnsafePath);
469    }
470    fs::canonicalize(path).map_err(|_| SqliteSyncError::UnsafePath)
471}
472
473fn reserve_temporary(destination: &Path) -> SqliteSyncResult<PathBuf> {
474    let parent = destination.parent().unwrap_or_else(|| Path::new("."));
475    let name = destination
476        .file_name()
477        .and_then(|value| value.to_str())
478        .ok_or(SqliteSyncError::UnsafePath)?;
479    for attempt in 0..16u8 {
480        let candidate = parent.join(format!(".{name}.{}-{attempt}.tmp", std::process::id()));
481        match OpenOptions::new()
482            .write(true)
483            .create_new(true)
484            .open(&candidate)
485        {
486            Ok(file) => {
487                drop(file);
488                return Ok(candidate);
489            }
490            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
491            Err(_) => return Err(SqliteSyncError::DatabaseOperation),
492        }
493    }
494    Err(SqliteSyncError::CapacityExceeded("backup temporary"))
495}
496
497fn publish_temporary(temporary: &Path, destination: &Path) -> SqliteSyncResult<()> {
498    fs::hard_link(temporary, destination).map_err(|_| SqliteSyncError::DatabaseOperation)?;
499    if fs::remove_file(temporary).is_err() {
500        let _ = fs::remove_file(destination);
501        return Err(SqliteSyncError::DatabaseOperation);
502    }
503    if sync_parent(destination).is_err() {
504        let _ = fs::remove_file(destination);
505        let _ = sync_parent(destination);
506        return Err(SqliteSyncError::DatabaseOperation);
507    }
508    Ok(())
509}
510
511fn validate_backup_connection(connection: &Connection) -> SqliteSyncResult<u32> {
512    let version: u32 = connection
513        .pragma_query_value(None, "user_version", |row| row.get(0))
514        .map_err(SqliteSyncError::database)?;
515    if version >= SQLITE_SYNC_SCHEMA_V5 {
516        let mode: String = connection
517            .query_row(
518                "SELECT outbox_mode FROM appcore_sync_storage_mode WHERE singleton=1",
519                [],
520                |row| row.get(0),
521            )
522            .map_err(SqliteSyncError::database)?;
523        if mode != "local" {
524            return Err(SqliteSyncError::UpdateRequired);
525        }
526    }
527    let application_id: i64 = connection
528        .pragma_query_value(None, "application_id", |row| row.get(0))
529        .map_err(SqliteSyncError::database)?;
530    if application_id != 0x4150_4353 {
531        return Err(SqliteSyncError::UpdateRequired);
532    }
533    let integrity: String = connection
534        .query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
535        .map_err(SqliteSyncError::database)?;
536    if matches!(
537        version,
538        SQLITE_SYNC_SCHEMA_V1
539            | SQLITE_SYNC_SCHEMA_V2
540            | SQLITE_SYNC_SCHEMA_V3
541            | SQLITE_SYNC_SCHEMA_V4
542            | SQLITE_SYNC_SCHEMA_V5
543    ) && integrity == "ok"
544    {
545        Ok(version)
546    } else if !matches!(
547        version,
548        SQLITE_SYNC_SCHEMA_V1
549            | SQLITE_SYNC_SCHEMA_V2
550            | SQLITE_SYNC_SCHEMA_V3
551            | SQLITE_SYNC_SCHEMA_V4
552            | SQLITE_SYNC_SCHEMA_V5
553    ) {
554        Err(SqliteSyncError::UpdateRequired)
555    } else {
556        Err(SqliteSyncError::IntegrityFailed)
557    }
558}
559
560fn sync_file(path: &Path) -> SqliteSyncResult<()> {
561    let mut options = OpenOptions::new();
562    options.read(true);
563    #[cfg(windows)]
564    options.write(true);
565    options
566        .open(path)
567        .and_then(|file| file.sync_all())
568        .map_err(|_| SqliteSyncError::DatabaseOperation)
569}
570
571#[cfg(unix)]
572fn sync_parent(path: &Path) -> SqliteSyncResult<()> {
573    let parent = path.parent().unwrap_or_else(|| Path::new("."));
574    File::open(parent)
575        .and_then(|directory| directory.sync_all())
576        .map_err(|_| SqliteSyncError::DatabaseOperation)
577}
578
579#[cfg(windows)]
580fn sync_parent(path: &Path) -> SqliteSyncResult<()> {
581    use std::os::windows::fs::OpenOptionsExt;
582    let parent = path.parent().ok_or(SqliteSyncError::UnsafePath)?;
583    OpenOptions::new()
584        .read(true)
585        .write(true)
586        .custom_flags(windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS)
587        .open(parent)
588        .and_then(|directory| directory.sync_all())
589        .map_err(|_| SqliteSyncError::DatabaseOperation)
590}
591#[cfg(not(any(unix, windows)))]
592fn sync_parent(_path: &Path) -> SqliteSyncResult<()> {
593    Err(SqliteSyncError::DatabaseOperation)
594}