2
0
Эх сурвалжийг харах

Merge pull request #2792 from ThomasWaldmann/comment-placeholder

support borg list repo --format {comment}, fixes #2081
TW 8 жил өмнө
parent
commit
e36be956f1

+ 4 - 1
src/borg/archive.py

@@ -291,7 +291,9 @@ class Archive:
         self.hard_links = {}
         self.stats = Statistics(output_json=log_json)
         self.show_progress = progress
-        self.name = name
+        self.name = name  # overwritten later with name from archive metadata
+        self.name_in_manifest = name  # can differ from .name later (if borg check fixed duplicate archive names)
+        self.comment = None
         self.checkpoint_interval = checkpoint_interval
         self.numeric_owner = numeric_owner
         self.noatime = noatime
@@ -340,6 +342,7 @@ class Archive:
         self.metadata = self._load_meta(self.id)
         self.metadata.cmdline = [safe_decode(arg) for arg in self.metadata.cmdline]
         self.name = self.metadata.name
+        self.comment = self.metadata.get('comment', '')
 
     @property
     def ts(self):

+ 3 - 3
src/borg/archiver.py

@@ -1322,7 +1322,7 @@ class Archiver:
             if args.json_lines:
                 self.print_error('The --json-lines option is only valid for listing archive contents, not archives.')
                 return self.exit_code
-            return self._list_repository(args, manifest, write)
+            return self._list_repository(args, repository, manifest, key, write)
 
     def _list_archive(self, args, repository, manifest, key, write):
         matcher = self.build_matcher(args.patterns, args.paths)
@@ -1350,14 +1350,14 @@ class Archiver:
 
         return self.exit_code
 
-    def _list_repository(self, args, manifest, write):
+    def _list_repository(self, args, repository, manifest, key, write):
         if args.format is not None:
             format = args.format
         elif args.short:
             format = "{archive}{NL}"
         else:
             format = "{archive:<36} {time} [{id}]{NL}"
-        formatter = ArchiveFormatter(format)
+        formatter = ArchiveFormatter(format, repository, manifest, key, json=args.json)
 
         output_data = []
 

+ 100 - 20
src/borg/helpers.py

@@ -1589,28 +1589,107 @@ class BaseFormatter:
 
 
 class ArchiveFormatter(BaseFormatter):
+    KEY_DESCRIPTIONS = {
+        'name': 'archive name interpreted as text (might be missing non-text characters, see barchive)',
+        'archive': 'archive name interpreted as text (might be missing non-text characters, see barchive)',
+        'barchive': 'verbatim archive name, can contain any character except NUL',
+        'comment': 'archive comment interpreted as text (might be missing non-text characters, see bcomment)',
+        'bcomment': 'verbatim archive comment, can contain any character except NUL',
+        'time': 'time (start) of creation of the archive',
+        # *start* is the key used by borg-info for this timestamp, this makes the formats more compatible
+        'start': 'time (start) of creation of the archive',
+        'end': 'time (end) of creation of the archive',
+        'id': 'internal ID of the archive',
+    }
+    KEY_GROUPS = (
+        ('name', 'archive', 'barchive', 'comment', 'bcomment', 'id'),
+        ('time', 'start', 'end'),
+    )
+
+    @classmethod
+    def available_keys(cls):
+        fake_archive_info = ArchiveInfo('archivename', b'\1'*32, datetime(1970, 1, 1, tzinfo=timezone.utc))
+        formatter = cls('', None, None, None)
+        keys = []
+        keys.extend(formatter.call_keys.keys())
+        keys.extend(formatter.get_item_data(fake_archive_info).keys())
+        return keys
+
+    @classmethod
+    def keys_help(cls):
+        help = []
+        keys = cls.available_keys()
+        for key in cls.FIXED_KEYS:
+            keys.remove(key)
 
