浏览代码

info: add --json option

Marian Beermann 8 年之前
父节点
当前提交
cc26bdf810
共有 3 个文件被更改,包括 65 次插入21 次删除
  1. 44 14
      src/borg/archiver.py
  2. 11 7
      src/borg/cache.py
  3. 10 0
      src/borg/testsuite/archiver.py

+ 44 - 14
src/borg/archiver.py

@@ -65,6 +65,10 @@ from .upgrader import AtticRepositoryUpgrader, BorgRepositoryUpgrader
 STATS_HEADER = "                       Original size      Compressed size    Deduplicated size"
 STATS_HEADER = "                       Original size      Compressed size    Deduplicated size"
 
 
 
 
+def print_as_json(obj):
+    print(json.dumps(obj, sort_keys=True, indent=4))
+
+
 def argument(args, str_or_bool):
 def argument(args, str_or_bool):
     """If bool is passed, return it. If str is passed, retrieve named attribute from args."""
     """If bool is passed, return it. If str is passed, retrieve named attribute from args."""
     if isinstance(str_or_bool, str):
     if isinstance(str_or_bool, str):
@@ -960,7 +964,7 @@ class Archiver:
         if any((args.location.archive, args.first, args.last, args.prefix)):
         if any((args.location.archive, args.first, args.last, args.prefix)):
             return self._info_archives(args, repository, manifest, key, cache)
             return self._info_archives(args, repository, manifest, key, cache)
         else:
         else:
-            return self._info_repository(repository, key, cache)
+            return self._info_repository(args, repository, key, cache)
 
 
     def _info_archives(self, args, repository, manifest, key, cache):
     def _info_archives(self, args, repository, manifest, key, cache):
         def format_cmdline(cmdline):
         def format_cmdline(cmdline):
@@ -998,20 +1002,44 @@ class Archiver:
                 print()
                 print()
         return self.exit_code
         return self.exit_code
 
 
-    def _info_repository(self, repository, key, cache):
-        print('Repository ID: %s' % bin_to_hex(repository.id))
-        if key.NAME == 'plaintext':
-            encrypted = 'No'
+    def _info_repository(self, args, repository, key, cache):
+        if args.json:
+            encryption = {
+                'mode': key.NAME,
+            }
+            if key.NAME.startswith('key file'):
+                encryption['keyfile'] = key.find_key()
+        else:
+            encryption = 'Encrypted: '
+            if key.NAME == 'plaintext':
+                encryption += 'No'
+            else:
+                encryption += 'Yes (%s)' % key.NAME
+            if key.NAME.startswith('key file'):
+                encryption += '\nKey file: %s' % key.find_key()
+
+        info = {
+            'id': bin_to_hex(repository.id),
+            'location': repository._location.canonical_path(),
+            'cache': cache.path,
+            'security_dir': cache.security_manager.dir,
+            'encryption': encryption,
+        }
+
+        if args.json:
+            info['cache-stats'] = cache.stats()
+            print_as_json(info)
         else:
         else:
-            encrypted = 'Yes (%s)' % key.NAME
-        print('Encrypted: %s' % encrypted)
-        if key.NAME.startswith('key file'):
-            print('Key file: %s' % key.find_key())
-        print('Cache: %s' % cache.path)
-        print('Security dir: %s' % cache.security_manager.dir)
-        print(DASHES)
-        print(STATS_HEADER)
-        print(str(cache))
+            print(textwrap.dedent("""
+            Repository ID: {id}
+            Location: {location}
+            {encryption}
+            Cache: {cache}
+            Security dir: {security_dir}
+            """).strip().format_map(info))
+            print(DASHES)
+            print(STATS_HEADER)
+            print(str(cache))
         return self.exit_code
         return self.exit_code
 
 
     @with_repository(exclusive=True)
     @with_repository(exclusive=True)
@@ -2542,6 +2570,8 @@ class Archiver:
         subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='',
         subparser.add_argument('location', metavar='REPOSITORY_OR_ARCHIVE', nargs='?', default='',
                                type=location_validator(),
                                type=location_validator(),
                                help='archive or repository to display information about')
                                help='archive or repository to display information about')
+        subparser.add_argument('--json', action='store_true',
+                               help='format output as JSON')
         self.add_archives_filters_args(subparser)
         self.add_archives_filters_args(subparser)
 
 
         break_lock_epilog = process_epilog("""
         break_lock_epilog = process_epilog("""

+ 11 - 7
src/borg/cache.py

@@ -219,18 +219,22 @@ All archives:   {0.total_size:>20s} {0.total_csize:>20s} {0.unique_csize:>20s}
 Chunk index:    {0.total_unique_chunks:20d} {0.total_chunks:20d}"""
 Chunk index:    {0.total_unique_chunks:20d} {0.total_chunks:20d}"""
         return fmt.format(self.format_tuple())
         return fmt.format(self.format_tuple())
 
 
-    def format_tuple(self):
+    Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks',
+                                     'total_chunks'])
+
+    def stats(self):
         # XXX: this should really be moved down to `hashindex.pyx`
         # XXX: this should really be moved down to `hashindex.pyx`
-        Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', 'total_chunks'])
-        stats = Summary(*self.chunks.summarize())._asdict()
+        stats = self.Summary(*self.chunks.summarize())._asdict()
+        return stats
+
+    def format_tuple(self):
+        stats = self.stats()
         for field in ['total_size', 'total_csize', 'unique_csize']:
         for field in ['total_size', 'total_csize', 'unique_csize']:
             stats[field] = format_file_size(stats[field])
             stats[field] = format_file_size(stats[field])
-        return Summary(**stats)
+        return self.Summary(**stats)
 
 
     def chunks_stored_size(self):
     def chunks_stored_size(self):
-        Summary = namedtuple('Summary', ['total_size', 'total_csize', 'unique_size', 'unique_csize', 'total_unique_chunks', 'total_chunks'])
-        stats = Summary(*self.chunks.summarize())
-        return stats.unique_csize
+        return self.stats()['unique_csize']
 
 
     def create(self):
     def create(self):
         """Create a new empty cache at `self.path`
         """Create a new empty cache at `self.path`

+ 10 - 0
src/borg/testsuite/archiver.py

@@ -1112,6 +1112,16 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         info_archive = self.cmd('info', '--first', '1', self.repository_location)
         info_archive = self.cmd('info', '--first', '1', self.repository_location)
         assert 'Archive name: test\n' in info_archive
         assert 'Archive name: test\n' in info_archive
 
 
+    def test_info_json(self):
+        self.create_regular_file('file1', size=1024 * 80)
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        self.cmd('create', self.repository_location + '::test', 'input')
+        info_repo = json.loads(self.cmd('info', '--json', self.repository_location))
+        assert len(info_repo['id']) == 64
+        assert info_repo['encryption']['mode'] == 'repokey'
+        assert 'keyfile' not in info_repo['encryption']
+        assert 'cache-stats' in info_repo
+
     def test_comment(self):
     def test_comment(self):
         self.create_regular_file('file1', size=1024 * 80)
         self.create_regular_file('file1', size=1024 * 80)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.cmd('init', '--encryption=repokey', self.repository_location)