1
0
Fork 0
mirror of https://github.com/borgbackup/borg.git synced 2025-02-24 15:12:00 +00:00

include item birthtime in archive (where available) (#3313)

include item birthtime in archive, fixes #3272

* use `safe_ns` when reading birthtime into attributes
* proper order for `birthtime` in `ITEM_KEYS` list
* use `bigint` wrapper for consistency
* Add tests to verify that birthtime is normally preserved, but not preserved when `--nobirthtime` is passed to `borg create`.
This commit is contained in:
Sam H 2017-11-13 08:55:10 -05:00 committed by Sam Hathaway
parent 1201b62e2b
commit e51cf43142
6 changed files with 78 additions and 4 deletions

View file

@ -280,7 +280,7 @@ class IncompatibleFilesystemEncodingError(Error):
"""Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable."""
def __init__(self, repository, key, manifest, name, cache=None, create=False,
checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, nobsdflags=False,
checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, nobirthtime=False, nobsdflags=False,
progress=False, chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None,
consider_part_files=False, log_json=False):
self.cwd = os.getcwd()
@ -298,6 +298,7 @@ def __init__(self, repository, key, manifest, name, cache=None, create=False,
self.numeric_owner = numeric_owner
self.noatime = noatime
self.noctime = noctime
self.nobirthtime = nobirthtime
self.nobsdflags = nobsdflags
assert (start is None) == (start_monotonic is None), 'Logic error: if start is given, start_monotonic must be given as well and vice versa.'
if start is None:
@ -683,6 +684,18 @@ def restore_attrs(self, path, item, symlink=False, fd=None):
else:
# old archives only had mtime in item metadata
atime = mtime
if 'birthtime' in item:
birthtime = item.birthtime
try:
# This should work on FreeBSD, NetBSD, and Darwin and be harmless on other platforms.
# See utimes(2) on either of the BSDs for details.
if fd:
os.utime(fd, None, ns=(atime, birthtime))
else:
os.utime(path, None, ns=(atime, birthtime), follow_symlinks=False)
except OSError:
# some systems don't support calling utime on a symlink
pass
try:
if fd:
os.utime(fd, None, ns=(atime, mtime))
@ -822,6 +835,9 @@ def stat_simple_attrs(self, st):
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 self.numeric_owner:
attrs['user'] = attrs['group'] = None
else:

View file

@ -536,7 +536,7 @@ def create_inner(archive, cache):
lock_wait=self.lock_wait, permit_adhoc_cache=args.no_cache_sync) as cache:
archive = Archive(repository, key, manifest, args.location.archive, cache=cache,
create=True, checkpoint_interval=args.checkpoint_interval,
numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime,
numeric_owner=args.numeric_owner, noatime=args.noatime, noctime=args.noctime, nobirthtime=args.nobirthtime,
nobsdflags=args.nobsdflags, progress=args.progress,
chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic,
log_json=args.log_json)
@ -2964,6 +2964,8 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
help='do not store atime into archive')
fs_group.add_argument('--noctime', dest='noctime', action='store_true',
help='do not store ctime into archive')
fs_group.add_argument('--nobirthtime', dest='nobirthtime', action='store_true',
help='do not store birthtime (creation date) into archive')
fs_group.add_argument('--nobsdflags', dest='nobsdflags', action='store_true',
help='do not read and store bsdflags (e.g. NODUMP, IMMUTABLE) into archive')
fs_group.add_argument('--ignore-inode', dest='ignore_inode', action='store_true',

View file

@ -1,6 +1,6 @@
# this set must be kept complete, otherwise the RobustUnpacker might malfunction:
ITEM_KEYS = frozenset(['path', 'source', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master',
'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'size',
'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'birthtime', 'size',
'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended',
'part'])

View file

@ -162,6 +162,7 @@ class Item(PropDict):
atime = PropDict._make_property('atime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int)
ctime = PropDict._make_property('ctime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int)
mtime = PropDict._make_property('mtime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int)
birthtime = PropDict._make_property('birthtime', int, 'bigint', encode=int_to_bigint, decode=bigint_to_int)
# size is only present for items with a chunk list and then it is sum(chunk_sizes)
# compatibility note: this is a new feature, in old archives size will be missing.

View file

@ -116,6 +116,28 @@ def is_utime_fully_supported():
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:
return True
except OSError as err:
pass
return False
def no_selinux(x):
# selinux fails our FUSE tests, thus ignore selinux xattrs
SELINUX_KEY = 'security.selinux'

View file

@ -53,7 +53,7 @@
from ..repository import Repository
from . import has_lchflags, has_llfuse
from . import BaseTestCase, changedir, environment_variable, no_selinux
from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported
from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported, is_birthtime_fully_supported
from .platform import fakeroot_detected
from .upgrader import attic_repo
from . import key
@ -493,6 +493,39 @@ def has_noatime(some_file):
# it touched the input file's atime while backing it up
assert sto.st_atime_ns == atime * 1e9
@pytest.mark.skipif(not is_utime_fully_supported(), reason='cannot properly setup and execute test without utime')
@pytest.mark.skipif(not is_birthtime_fully_supported(), reason='cannot properly setup and execute test without birthtime')
def test_birthtime(self):
self.create_test_files()
birthtime, mtime, atime = 946598400, 946684800, 946771200
os.utime('input/file1', (atime, birthtime))
os.utime('input/file1', (atime, mtime))
self.cmd('init', '--encryption=repokey', self.repository_location)
self.cmd('create', self.repository_location + '::test', 'input')
with changedir('output'):
self.cmd('extract', self.repository_location + '::test')
sti = os.stat('input/file1')
sto = os.stat('output/input/file1')
assert int(sti.st_birthtime * 1e9) == int(sto.st_birthtime * 1e9) == birthtime * 1e9
assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9
@pytest.mark.skipif(not is_utime_fully_supported(), reason='cannot properly setup and execute test without utime')
@pytest.mark.skipif(not is_birthtime_fully_supported(), reason='cannot properly setup and execute test without birthtime')
def test_nobirthtime(self):
self.create_test_files()
birthtime, mtime, atime = 946598400, 946684800, 946771200
os.utime('input/file1', (atime, birthtime))
os.utime('input/file1', (atime, mtime))
self.cmd('init', '--encryption=repokey', self.repository_location)
self.cmd('create', '--nobirthtime', self.repository_location + '::test', 'input')
with changedir('output'):
self.cmd('extract', self.repository_location + '::test')
sti = os.stat('input/file1')
sto = os.stat('output/input/file1')
assert int(sti.st_birthtime * 1e9) == birthtime * 1e9
assert int(sto.st_birthtime * 1e9) == mtime * 1e9
assert sti.st_mtime_ns == sto.st_mtime_ns == mtime * 1e9
def _extract_repository_id(self, path):
with Repository(self.repository_path) as repository:
return repository.id