mirror of
https://github.com/borgbackup/borg.git
synced 2026-05-28 04:03:21 -04:00
macOS: retrieve birthtime in nanosecond precision via system call, fixes #8724
This commit is contained in:
parent
0fe038192c
commit
a6be4c4f6c
4 changed files with 62 additions and 10 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue