Browse Source

info: add --json option

Marian Beermann 8 years ago
parent
commit
cc26bdf810
3 changed files with 65 additions and 21 deletions
  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)