-    def __init__(self, format):
-        self.format = partial_format(format, self.FIXED_KEYS)
-
-    def get_item_data(self, archive):
-        return {
-            # *name* is the key used by borg-info for the archive name, this makes the formats more compatible
-            'name': remove_surrogates(archive.name),
-            'barchive': archive.name,
-            'archive': remove_surrogates(archive.name),
-            'id': bin_to_hex(archive.id),
-            'time': format_time(to_localtime(archive.ts)),
-            # *start* is the key used by borg-info for this timestamp, this makes the formats more compatible
-            'start': format_time(to_localtime(archive.ts)),
+        for group in cls.KEY_GROUPS:
+            for key in group:
+                keys.remove(key)
+                text = "- " + key
+                if key in cls.KEY_DESCRIPTIONS:
+                    text += ": " + cls.KEY_DESCRIPTIONS[key]
+                help.append(text)
+            help.append("")
+        assert not keys, str(keys)
+        return "\n".join(help)
+
+    def __init__(self, format, repository, manifest, key, *, json=False):
+        self.repository = repository
+        self.manifest = manifest
+        self.key = key
+        self.name = None
+        self.id = None
+        self._archive = None
+        self.json = json
+        static_keys = {}  # here could be stuff on repo level, above archive level
+        static_keys.update(self.FIXED_KEYS)
+        self.format = partial_format(format, static_keys)
+        self.format_keys = {f[1] for f in Formatter().parse(format)}
+        self.call_keys = {
+            'comment': partial(self.get_comment, rs=True),
+            'bcomment': partial(self.get_comment, rs=False),
+            'end': self.get_ts_end,
         }
+        self.used_call_keys = set(self.call_keys) & self.format_keys
+        if self.json:
+            self.item_data = {}
+            self.format_item = self.format_item_json
+        else:
+            self.item_data = static_keys
 
-    @staticmethod
-    def keys_help():
-        return "- archive, name: archive name interpreted as text (might be missing non-text characters, see barchive)\n" \
-               "- barchive: verbatim archive name, can contain any character except NUL\n" \
-               "- time: time of creation of the archive\n" \
-               "- id: internal ID of the archive"
+    def format_item_json(self, item):
+        return json.dumps(self.get_item_data(item)) + '\n'
+
+    def get_item_data(self, archive_info):
+        self.name = archive_info.name
+        self.id = archive_info.id
+        item_data = {}
+        item_data.update(self.item_data)
+        item_data.update({
+            'name': remove_surrogates(archive_info.name),
+            '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)),
+        })
+        for key in self.used_call_keys:
+            item_data[key] = self.call_keys[key]()
+        return item_data
+
+    @property
+    def archive(self):
+        """lazy load / update loaded archive"""
+        if self._archive is None or self._archive.id != self.id:
+            from .archive import Archive
+            self._archive = Archive(self.repository, self.key, self.manifest, self.name)
+        return self._archive
+
+    def get_comment(self, rs):
+        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))
 
 
 class ItemFormatter(BaseFormatter):
@@ -1716,9 +1795,10 @@ class ItemFormatter(BaseFormatter):
         self.used_call_keys = set(self.call_keys) & self.format_keys
 
     def get_item_data(self, item):
+        item_data = {}
+        item_data.update(self.item_data)
         mode = stat.filemode(item.mode)
         item_type = mode[0]
-        item_data = self.item_data
 
         source = item.get('source', '')
         extra = ''

+ 5 - 2
src/borg/testsuite/archiver.py

@@ -1791,8 +1791,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
     def test_list_repository_format(self):
         self.cmd('init', '--encryption=repokey', self.repository_location)
-        self.cmd('create', self.repository_location + '::test-1', src_dir)
-        self.cmd('create', self.repository_location + '::test-2', src_dir)
+        self.cmd('create', '--comment', 'comment 1', self.repository_location + '::test-1', src_dir)
+        self.cmd('create', '--comment', 'comment 2', self.repository_location + '::test-2', src_dir)
         output_1 = self.cmd('list', self.repository_location)
         output_2 = self.cmd('list', '--format', '{archive:<36} {time} [{id}]{NL}', self.repository_location)
         self.assertEqual(output_1, output_2)
@@ -1800,6 +1800,9 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.assertEqual(output_1, 'test-1\ntest-2\n')
         output_1 = self.cmd('list', '--format', '{barchive}/', self.repository_location)
         self.assertEqual(output_1, 'test-1/test-2/')
+        output_3 = self.cmd('list', '--format', '{name} {comment}{NL}', self.repository_location)
+        self.assert_in('test-1 comment 1\n', output_3)
+        self.assert_in('test-2 comment 2\n', output_3)
 
     def test_list_hash(self):
         self.create_regular_file('empty_file', size=0)