Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/borg/platform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
from .base import listxattr, getxattr, setxattr
from .base import acl_get, acl_set
from .base import set_flags, get_flags
from .base import SyncFile
from .windows import SyncFile
from .windows import process_alive, local_pid_alive
from .windows import getosusername
from . import windows_ug as platform_ug
Expand Down
2 changes: 1 addition & 1 deletion src/borg/platform/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class SyncFile:

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

TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
See platform/windows.pyx for the Windows implementation using CreateFile with FILE_FLAG_WRITE_THROUGH.
"""

def __init__(self, path, *, fd=None, binary=False):
Expand Down
89 changes: 89 additions & 0 deletions src/borg/platform/windows.pyx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import ctypes
import ctypes.wintypes
import errno as errno_mod
import msvcrt
import os
import platform

from .base import SyncFile as BaseSyncFile


cdef extern from 'windows.h':
ctypedef void* HANDLE
Expand All @@ -13,6 +19,89 @@ cdef extern from 'windows.h':
cdef extern int PROCESS_QUERY_INFORMATION


# Win32 API constants for CreateFileW
GENERIC_WRITE = 0x40000000
FILE_SHARE_READ = 0x00000001
CREATE_NEW = 1
FILE_ATTRIBUTE_NORMAL = 0x80
FILE_FLAG_WRITE_THROUGH = 0x80000000
ERROR_FILE_EXISTS = 80

_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
_CreateFileW = _kernel32.CreateFileW
_CreateFileW.restype = ctypes.wintypes.HANDLE
_CreateFileW.argtypes = [
ctypes.wintypes.LPCWSTR,
ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD,
ctypes.c_void_p,
ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD,
ctypes.wintypes.HANDLE,
]
_CloseHandle = _kernel32.CloseHandle
INVALID_HANDLE_VALUE = ctypes.wintypes.HANDLE(-1).value


class SyncFile(BaseSyncFile):
"""
Windows SyncFile using FILE_FLAG_WRITE_THROUGH for data durability.

FILE_FLAG_WRITE_THROUGH instructs Windows to write through any intermediate
cache and go directly to disk, providing data durability guarantees similar
to fdatasync/F_FULLFSYNC on POSIX/macOS systems.

When an already-open fd is provided, falls back to base implementation.
"""

def __init__(self, path, *, fd=None, binary=False):
if fd is not None:
# An already-opened fd was provided (e.g., from SaveFile via mkstemp).
# We cannot change its flags, so fall back to the base implementation.
super().__init__(path, fd=fd, binary=binary)
return

self.path = path
handle = _CreateFileW(
str(path),
GENERIC_WRITE,
FILE_SHARE_READ,
None,
CREATE_NEW, # fail if file exists, matching Python's 'x' mode
FILE_FLAG_WRITE_THROUGH | FILE_ATTRIBUTE_NORMAL,
None,
)
if handle == INVALID_HANDLE_VALUE:
error = ctypes.get_last_error()
if error == ERROR_FILE_EXISTS:
raise FileExistsError(errno_mod.EEXIST, os.strerror(errno_mod.EEXIST), str(path))
raise ctypes.WinError(error)

try:
oflags = os.O_WRONLY | (os.O_BINARY if binary else os.O_TEXT)
c_fd = msvcrt.open_osfhandle(handle, oflags)
Comment on lines +81 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the flag values from os valid for that msvcrt call?

except Exception:
_CloseHandle(handle)
raise

try:
mode = "wb" if binary else "w"
self.f = os.fdopen(c_fd, mode=mode)
except Exception:
os.close(c_fd) # Also closes the underlying Windows handle
raise
self.fd = self.f.fileno()

def sync(self):
"""Flush and sync to persistent storage.

With FILE_FLAG_WRITE_THROUGH, writes already go to stable storage.
We still call os.fsync (FlushFileBuffers) for belt-and-suspenders safety.
"""
self.f.flush()
os.fsync(self.fd)


def getosusername():
"""Return the OS username."""
return os.getlogin()
Expand Down
1 change: 1 addition & 0 deletions src/borg/testsuite/platform/platform_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def are_acls_working():
skipif_not_posix = pytest.mark.skipif(not (is_linux or is_freebsd or is_darwin), reason="POSIX-only tests")
skipif_fakeroot_detected = pytest.mark.skipif(fakeroot_detected(), reason="not compatible with fakeroot")
skipif_acls_not_working = pytest.mark.skipif(not are_acls_working(), reason="ACLs do not work")
skipif_not_win32 = pytest.mark.skipif(not is_win32, reason="Windows-only test")
skipif_no_ubel_user = pytest.mark.skipif(not user_exists("übel"), reason="requires übel user")


Expand Down
90 changes: 90 additions & 0 deletions src/borg/testsuite/platform/windows_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import os
import tempfile

import pytest

from .platform_test import skipif_not_win32

# Set module-level skips
pytestmark = [skipif_not_win32]


def test_syncfile_basic():
"""Integration: SyncFile creates file and writes data correctly."""
from ...platform.windows import SyncFile

with tempfile.TemporaryDirectory() as tmpdir:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc there is a pytest fixture for temp dirs.
one is even using pathlib, so code can be prettier.

path = os.path.join(tmpdir, "testfile")
with SyncFile(path, binary=True) as sf:
sf.write(b"hello borg")
with open(path, "rb") as f:
assert f.read() == b"hello borg"


def test_syncfile_file_exists_error():
"""SyncFile raises FileExistsError if file already exists."""
from ...platform.windows import SyncFile

with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "testfile")
open(path, "w").close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathlib Paths have .touch method.

with pytest.raises(FileExistsError):
SyncFile(path, binary=True)


def test_syncfile_text_mode():
"""SyncFile works in text mode."""
from ...platform.windows import SyncFile

with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "testfile.txt")
with SyncFile(path) as sf:
sf.write("hello text")
with open(path, "r") as f:
assert f.read() == "hello text"


def test_syncfile_fd_fallback():
"""SyncFile with fd falls back to base implementation (mirrors SaveFile usage)."""
from ...platform.windows import SyncFile

with tempfile.TemporaryDirectory() as tmpdir:
fd, path = tempfile.mkstemp(dir=tmpdir)
with SyncFile(path, fd=fd, binary=True) as sf:
sf.write(b"fallback test")
with open(path, "rb") as f:
assert f.read() == b"fallback test"


def test_syncfile_sync():
"""Explicit sync() does not raise."""
from ...platform.windows import SyncFile

with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "testfile")
with SyncFile(path, binary=True) as sf:
sf.write(b"sync test data")
sf.sync()


def test_syncfile_uses_write_through(monkeypatch):
"""Verify CreateFileW is called with FILE_FLAG_WRITE_THROUGH."""
from ...platform import windows

calls = []
original = windows._CreateFileW

def mock_create(*args):
calls.append(args)
return original(*args)

monkeypatch.setattr(windows, "_CreateFileW", mock_create)

with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "testfile")
with windows.SyncFile(path, binary=True) as sf:
sf.write(b"write-through test")

assert len(calls) == 1
flags_attrs = calls[0][5] # 6th arg: dwFlagsAndAttributes
assert flags_attrs & windows.FILE_FLAG_WRITE_THROUGH