diff --git a/src/borg/archive.py b/src/borg/archive.py index 617f0508f..7e1272d97 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -33,7 +33,7 @@ from .helpers import Manifest from .helpers import hardlinkable from .helpers import ChunkIteratorFileWrapper, normalize_chunker_params, open_item from .helpers import Error, IntegrityError, set_ec -from .platform import uid2user, user2uid, gid2group, group2gid +from .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns from .helpers import parse_timestamp, to_localtime from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates @@ -1139,7 +1139,7 @@ class MetadataCollector: self.noxattrs = noxattrs self.nobirthtime = nobirthtime - def stat_simple_attrs(self, st): + def stat_simple_attrs(self, st, path, fd=None): attrs = dict( mode=st.st_mode, uid=st.st_uid, @@ -1153,9 +1153,10 @@ class MetadataCollector: attrs['atime'] = safe_ns(st.st_atime_ns) if not self.noctime: attrs['ctime'] = safe_ns(st.st_ctime_ns) - if not self.nobirthtime and hasattr(st, 'st_birthtime'): - # sadly, there's no stat_result.st_birthtime_ns - attrs['birthtime'] = safe_ns(int(st.st_birthtime * 10**9)) + if not self.nobirthtime: + birthtime_ns = get_birthtime_ns(st, path, fd=fd) + if birthtime_ns is not None: + attrs['birthtime'] = safe_ns(birthtime_ns) if self.numeric_ids: attrs['user'] = attrs['group'] = None else: @@ -1185,7 +1186,7 @@ class MetadataCollector: return attrs def stat_attrs(self, st, path, fd=None): - attrs = self.stat_simple_attrs(st) + attrs = self.stat_simple_attrs(st, path, fd=fd) attrs.update(self.stat_ext_attrs(st, path, fd=fd)) return attrs @@ -1434,7 +1435,7 @@ class FilesystemObjectProcessors: with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags, noatime=True) as fd: with backup_io('fstat'): st = stat_update_check(st, os.fstat(fd)) - item.update(self.metadata_collector.stat_simple_attrs(st)) + item.update(self.metadata_collector.stat_simple_attrs(st, path, fd=fd)) is_special_file = is_special(st.st_mode) if is_special_file: # we process a special file like a regular file. reflect that in mode, diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index 98f926230..713aa2457 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -40,3 +40,16 @@ elif is_darwin: # pragma: darwin only from .darwin import API_VERSION as OS_API_VERSION from .darwin import listxattr, getxattr, setxattr from .darwin import acl_get, acl_set + from .darwin import is_darwin_feature_64_bit_inode, _get_birthtime_ns + + +def get_birthtime_ns(st, path, fd=None): + if hasattr(st, "st_birthtime_ns"): + # added in Python 3.12 but not always available. + return st.st_birthtime_ns + elif is_darwin and is_darwin_feature_64_bit_inode: + return _get_birthtime_ns(fd or path, follow_symlinks=False) + elif hasattr(st, "st_birthtime"): + return int(st.st_birthtime * 10**9) + else: + return None diff --git a/src/borg/platform/darwin.pyx b/src/borg/platform/darwin.pyx index e43e26742..64cd87d9e 100644 --- a/src/borg/platform/darwin.pyx +++ b/src/borg/platform/darwin.pyx @@ -2,6 +2,7 @@ import os from libc.stdint cimport uint32_t from libc cimport errno +from posix.time cimport timespec from .posix import user2uid, group2gid from ..helpers import safe_decode, safe_encode @@ -9,6 +10,18 @@ from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_str API_VERSION = '1.4_01' +cdef extern from *: + """ + #ifdef _DARWIN_FEATURE_64_BIT_INODE + #define DARWIN_FEATURE_64_BIT_INODE_DEFINED 1 + #else + #define DARWIN_FEATURE_64_BIT_INODE_DEFINED 0 + #endif + """ + int DARWIN_FEATURE_64_BIT_INODE_DEFINED + +is_darwin_feature_64_bit_inode = DARWIN_FEATURE_64_BIT_INODE_DEFINED != 0 + cdef extern from "sys/xattr.h": ssize_t c_listxattr "listxattr" (const char *path, char *list, size_t size, int flags) ssize_t c_flistxattr "flistxattr" (int filedes, char *list, size_t size, int flags) @@ -37,6 +50,14 @@ cdef extern from "sys/acl.h": char *acl_to_text(acl_t acl, ssize_t *len_p) int ACL_TYPE_EXTENDED +cdef extern from "sys/stat.h": + cdef struct stat: + timespec st_birthtimespec + + int c_stat "stat" (const char *path, stat *buf) + int c_lstat "lstat" (const char *path, stat *buf) + int c_fstat "fstat" (int filedes, stat *buf) + def listxattr(path, *, follow_symlinks=False): def func(path, buf, size): @@ -159,3 +180,20 @@ def acl_set(path, item, numeric_ids=False, fd=None): raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path)) finally: acl_free(acl) + + +def _get_birthtime_ns(path, follow_symlinks=False): + if isinstance(path, str): + path = os.fsencode(path) + cdef stat stat_info + cdef int result + if isinstance(path, int): + result = c_fstat(path, &stat_info) + else: + if follow_symlinks: + result = c_stat(path, &stat_info) + else: + result = c_lstat(path, &stat_info) + if result != 0: + raise OSError(errno.errno, os.strerror(errno.errno), os.fsdecode(path)) + return stat_info.st_birthtimespec.tv_sec * 1_000_000_000 + stat_info.st_birthtimespec.tv_nsec diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index bc1fe691b..bc9aee798 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -1516,19 +1516,19 @@ class ArchiverTestCase(ArchiverTestCaseBase): input_path = os.path.abspath('input/file') xa_key, xa_value = b'com.apple.ResourceFork', b'whatshouldbehere' # issue #7234 xattr.setxattr(input_path.encode(), xa_key, xa_value) - birthtime_expected = os.stat(input_path).st_birthtime + birthtime_expected = platform.get_birthtime_ns(os.stat(input_path), input_path) mtime_expected = os.stat(input_path).st_mtime_ns # atime_expected = os.stat(input_path).st_atime_ns self.cmd('create', self.repository_location + '::test', 'input') with changedir('output'): self.cmd('extract', self.repository_location + '::test') extracted_path = os.path.abspath('input/file') - birthtime_extracted = os.stat(extracted_path).st_birthtime + birthtime_extracted = platform.get_birthtime_ns(os.stat(extracted_path), extracted_path) mtime_extracted = os.stat(extracted_path).st_mtime_ns # atime_extracted = os.stat(extracted_path).st_atime_ns xa_value_extracted = xattr.getxattr(extracted_path.encode(), xa_key) assert xa_value_extracted == xa_value - assert same_ts_ns(birthtime_extracted * 1e9, birthtime_expected * 1e9) + assert same_ts_ns(birthtime_extracted, birthtime_expected) assert same_ts_ns(mtime_extracted, mtime_expected) # assert same_ts_ns(atime_extracted, atime_expected) # still broken, but not really important.