Browse Source

Merge pull request #8751 from songxiaocheng/fix/macos-birthtime-1.4

fix: retrieve `birthtime` in nanosecond precision via system call (1.4-maint)
TW 1 month ago
parent
commit
1659be23f8
4 changed files with 62 additions and 10 deletions
  1. 8 7
      src/borg/archive.py
  2. 13 0
      src/borg/platform/__init__.py
  3. 38 0
      src/borg/platform/darwin.pyx
  4. 3 3
      src/borg/testsuite/archiver.py

+ 8 - 7
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,

+ 13 - 0
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

+ 38 - 0
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

+ 3 - 3
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.