pb_mapper_core/durable_file.rs
1//! Atomic replace and parent-directory durability.
2//!
3//! These are general file primitives, not credential ones — they lived in the
4//! auth persistence layer only because that is where the first caller was. Both
5//! report `io::Result` and leave it to the caller to map into its own error
6//! type.
7
8use std::fs::File;
9use std::path::Path;
10
11/// Replaces `to` with `from`, atomically where the platform allows it.
12///
13/// This is `std::fs::rename` everywhere. On Windows, std tries `MoveFileExW` and,
14/// when that is refused with `ERROR_ACCESS_DENIED` — as it is while any handle
15/// is open on `to` — retries through `FileRenameInfoEx` with POSIX semantics,
16/// which replace `to` regardless (Windows 10 1607 and later, on a file system
17/// that supports it). A bare `MoveFileExW` stops at the refusal.
18///
19/// The rename is not durable on its own on any platform: callers follow it
20/// with [`sync_parent_directory`]. They rename from the same directory, so the
21/// `MOVEFILE_WRITE_THROUGH` flag std does not pass would add nothing — it only
22/// flushes a move that crosses volumes.
23pub fn replace_file(from: &Path, to: &Path) -> std::io::Result<()> {
24 std::fs::rename(from, to)
25}
26
27/// Fsyncs the directory holding `path`, so a rename into it survives a crash.
28///
29/// A path with no parent is a no-op rather than an error.
30pub fn sync_parent_directory(path: &Path) -> std::io::Result<()> {
31 let Some(parent) = path.parent() else {
32 return Ok(());
33 };
34 open_directory_for_sync(parent).and_then(|directory| directory.sync_all())
35}
36
37fn open_directory_for_sync(path: &Path) -> std::io::Result<File> {
38 #[cfg(windows)]
39 {
40 use std::fs::OpenOptions;
41 use std::os::windows::fs::OpenOptionsExt;
42 const GENERIC_READ: u32 = 0x8000_0000;
43 const GENERIC_WRITE: u32 = 0x4000_0000;
44 const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
45 OpenOptions::new()
46 .access_mode(GENERIC_READ | GENERIC_WRITE)
47 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
48 .open(path)
49 }
50 #[cfg(not(windows))]
51 File::open(path)
52}