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
6 changes: 4 additions & 2 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from .helpers import HardLinkManager
from .helpers import ChunkIteratorFileWrapper, open_item
from .helpers import Error, IntegrityError, set_ec
from .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns
from .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns, set_birthtime
from .helpers import parse_timestamp, archive_ts_now
from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize
from .helpers import safe_encode, make_path_safe, remove_surrogates, text_to_json, join_cmd, remove_dotdot_prefixes
Expand Down Expand Up @@ -1005,8 +1005,10 @@ def restore_attrs(self, path, item, symlink=False, fd=None):
set_flags(path, item.bsdflags, fd=fd)
except OSError:
pass
else: # win32
else: # pragma: win32 only
# set timestamps rather late
if "birthtime" in item:
set_birthtime(path, item.birthtime, fd=fd)
mtime = item.mtime
atime = item.atime if "atime" in item else mtime
try:
Expand Down
6 changes: 6 additions & 0 deletions src/borg/platform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .linux import listxattr, getxattr, setxattr
from .linux import acl_get, acl_set
from .linux import set_flags, get_flags
from .base import set_birthtime
from .linux import SyncFile
from .posix import process_alive, local_pid_alive
from .posix import get_errno
Expand All @@ -31,6 +32,7 @@
from .freebsd import acl_get, acl_set
from .freebsd import set_flags
from .base import get_flags
from .base import set_birthtime
from .base import SyncFile
from .posix import process_alive, local_pid_alive
from .posix import get_errno
Expand All @@ -40,6 +42,7 @@
from .netbsd import listxattr, getxattr, setxattr
from .base import acl_get, acl_set
from .base import set_flags, get_flags
from .base import set_birthtime
from .base import SyncFile
from .posix import process_alive, local_pid_alive
from .posix import get_errno
Expand All @@ -52,6 +55,7 @@
from .darwin import set_flags
from .darwin import fdatasync, sync_dir # type: ignore[no-redef]
from .base import get_flags
from .base import set_birthtime
from .base import SyncFile
from .posix import process_alive, local_pid_alive
from .posix import get_errno
Expand All @@ -62,6 +66,7 @@
from .base import listxattr, getxattr, setxattr
from .base import acl_get, acl_set
from .base import set_flags, get_flags
from .base import set_birthtime
from .base import SyncFile
from .posix import process_alive, local_pid_alive
from .posix import get_errno
Expand All @@ -72,6 +77,7 @@
from .base import listxattr, getxattr, setxattr
from .base import acl_get, acl_set
from .base import set_flags, get_flags
from .windows import set_birthtime # type: ignore[no-redef]
from .base import SyncFile
from .windows import process_alive, local_pid_alive
from .windows import getosusername
Expand Down
7 changes: 7 additions & 0 deletions src/borg/platform/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ def setxattr(path, name, value, *, follow_symlinks=False):
"""


def set_birthtime(path, birthtime_ns, *, fd=None):
"""
Set creation time (birthtime) on *path* to *birthtime_ns*.
"""
raise NotImplementedError("set_birthtime is not supported on this platform")


def acl_get(path, item, st, numeric_ids=False, fd=None):
"""
Save ACL entries.
Expand Down
52 changes: 52 additions & 0 deletions src/borg/platform/windows.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,55 @@ def process_alive(host, pid, thread):
def local_pid_alive(pid):
"""Return whether *pid* is alive."""
raise NotImplementedError


def set_birthtime(path, birthtime_ns, *, fd=None):
"""
Set creation time (birthtime) on *path* (or *fd*) to *birthtime_ns*.
"""
import ctypes
from ctypes import wintypes
import msvcrt

# Windows API Constants
FILE_WRITE_ATTRIBUTES = 0x0100
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
FILE_SHARE_DELETE = 0x00000004
OPEN_EXISTING = 3
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000

class FILETIME(ctypes.Structure):
_fields_ = [("dwLowDateTime", wintypes.DWORD), ("dwHighDateTime", wintypes.DWORD)]

