Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/borg/platform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from .darwin import acl_get, acl_set
from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns
from .darwin import set_flags
from .darwin import fdatasync, sync_dir # type: ignore[no-redef]
from .base import get_flags
from .base import SyncFile
from .posix import process_alive, local_pid_alive
Expand Down
1 change: 0 additions & 1 deletion src/borg/platform/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ class SyncFile:

Calling SyncFile(path) for an existing path will raise FileExistsError. See the comment in __init__.

TODO: Use F_FULLSYNC on macOS.
TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
"""

Expand Down
30 changes: 30 additions & 0 deletions src/borg/platform/darwin.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,33 @@ def set_flags(path, bsd_flags, fd=None):
path_bytes = os.fsencode(path)
if lchflags(path_bytes, c_flags) == -1:
raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path_bytes))


import errno as errno_mod
import fcntl as fcntl_mod


def fdatasync(fd):
"""macOS fdatasync using F_FULLFSYNC for true data durability.

On macOS, os.fsync() only flushes to the drive's write cache.
fcntl F_FULLFSYNC flushes to persistent storage.
Falls back to os.fsync() if F_FULLFSYNC is not supported.
"""
try:
fcntl_mod.fcntl(fd, fcntl_mod.F_FULLFSYNC)
except OSError:
# F_FULLFSYNC not supported (e.g. network filesystem), fall back
os.fsync(fd)


def sync_dir(path):
"""Sync a directory to persistent storage on macOS using F_FULLFSYNC."""
fd = os.open(str(path), os.O_RDONLY)
try:
fdatasync(fd)
except OSError as os_error:
if os_error.errno != errno_mod.EINVAL:
raise
finally:
os.close(fd)
63 changes: 63 additions & 0 deletions src/borg/testsuite/platform/darwin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import tempfile

from ...platform import acl_get, acl_set
from ...platform import fdatasync, sync_dir
from .platform_test import skipif_not_darwin, skipif_fakeroot_detected, skipif_acls_not_working

# Set module-level skips
Expand Down Expand Up @@ -46,3 +47,65 @@ def test_extended_acl():
b"group:ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000000::0:allow:read"
in get_acl(file2.name, numeric_ids=True)["acl_extended"]
)


def test_fdatasync_uses_f_fullfsync(monkeypatch):
"""Verify fcntl F_FULLFSYNC is called."""
import fcntl as fcntl_mod
from ...platform import darwin

calls = []
original_fcntl = fcntl_mod.fcntl

def mock_fcntl(fd, cmd, *args):
calls.append((fd, cmd))
return original_fcntl(fd, cmd, *args)

monkeypatch.setattr(fcntl_mod, "fcntl", mock_fcntl)

with tempfile.NamedTemporaryFile() as tmp:
tmp.write(b"test data")
tmp.flush()
darwin.fdatasync(tmp.fileno())

assert any(cmd == fcntl_mod.F_FULLFSYNC for _, cmd in calls), "fdatasync should call fcntl with F_FULLFSYNC"


def test_fdatasync_falls_back_to_fsync(monkeypatch):
"""Verify os.fsync fallback when F_FULLFSYNC fails."""
import fcntl as fcntl_mod
from ...platform import darwin

fsync_calls = []

def mock_fcntl(fd, cmd, *args):
if cmd == fcntl_mod.F_FULLFSYNC:
raise OSError("F_FULLFSYNC not supported")
return 0

def mock_fsync(fd):
fsync_calls.append(fd)

monkeypatch.setattr(fcntl_mod, "fcntl", mock_fcntl)
monkeypatch.setattr(os, "fsync", mock_fsync)

with tempfile.NamedTemporaryFile() as tmp:
tmp.write(b"test data")
tmp.flush()
darwin.fdatasync(tmp.fileno())

assert len(fsync_calls) == 1, "Should fall back to os.fsync when F_FULLFSYNC fails"


def test_fdatasync_basic():
"""Integration: fdatasync completes on a real file without error."""
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(b"test data for fdatasync")
tmp.flush()
fdatasync(tmp.fileno())


def test_sync_dir_basic():
"""Integration: sync_dir completes on a real directory without error."""
with tempfile.TemporaryDirectory() as tmpdir:
sync_dir(tmpdir)