-
-
Notifications
You must be signed in to change notification settings - Fork 827
windows: implement st_birthtime restoration, fixes #8730 #9341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,3 +37,51 @@ def process_alive(host, pid, thread): | |
| def local_pid_alive(pid): | ||
| """Return whether *pid* is alive.""" | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| def set_birthtime(path, birthtime_ns): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there also a way to do this with an open file descriptor?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a lot of borg file functions take a file descriptor and a path. if the (open) file descriptor is provided, the function uses that and if not, it falls back to using the path. |
||
| """ | ||
| Set creation time (birthtime) on *path* to *birthtime_ns*. | ||
| """ | ||
| import ctypes | ||
| from ctypes import wintypes | ||
|
|
||
| # 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 | ||
| # Units: 100-nanosecond intervals | ||
| # Epoch: Jan 1, 1601 | ||
| unix_epoch_in_100ns = 116444736000000000 | ||
| intervals = (birthtime_ns // 100) + unix_epoch_in_100ns | ||
|
|
||
| ft = FILETIME() | ||
| ft.dwLowDateTime = intervals & 0xFFFFFFFF | ||
| ft.dwHighDateTime = intervals >> 32 | ||
|
|
||
| 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 | ||
|
|
||
| try: | ||
| # SetFileTime(handle, lpCreationTime, lpLastAccessTime, lpLastWriteTime) | ||
| ctypes.windll.kernel32.SetFileTime(handle, ctypes.byref(ft), None, None) | ||
| finally: | ||
| ctypes.windll.kernel32.CloseHandle(handle) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 borg import platform as borg_platform | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't do absolute imports. |
||
| from ..platformflags import is_win32, is_darwin | ||
|
|
||
| # Does this version of llfuse support ns precision? | ||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -185,42 +185,51 @@ def are_fifos_supported(): | |
| def is_utime_fully_supported(): | ||
| with unopened_tempfile() as filepath: | ||
| # Some filesystems (such as SSHFS) don't support utime on symlinks | ||
| if are_symlinks_supported(): | ||
| if are_symlinks_supported() and not is_win32: | ||
| 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(): | ||
| if are_symlinks_supported() and not is_win32: | ||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't kill it for other platforms. |
||
| 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 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import shutil | ||
| import socket | ||
| import stat | ||
| import sys | ||
| import subprocess | ||
|
|
||
| import pytest | ||
|
|
@@ -211,19 +212,23 @@ 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)) | ||
| birthtime_ns, mtime_ns, atime_ns = 946598400 * 10**9, 946684800 * 10**9, 946771200 * 10**9 | ||
| if sys.platform == "win32": | ||
| platform.set_birthtime("input/file1", birthtime_ns) | ||
|
Comment on lines
+216
to
+217
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is_win32 also: don't kill it for other platforms. |
||
| os.utime("input/file1", ns=(atime_ns, mtime_ns)) | ||
| cmd(archiver, "repo-create", RK_ENCRYPTION) | ||
| 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 sys.platform == "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): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,7 @@ | |
| from ...helpers import flags_noatime, flags_normal | ||
| from .. import changedir, same_ts_ns, granularity_sleep | ||
| from .. import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported, is_birthtime_fully_supported | ||
| from ...platform import get_birthtime_ns | ||
| from ...platform import get_birthtime_ns, set_birthtime # noqa: F401 | ||
| from ...platformflags import is_darwin, is_freebsd, is_win32 | ||
| from . import ( | ||
| RK_ENCRYPTION, | ||
|
|
@@ -168,7 +168,7 @@ def test_birthtime(archivers, request): | |
| archiver = request.getfixturevalue(archivers) | ||
| create_test_files(archiver.input_path) | ||
| birthtime, mtime, atime = 946598400, 946684800, 946771200 | ||
| os.utime("input/file1", (atime, birthtime)) | ||
| set_birthtime("input/file1", birthtime * 1_000_000_000) # noqa: F821 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't kill it for other platforms. |
||
| os.utime("input/file1", (atime, mtime)) | ||
| cmd(archiver, "repo-create", RK_ENCRYPTION) | ||
| cmd(archiver, "create", "test", "input") | ||
|
|
@@ -177,7 +177,11 @@ def test_birthtime(archivers, request): | |
| sti = os.stat("input/file1") | ||
| sto = os.stat("output/input/file1") | ||
| assert same_ts_ns(sti.st_birthtime * 1e9, sto.st_birthtime * 1e9) | ||
| assert same_ts_ns(sto.st_birthtime * 1e9, birthtime * 1e9) | ||
| if is_win32: | ||
| # allow for small differences (e.g. 10ms) | ||
| assert abs(sto.st_birthtime * 1e9 - birthtime * 1e9) < 10_000_000 | ||
|
Comment on lines
+181
to
+182
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we generally have to expect up to 10ms variance on windows, this could be moved into same_ts_ns. |
||
| else: | ||
| assert same_ts_ns(sto.st_birthtime * 1e9, birthtime * 1e9) | ||
| assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns) | ||
| assert same_ts_ns(sto.st_mtime_ns, mtime * 1e9) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import os | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the end, this should be in the tests of the extract command. |
||
| import pytest | ||
| from ...platform import set_birthtime, get_birthtime_ns | ||
| from ...platformflags import is_win32 | ||
| from . import cmd, generate_archiver_tests, changedir | ||
|
|
||
|
|
||
| def pytest_generate_tests(metafunc): | ||
| generate_archiver_tests(metafunc, kinds="local") | ||
|
|
||
|
|
||
| @pytest.mark.skipif(not is_win32, reason="Windows only test") | ||
| def test_birthtime_restore(archivers, request): | ||
| archiver = request.getfixturevalue(archivers) | ||
| cmd(archiver, "repo-create", "--encryption=none") | ||
|
|
||
| # Create a file in input directory | ||
| input_file = os.path.join(archiver.input_path, "test_file") | ||
| if not os.path.exists(archiver.input_path): | ||
| os.makedirs(archiver.input_path) | ||
| with open(input_file, "w") as f: | ||
| f.write("data") | ||
|
|
||
| st = os.stat(input_file) | ||
| original_birthtime = get_birthtime_ns(st, input_file) | ||
|
|
||
| # Set an old birthtime (10 years ago) | ||
| # 10 years * 365 days * 24 hours * 3600 seconds * 10^9 ns/s | ||
| old_birthtime_ns = original_birthtime - 10 * 365 * 24 * 3600 * 10**9 | ||
| # Ensure it's 100ns aligned (Windows precision) | ||
| old_birthtime_ns = (old_birthtime_ns // 100) * 100 | ||
| set_birthtime(input_file, old_birthtime_ns) | ||
|
|
||
| # Verify it was set correctly initially | ||
| st_verify = os.stat(input_file) | ||
| assert get_birthtime_ns(st_verify, input_file) == old_birthtime_ns | ||
|
|
||
| # Archive it | ||
| cmd(archiver, "create", "test", "input") | ||
|
|
||
| # Extract it to a different location | ||
| if not os.path.exists("output"): | ||
| os.makedirs("output") | ||
|
Comment on lines
+42
to
+43
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't we always have the output dir? also: check makedirs parameters. |
||
| with changedir("output"): | ||
| cmd(archiver, "extract", "test") | ||
|
|
||
| # Check restored birthtime | ||
| restored_file = os.path.join("output", "input", "test_file") | ||
| st_restored = os.stat(restored_file) | ||
| restored_birthtime = get_birthtime_ns(st_restored, restored_file) | ||
|
|
||
| assert restored_birthtime == old_birthtime_ns | ||
Uh oh!
There was an error while loading. Please reload this page.