diff --git a/src/borg/archiver.py b/src/borg/archiver.py index c0e847425b..b0e425a8ff 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -4457,6 +4457,8 @@ def parse_args(self, args=None): self.do_list, self.do_mount, self.do_umount} if func not in bypass_allowed: raise Error('Not allowed to bypass locking mechanism for chosen command') + if getattr(args, 'timestamp', None): + args.location = args.location.with_timestamp(args.timestamp) return args def prerun_checks(self, logger, is_serve): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 81f253108b..f61e15b71a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -606,7 +606,7 @@ def timestamp(s): try: # is it pointing to a file / directory? ts = safe_s(os.stat(s).st_mtime) - return datetime.utcfromtimestamp(ts) + return datetime.fromtimestamp(ts, tz=timezone.utc) except OSError: # didn't work, try parsing as timestamp. UTC, no TZ, no microsecs support. for format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S+00:00', @@ -615,7 +615,7 @@ def timestamp(s): '%Y-%m-%d', '%Y-%j', ): try: - return datetime.strptime(s, format) + return datetime.strptime(s, format).replace(tzinfo=timezone.utc) except ValueError: continue raise ValueError @@ -718,7 +718,7 @@ def format_line(format, data): raise PlaceholderError(format, data, e.__class__.__name__, str(e)) -def replace_placeholders(text): +def replace_placeholders(text, overrides={}): """Replace placeholders in text with their values.""" from .platform import fqdn, hostname current_time = datetime.now(timezone.utc) @@ -735,6 +735,7 @@ def replace_placeholders(text): 'borgmajor': '%d' % borg_version_tuple[:1], 'borgminor': '%d.%d' % borg_version_tuple[:2], 'borgpatch': '%d.%d.%d' % borg_version_tuple[:3], + **overrides, } return format_line(text, data) @@ -1103,13 +1104,13 @@ class Location: | # or """ + optional_archive_re, re.VERBOSE) # archive name (optional, may be empty) - def __init__(self, text=''): - if not self.parse(text): + def __init__(self, text='', overrides={}): + if not self.parse(text, overrides): raise ValueError('Invalid location format: "%s"' % self.orig) - def parse(self, text): + def parse(self, text, overrides={}): self.orig = text - text = replace_placeholders(text) + text = replace_placeholders(text, overrides) valid = self._parse(text) if valid: return True @@ -1202,6 +1203,12 @@ def canonical_path(self): ':{}'.format(self.port) if self.port else '', path) + def with_timestamp(self, timestamp): + return Location(self.orig, overrides={ + 'now': DatetimeWrapper(timestamp.astimezone(None)), + 'utcnow': DatetimeWrapper(timestamp), + }) + def location_validator(archive=None, proto=None): def validator(text): diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py index 83ba9c3859..43ccb1d9b2 100644 --- a/src/borg/testsuite/helpers.py +++ b/src/borg/testsuite/helpers.py @@ -189,6 +189,10 @@ def test_user_parsing(self): assert repr(Location('ssh://host/path::2016-12-31@23:59:59')) == \ "Location(proto='ssh', user=None, host='host', port=None, path='/path', archive='2016-12-31@23:59:59')" + def test_with_timestamp(self): + assert repr(Location('path::archive-{utcnow}').with_timestamp(datetime(2002, 9, 19, tzinfo=timezone.utc))) == \ + "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive-2002-09-19T00:00:00')" + def test_underspecified(self, monkeypatch): monkeypatch.delenv('BORG_REPO', raising=False) with pytest.raises(ValueError): @@ -894,6 +898,10 @@ def test_replace_placeholders(): assert int(replace_placeholders('{now:%Y}')) == now.year +def test_override_placeholders(): + assert replace_placeholders('{uuid4}', overrides={'uuid4': "overridden"}) == "overridden" + + def working_swidth(): return platform.swidth('선') == 2