diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index c8638aa38..c8e652951 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -18,7 +18,7 @@ logger = create_logger() from .errors import Error from .fs import get_keys_dir -from .time import format_time, to_localtime, safe_timestamp, safe_s +from .time import format_time, isoformat_time, to_localtime, safe_timestamp, safe_s from .usergroup import uid2user from .. import __version__ as borg_version from .. import __version_tuple__ as borg_version_tuple @@ -549,6 +549,7 @@ class ArchiveFormatter(BaseFormatter): if self.json: self.item_data = {} self.format_item = self.format_item_json + self.format_time = self.format_time_json else: self.item_data = static_keys @@ -565,8 +566,8 @@ class ArchiveFormatter(BaseFormatter): 'archive': remove_surrogates(archive_info.name), 'barchive': archive_info.name, 'id': bin_to_hex(archive_info.id), - 'time': format_time(to_localtime(archive_info.ts)), - 'start': format_time(to_localtime(archive_info.ts)), + 'time': self.format_time(archive_info.ts), + 'start': self.format_time(archive_info.ts), }) for key in self.used_call_keys: item_data[key] = self.call_keys[key]() @@ -584,7 +585,15 @@ class ArchiveFormatter(BaseFormatter): return remove_surrogates(self.archive.comment) if rs else self.archive.comment def get_ts_end(self): - return format_time(to_localtime(self.archive.ts_end)) + return self.format_time(self.archive.ts_end) + + def format_time(self, ts): + t = to_localtime(ts) + return format_time(t) + + def format_time_json(self, ts): + t = to_localtime(ts) + return isoformat_time(t) class ItemFormatter(BaseFormatter): @@ -657,6 +666,12 @@ class ItemFormatter(BaseFormatter): 'archiveid': archive.fpr, } static_keys.update(self.FIXED_KEYS) + if self.json_lines: + self.item_data = {} + self.format_item = self.format_item_json + self.format_time = self.format_time_json + else: + self.item_data = static_keys self.format = partial_format(format, static_keys) self.format_keys = {f[1] for f in Formatter().parse(format)} self.call_keys = { @@ -676,11 +691,6 @@ class ItemFormatter(BaseFormatter): for hash_function in hashlib.algorithms_guaranteed: self.add_key(hash_function, partial(self.hash_item, hash_function)) self.used_call_keys = set(self.call_keys) & self.format_keys - if self.json_lines: - self.item_data = {} - self.format_item = self.format_item_json - else: - self.item_data = static_keys def format_item_json(self, item): return json.dumps(self.get_item_data(item)) + '\n' @@ -758,7 +768,12 @@ class ItemFormatter(BaseFormatter): return hash.hexdigest() def format_time(self, key, item): - return format_time(safe_timestamp(item.get(key) or item.mtime)) + t = self.time(key, item) + return format_time(t) + + def format_time_json(self, key, item): + t = self.time(key, item) + return isoformat_time(t) def time(self, key, item): return safe_timestamp(item.get(key) or item.mtime) @@ -884,7 +899,7 @@ def basic_json_data(manifest, *, cache=None, extra=None): 'mode': key.ARG_NAME, }, }) - data['repository']['last_modified'] = format_time(to_localtime(manifest.last_timestamp.replace(tzinfo=timezone.utc))) + data['repository']['last_modified'] = isoformat_time(to_localtime(manifest.last_timestamp.replace(tzinfo=timezone.utc))) if key.NAME.startswith('key file'): data['encryption']['keyfile'] = key.find_key() if cache: diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index 44e49546f..7203ba2fa 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -87,11 +87,17 @@ def safe_timestamp(item_timestamp_ns): def format_time(t): - """use ISO-8601 date and time format + """use ISO-8601-like date and time format (human readable, with wkday and blank date/time separator) """ return t.strftime('%a, %Y-%m-%d %H:%M:%S') +def isoformat_time(t): + """use ISO-8601 date and time format (machine readable, no wkday, no microseconds either) + """ + return t.strftime('%Y-%m-%dT%H:%M:%S') # note: first make all datetime objects tz aware before adding %z here. + + def format_timedelta(td): """Format timedelta in a human friendly format """ diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 0d28eb310..0eaea5e74 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -60,6 +60,8 @@ from . import key src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +ISO_FORMAT = '%Y-%m-%dT%H:%M:%S' + def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', binary_output=False, **kw): if fork: @@ -1304,6 +1306,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): repository = info_repo['repository'] assert len(repository['id']) == 64 assert 'last_modified' in repository + assert datetime.strptime(repository['last_modified'], ISO_FORMAT) # must not raise assert info_repo['encryption']['mode'] == 'repokey' assert 'keyfile' not in info_repo['encryption'] cache = info_repo['cache'] @@ -1846,9 +1849,11 @@ class ArchiverTestCase(ArchiverTestCaseBase): list_repo = json.loads(self.cmd('list', '--json', self.repository_location)) repository = list_repo['repository'] assert len(repository['id']) == 64 - assert 'last_modified' in repository + assert datetime.strptime(repository['last_modified'], ISO_FORMAT) # must not raise assert list_repo['encryption']['mode'] == 'repokey' assert 'keyfile' not in list_repo['encryption'] + archive0 = list_repo['archives'][0] + assert datetime.strptime(archive0['time'], ISO_FORMAT) # must not raise list_archive = self.cmd('list', '--json-lines', self.repository_location + '::test') items = [json.loads(s) for s in list_archive.splitlines()] @@ -1856,6 +1861,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): file1 = items[1] assert file1['path'] == 'input/file1' assert file1['size'] == 81920 + assert datetime.strptime(file1['isomtime'], ISO_FORMAT) # must not raise list_archive = self.cmd('list', '--json-lines', '--format={sha256}', self.repository_location + '::test') items = [json.loads(s) for s in list_archive.splitlines()]