Skip to main content

toggle/
platform.rs

1// Platform-specific helpers for atomic file operations
2
3use std::fs::File;
4use std::io;
5use std::path::Path;
6
7/// Perform a durable fsync that guarantees data reaches persistent storage.
8/// On macOS, uses F_FULLFSYNC (fcntl) because fsync() only flushes to the
9/// disk write cache, not to the physical media.
10/// On other platforms, uses fdatasync (sync_data) which is cheaper than
11/// sync_all since it skips timestamp metadata updates.
12#[cfg(target_os = "macos")]
13pub fn durable_sync(file: &File) -> io::Result<()> {
14    use std::os::unix::io::AsRawFd;
15    let ret = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_FULLFSYNC) };
16    if ret == -1 {
17        Err(io::Error::last_os_error())
18    } else {
19        Ok(())
20    }
21}
22
23#[cfg(not(target_os = "macos"))]
24pub fn durable_sync(file: &File) -> io::Result<()> {
25    file.sync_data()
26}
27
28/// Fsync a directory to ensure metadata (directory entries) is persisted.
29/// This is important after rename operations to ensure durability.
30/// On Windows, this is a no-op since NTFS journals directory metadata.
31#[cfg(unix)]
32pub fn sync_dir(path: &Path) -> io::Result<()> {
33    let dir = File::open(path)?;
34    durable_sync(&dir)
35}
36
37#[cfg(not(unix))]
38pub fn sync_dir(_path: &Path) -> io::Result<()> {
39    // Windows NTFS journals directory metadata; explicit sync not needed.
40    Ok(())
41}
42
43/// Perform an atomic rename with platform-specific retry logic.
44/// On Windows, antivirus and search indexer can briefly lock files,
45/// so we retry with backoff on sharing violations.
46#[cfg(windows)]
47pub fn rename_with_retry(from: &Path, to: &Path) -> io::Result<()> {
48    use std::thread;
49    use std::time::Duration;
50
51    let delays = [50, 100, 200];
52    let mut last_err = None;
53
54    for (attempt, delay_ms) in std::iter::once(&0u64).chain(delays.iter()).enumerate() {
55        if attempt > 0 {
56            thread::sleep(Duration::from_millis(*delay_ms));
57        }
58        match std::fs::rename(from, to) {
59            Ok(()) => return Ok(()),
60            Err(e) => {
61                // ERROR_SHARING_VIOLATION = 32
62                if e.raw_os_error() == Some(32) {
63                    last_err = Some(e);
64                    continue;
65                }
66                return Err(e);
67            }
68        }
69    }
70
71    Err(last_err
72        .unwrap_or_else(|| io::Error::new(io::ErrorKind::Other, "rename failed after retries")))
73}
74
75#[cfg(not(windows))]
76pub fn rename_with_retry(from: &Path, to: &Path) -> io::Result<()> {
77    std::fs::rename(from, to)
78}
79
80/// Resolve symlinks to their canonical target path.
81/// Atomic operations should operate on the real file, not the symlink entry.
82pub fn resolve_symlinks(path: &Path) -> io::Result<std::path::PathBuf> {
83    std::fs::canonicalize(path)
84}