ソースを参照

Merge pull request #2928 from ThomasWaldmann/json-isoformat

Json isoformat
TW 7 年 前
コミット
72c8ec2583
3 ファイル変更40 行追加13 行削除
  1. 26 11
      src/borg/helpers/parseformat.py
  2. 7 1
      src/borg/helpers/time.py
  3. 7 1
      src/borg/testsuite/archiver.py

+ 26 - 11
src/borg/helpers/parseformat.py

@@ -18,7 +18,7 @@ logger = create_logger()
 
 
 from .errors import Error
 from .errors import Error
 from .fs import get_keys_dir
 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 .usergroup import uid2user
 from .. import __version__ as borg_version
 from .. import __version__ as borg_version
 from .. import __version_tuple__ as borg_version_tuple
 from .. import __version_tuple__ as borg_version_tuple
@@ -549,6 +549,7 @@ class ArchiveFormatter(BaseFormatter):
         if self.json:
         if self.json:
             self.item_data = {}
             self.item_data = {}
             self.format_item = self.format_item_json
             self.format_item = self.format_item_json
+            self.format_time = self.format_time_json
         else:
         else:
             self.item_data = static_keys
             self.item_data = static_keys
 
 
@@ -565,8 +566,8 @@ class ArchiveFormatter(BaseFormatter):
             'archive': remove_surrogates(archive_info.name),
             'archive': remove_surrogates(archive_info.name),
             'barchive': archive_info.name,
             'barchive': archive_info.name,
             'id': bin_to_hex(archive_info.id),
             '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:
         for key in self.used_call_keys:
             item_data[key] = self.call_keys[key]()
             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
         return remove_surrogates(self.archive.comment) if rs else self.archive.comment
 
 
     def get_ts_end(self):
     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):
 class ItemFormatter(BaseFormatter):
@@ -657,6 +666,12 @@ class ItemFormatter(BaseFormatter):
             'archiveid': archive.fpr,
             'archiveid': archive.fpr,
         }
         }
         static_keys.update(self.FIXED_KEYS)
         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 = partial_format(format, static_keys)
         self.format_keys = {f[1] for f in Formatter().parse(format)}
         self.format_keys = {f[1] for f in Formatter().parse(format)}
         self.call_keys = {
         self.call_keys = {
@@ -676,11 +691,6 @@ class ItemFormatter(BaseFormatter):
         for hash_function in hashlib.algorithms_guaranteed:
         for hash_function in hashlib.algorithms_guaranteed:
             self.add_key(hash_function, partial(self.hash_item, hash_function))
             self.add_key(hash_function, partial(self.hash_item, hash_function))
         self.used_call_keys = set(self.call_keys) & self.format_keys
         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):
     def format_item_json(self, item):
         return json.dumps(self.get_item_data(item)) + '\n'
         return json.dumps(self.get_item_data(item)) + '\n'
@@ -758,7 +768,12 @@ class ItemFormatter(BaseFormatter):
         return hash.hexdigest()
         return hash.hexdigest()
 
 
     def format_time(self, key, item):
     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):
     def time(self, key, item):
         return safe_timestamp(item.get(key) or item.mtime)
         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,
             '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'):
     if key.NAME.startswith('key file'):
         data['encryption']['keyfile'] = key.find_key()
         data['encryption']['keyfile'] = key.find_key()
     if cache:
     if cache:

+ 7 - 1
src/borg/helpers/time.py

@@ -87,11 +87,17 @@ def safe_timestamp(item_timestamp_ns):
 
 
 
 
 def format_time(t):
 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')
     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):
 def format_timedelta(td):
     """Format timedelta in a human friendly format
     """Format timedelta in a human friendly format
     """
     """

+ 7 - 1
src/borg/testsuite/archiver.py

@@ -60,6 +60,8 @@ from . import key
 
 
 src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 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):
 def exec_cmd(*args, archiver=None, fork=False, exe=None, input=b'', binary_output=False, **kw):
     if fork:
     if fork:
@@ -1304,6 +1306,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         repository = info_repo['repository']
         repository = info_repo['repository']
         assert len(repository['id']) == 64
         assert len(repository['id']) == 64
         assert 'last_modified' in repository
         assert 'last_modified' in repository
+        assert datetime.strptime(repository['last_modified'], ISO_FORMAT)  # must not raise
         assert info_repo['encryption']['mode'] == 'repokey'
         assert info_repo['encryption']['mode'] == 'repokey'
         assert 'keyfile' not in info_repo['encryption']
         assert 'keyfile' not in info_repo['encryption']
         cache = info_repo['cache']
         cache = info_repo['cache']
@@ -1846,9 +1849,11 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         list_repo = json.loads(self.cmd('list', '--json', self.repository_location))
         list_repo = json.loads(self.cmd('list', '--json', self.repository_location))
         repository = list_repo['repository']
         repository = list_repo['repository']
         assert len(repository['id']) == 64
         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 list_repo['encryption']['mode'] == 'repokey'
         assert 'keyfile' not in list_repo['encryption']
         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')
         list_archive = self.cmd('list', '--json-lines', self.repository_location + '::test')
         items = [json.loads(s) for s in list_archive.splitlines()]
         items = [json.loads(s) for s in list_archive.splitlines()]
@@ -1856,6 +1861,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         file1 = items[1]
         file1 = items[1]
         assert file1['path'] == 'input/file1'
         assert file1['path'] == 'input/file1'
         assert file1['size'] == 81920
         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')
         list_archive = self.cmd('list', '--json-lines', '--format={sha256}', self.repository_location + '::test')
         items = [json.loads(s) for s in list_archive.splitlines()]
         items = [json.loads(s) for s in list_archive.splitlines()]