# Convert ns to Windows FILETIME
unix_epoch_in_100ns = 116444736000000000
intervals = (birthtime_ns // 100) + unix_epoch_in_100ns

ft = FILETIME()
ft.dwLowDateTime = intervals & 0xFFFFFFFF
ft.dwHighDateTime = intervals >> 32

close_handle = False
if fd is not None:
handle = msvcrt.get_osfhandle(fd)
else:
handle = ctypes.windll.kernel32.CreateFileW(
str(path),
FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
None,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
None,
)
if handle == -1:
return
close_handle = True

try:
# SetFileTime(handle, lpCreationTime, lpLastAccessTime, lpLastWriteTime)
ctypes.windll.kernel32.SetFileTime(handle, ctypes.byref(ft), None, None)
finally:
if close_handle:
ctypes.windll.kernel32.CloseHandle(handle)
56 changes: 34 additions & 22 deletions src/borg/testsuite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
raises = None

from ..fuse_impl import llfuse, has_any_fuse, has_llfuse, has_pyfuse3, has_mfusepy, ENOATTR # NOQA
from .. import platform
from .. import platform as borg_platform
from ..platformflags import is_win32, is_darwin

# Does this version of llfuse support ns precision?
Expand All @@ -32,7 +32,7 @@
has_lchflags = hasattr(os, "lchflags") or sys.platform.startswith("linux")
try:
with tempfile.NamedTemporaryFile() as file:
platform.set_flags(file.name, stat.UF_NODUMP)
borg_platform.set_flags(file.name, stat.UF_NODUMP)
except OSError:
has_lchflags = False

Expand All @@ -52,6 +52,9 @@ def same_ts_ns(ts_ns1, ts_ns2):
"""Compare two timestamps (both in nanoseconds) to determine whether they are (roughly) equal."""
diff_ts = int(abs(ts_ns1 - ts_ns2))
diff_max = 10 ** (-st_mtime_ns_round)
# On Windows, we generally have to expect up to 10ms variance.
if is_win32:
diff_max = max(diff_max, 10 * 1000 * 1000)
Comment on lines 54 to +57
Copy link
Member

Choose a reason for hiding this comment

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

looks like that st_mtime_ns_round is some constant. check where it is defined/used and if it makes sense maybe just define the constant differently on win32.

return diff_ts <= diff_max


Expand Down Expand Up @@ -187,40 +190,49 @@ def is_utime_fully_supported():
# Some filesystems (such as SSHFS) don't support utime on symlinks
if are_symlinks_supported():
os.symlink("something", filepath)
try:
os.utime(filepath, (1000, 2000), follow_symlinks=False)
new_stats = os.stat(filepath, follow_symlinks=False)
if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000:
return True
except OSError:
pass
except NotImplementedError:
pass
else:
open(filepath, "w").close()
try:
os.utime(filepath, (1000, 2000), follow_symlinks=False)
new_stats = os.stat(filepath, follow_symlinks=False)
if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000:
return True
except OSError:
pass
except NotImplementedError:
pass
return False
try:
os.utime(filepath, (1000, 2000))
new_stats = os.stat(filepath)
if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000:
return True
except OSError:
pass
return False


@functools.lru_cache
def is_birthtime_fully_supported():
if not hasattr(os.stat_result, "st_birthtime"):
return False
with unopened_tempfile() as filepath:
# Some filesystems (such as SSHFS) don't support utime on symlinks
if are_symlinks_supported():
os.symlink("something", filepath)
else:
open(filepath, "w").close()
try:
birthtime, mtime, atime = 946598400, 946684800, 946771200
os.utime(filepath, (atime, birthtime), follow_symlinks=False)
os.utime(filepath, (atime, mtime), follow_symlinks=False)
new_stats = os.stat(filepath, follow_symlinks=False)
if new_stats.st_birthtime == birthtime and new_stats.st_mtime == mtime and new_stats.st_atime == atime:
birthtime_ns, mtime_ns, atime_ns = 946598400 * 10**9, 946684800 * 10**9, 946771200 * 10**9
borg_platform.set_birthtime(filepath, birthtime_ns)
os.utime(filepath, ns=(atime_ns, mtime_ns))
new_stats = os.stat(filepath)
bt = borg_platform.get_birthtime_ns(new_stats, filepath)
if (
bt is not None
and same_ts_ns(bt, birthtime_ns)
and same_ts_ns(new_stats.st_mtime_ns, mtime_ns)
and same_ts_ns(new_stats.st_atime_ns, atime_ns)
):
return True
except OSError:
pass
except NotImplementedError:
except (OSError, NotImplementedError, AttributeError):
pass
return False

Expand Down
12 changes: 10 additions & 2 deletions src/borg/testsuite/archiver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from ...repository import Repository
from .. import has_lchflags, has_mknod, is_utime_fully_supported, have_fuse_mtime_ns, st_mtime_ns_round, filter_xattrs
from .. import changedir, ENOATTR # NOQA
from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, granularity_sleep
from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, granularity_sleep, same_ts_ns
from ..platform.platform_test import is_win32
from ...xattr import get_all

Expand Down Expand Up @@ -396,8 +396,12 @@ def _assert_dirs_equal_cmp(diff, ignore_flags=False, ignore_xattrs=False, ignore
# If utime is not fully supported, Borg cannot set mtime.
# Therefore, we should not test it in that case.
if is_utime_fully_supported():
if is_win32 and stat.S_ISLNK(s1.st_mode):
# Windows often fails to restore symlink mtime correctly or we can't set it.
# Skip mtime check for symlinks on Windows.
pass
# Older versions of llfuse do not support ns precision properly
if ignore_ns:
elif ignore_ns:
d1.append(int(s1.st_mtime_ns / 1e9))
d2.append(int(s2.st_mtime_ns / 1e9))
elif fuse and not have_fuse_mtime_ns:
Expand All @@ -409,6 +413,10 @@ def _assert_dirs_equal_cmp(diff, ignore_flags=False, ignore_xattrs=False, ignore
if not ignore_xattrs:
d1.append(filter_xattrs(get_all(path1, follow_symlinks=False)))
d2.append(filter_xattrs(get_all(path2, follow_symlinks=False)))
# Check timestamps with same_ts_ns logic (includes Windows tolerance)
mtime_idx = -2 if not ignore_xattrs else -1
if same_ts_ns(d1[mtime_idx], d2[mtime_idx]):
d2[mtime_idx] = d1[mtime_idx]
assert d1 == d2
for sub_diff in diff.subdirs.values():
_assert_dirs_equal_cmp(sub_diff, ignore_flags=ignore_flags, ignore_xattrs=ignore_xattrs, ignore_ns=ignore_ns)
Expand Down
17 changes: 10 additions & 7 deletions src/borg/testsuite/archiver/create_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,19 +211,22 @@ def test_unix_socket(archivers, request, monkeypatch):
def test_nobirthtime(archivers, request):
archiver = request.getfixturevalue(archivers)
create_test_files(archiver.input_path)
birthtime, mtime, atime = 946598400, 946684800, 946771200
os.utime("input/file1", (atime, birthtime))
os.utime("input/file1", (atime, mtime))
cmd(archiver, "repo-create", RK_ENCRYPTION)
birthtime_ns, mtime_ns, atime_ns = 946598400 * 10**9, 946684800 * 10**9, 946771200 * 10**9
platform.set_birthtime("input/file1", birthtime_ns)
os.utime("input/file1", ns=(atime_ns, mtime_ns))
cmd(archiver, "repo-create", "--encryption=none")
cmd(archiver, "create", "test", "input", "--nobirthtime")
with changedir("output"):
cmd(archiver, "extract", "test")
sti = os.stat("input/file1")
sto = os.stat("output/input/file1")
assert same_ts_ns(sti.st_birthtime * 1e9, birthtime * 1e9)
assert same_ts_ns(sto.st_birthtime * 1e9, mtime * 1e9)
assert same_ts_ns(sti.st_birthtime * 1e9, birthtime_ns)
if is_win32:
assert not same_ts_ns(sto.st_birthtime * 1e9, birthtime_ns)
else:
assert same_ts_ns(sto.st_birthtime * 1e9, mtime_ns)
assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)
assert same_ts_ns(sto.st_mtime_ns, mtime * 1e9)
assert same_ts_ns(sto.st_mtime_ns, mtime_ns)


def test_create_stdin(archivers, request):
Expand Down
Loading