Prechádzať zdrojové kódy

Merge pull request #847 from Ape/pr/archive_comments

Add archive comments
TW 9 rokov pred
rodič
commit
938302f2f2

+ 15 - 9
borg/archive.py

@@ -180,7 +180,7 @@ class Archive:
     def load(self, id):
     def load(self, id):
         self.id = id
         self.id = id
         self.metadata = self._load_meta(self.id)
         self.metadata = self._load_meta(self.id)
-        decode_dict(self.metadata, (b'name', b'hostname', b'username', b'time', b'time_end'))
+        decode_dict(self.metadata, (b'name', b'comment', b'hostname', b'username', b'time', b'time_end'))
         self.metadata[b'cmdline'] = [arg.decode('utf-8', 'surrogateescape') for arg in self.metadata[b'cmdline']]
         self.metadata[b'cmdline'] = [arg.decode('utf-8', 'surrogateescape') for arg in self.metadata[b'cmdline']]
         self.name = self.metadata[b'name']
         self.name = self.metadata[b'name']
 
 
@@ -240,7 +240,7 @@ Number of files: {0.stats.nfiles}'''.format(
         del self.manifest.archives[self.checkpoint_name]
         del self.manifest.archives[self.checkpoint_name]
         self.cache.chunk_decref(self.id, self.stats)
         self.cache.chunk_decref(self.id, self.stats)
 
 
-    def save(self, name=None, timestamp=None):
+    def save(self, name=None, comment=None, timestamp=None):
         name = name or self.name
         name = name or self.name
         if name in self.manifest.archives:
         if name in self.manifest.archives:
             raise self.AlreadyExists(name)
             raise self.AlreadyExists(name)
@@ -256,6 +256,7 @@ Number of files: {0.stats.nfiles}'''.format(
         metadata = StableDict({
         metadata = StableDict({
             'version': 1,
             'version': 1,
             'name': name,
             'name': name,
+            'comment': comment,
             'items': self.items_buffer.chunks,
             'items': self.items_buffer.chunks,
             'cmdline': sys.argv,
             'cmdline': sys.argv,
             'hostname': socket.gethostname(),
             'hostname': socket.gethostname(),
@@ -447,17 +448,22 @@ Number of files: {0.stats.nfiles}'''.format(
             except OSError:
             except OSError:
                 pass
                 pass
 
 
-    def rename(self, name):
-        if name in self.manifest.archives:
-            raise self.AlreadyExists(name)
+    def set_meta(self, key, value):
         metadata = StableDict(self._load_meta(self.id))
         metadata = StableDict(self._load_meta(self.id))
-        metadata[b'name'] = name
+        metadata[key] = value
         data = msgpack.packb(metadata, unicode_errors='surrogateescape')
         data = msgpack.packb(metadata, unicode_errors='surrogateescape')
         new_id = self.key.id_hash(data)
         new_id = self.key.id_hash(data)
         self.cache.add_chunk(new_id, data, self.stats)
         self.cache.add_chunk(new_id, data, self.stats)
-        self.manifest.archives[name] = {'id': new_id, 'time': metadata[b'time']}
+        self.manifest.archives[self.name] = {'id': new_id, 'time': metadata[b'time']}
         self.cache.chunk_decref(self.id, self.stats)
         self.cache.chunk_decref(self.id, self.stats)
-        del self.manifest.archives[self.name]
+
+    def rename(self, name):
+        if name in self.manifest.archives:
+            raise self.AlreadyExists(name)
+        oldname = self.name
+        self.name = name
+        self.set_meta(b'name', name)
+        del self.manifest.archives[oldname]
 
 
     def delete(self, stats, progress=False):
     def delete(self, stats, progress=False):
         unpacker = msgpack.Unpacker(use_list=False)
         unpacker = msgpack.Unpacker(use_list=False)
@@ -879,7 +885,7 @@ class ArchiveChecker:
                 archive = StableDict(msgpack.unpackb(data))
                 archive = StableDict(msgpack.unpackb(data))
                 if archive[b'version'] != 1:
                 if archive[b'version'] != 1:
                     raise Exception('Unknown archive metadata version')
                     raise Exception('Unknown archive metadata version')
-                decode_dict(archive, (b'name', b'hostname', b'username', b'time', b'time_end'))
+                decode_dict(archive, (b'name', b'comment', b'hostname', b'username', b'time', b'time_end'))
                 archive[b'cmdline'] = [arg.decode('utf-8', 'surrogateescape') for arg in archive[b'cmdline']]
                 archive[b'cmdline'] = [arg.decode('utf-8', 'surrogateescape') for arg in archive[b'cmdline']]
                 items_buffer = ChunkBuffer(self.key)
                 items_buffer = ChunkBuffer(self.key)
                 items_buffer.write_chunk = add_callback
                 items_buffer.write_chunk = add_callback

+ 29 - 1
borg/archiver.py

@@ -262,7 +262,7 @@ class Archiver:
                               args.keep_tag_files, skip_inodes, path, restrict_dev,
                               args.keep_tag_files, skip_inodes, path, restrict_dev,
                               read_special=args.read_special, dry_run=dry_run)
                               read_special=args.read_special, dry_run=dry_run)
             if not dry_run:
             if not dry_run:
-                archive.save(timestamp=args.timestamp)
+                archive.save(comment=args.comment, timestamp=args.timestamp)
                 if args.progress:
                 if args.progress:
                     archive.stats.show_progress(final=True)
                     archive.stats.show_progress(final=True)
                 if args.stats:
                 if args.stats:
@@ -628,6 +628,16 @@ class Archiver:
         cache.commit()
         cache.commit()
         return self.exit_code
         return self.exit_code
 
 
+    @with_repository(exclusive=True, cache=True)
+    @with_archive
+    def do_comment(self, args, repository, manifest, key, cache, archive):
+        """Set the archive comment"""
+        archive.set_meta(b'comment', args.comment)
+        manifest.write()
+        repository.commit()
+        cache.commit()
+        return self.exit_code
+
     @with_repository(exclusive=True, cache=True)
     @with_repository(exclusive=True, cache=True)
     def do_delete(self, args, repository, manifest, key, cache):
     def do_delete(self, args, repository, manifest, key, cache):
         """Delete an existing repository or archive"""
         """Delete an existing repository or archive"""
@@ -735,6 +745,7 @@ class Archiver:
         stats = archive.calc_stats(cache)
         stats = archive.calc_stats(cache)
         print('Name:', archive.name)
         print('Name:', archive.name)
         print('Fingerprint: %s' % hexlify(archive.id).decode('ascii'))
         print('Fingerprint: %s' % hexlify(archive.id).decode('ascii'))
+        print('Comment:', archive.metadata.get(b'comment', ''))
         print('Hostname:', archive.metadata[b'hostname'])
         print('Hostname:', archive.metadata[b'hostname'])
         print('Username:', archive.metadata[b'username'])
         print('Username:', archive.metadata[b'username'])
         print('Time (start): %s' % format_time(to_localtime(archive.ts)))
         print('Time (start): %s' % format_time(to_localtime(archive.ts)))
@@ -1179,6 +1190,8 @@ class Archiver:
                                           formatter_class=argparse.RawDescriptionHelpFormatter,
                                           formatter_class=argparse.RawDescriptionHelpFormatter,
                                           help='create backup')
                                           help='create backup')
         subparser.set_defaults(func=self.do_create)
         subparser.set_defaults(func=self.do_create)
+        subparser.add_argument('--comment', dest='comment', metavar='COMMENT', default='',
+                               help='add a comment text to the archive')
         subparser.add_argument('-s', '--stats', dest='stats',
         subparser.add_argument('-s', '--stats', dest='stats',
                                action='store_true', default=False,
                                action='store_true', default=False,
                                help='print statistics for the created archive')
                                help='print statistics for the created archive')
@@ -1356,6 +1369,21 @@ class Archiver:
                                type=archivename_validator(),
                                type=archivename_validator(),
                                help='the new archive name to use')
                                help='the new archive name to use')
 
 
+        comment_epilog = textwrap.dedent("""
+        This command sets the archive comment.
+        """)
+        subparser = subparsers.add_parser('comment', parents=[common_parser],
+                                          description=self.do_comment.__doc__,
+                                          epilog=comment_epilog,
+                                          formatter_class=argparse.RawDescriptionHelpFormatter,
+                                          help='set the archive comment')
+        subparser.set_defaults(func=self.do_comment)
+        subparser.add_argument('location', metavar='ARCHIVE',
+                               type=location_validator(archive=True),
+                               help='archive to modify')
+        subparser.add_argument('comment', metavar='COMMENT',
+                               help='the new archive comment')
+
         delete_epilog = textwrap.dedent("""
         delete_epilog = textwrap.dedent("""
         This command deletes an archive from the repository or the complete repository.
         This command deletes an archive from the repository or the complete repository.
         Disk space is reclaimed accordingly. If you delete the complete repository, the
         Disk space is reclaimed accordingly. If you delete the complete repository, the

+ 13 - 0
borg/testsuite/archiver.py

@@ -754,6 +754,19 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.assert_in('test.3', manifest.archives)
         self.assert_in('test.3', manifest.archives)
         self.assert_in('test.4', manifest.archives)
         self.assert_in('test.4', manifest.archives)
 
 
+    def test_comment(self):
+        self.create_regular_file('file1', size=1024 * 80)
+        self.cmd('init', self.repository_location)
+        self.cmd('create', self.repository_location + '::test1', 'input')
+        self.cmd('create', '--comment', 'this is the comment', self.repository_location + '::test2', 'input')
+        assert 'Comment: \n' in self.cmd('info', self.repository_location + '::test1')
+        assert 'Comment: this is the comment' in self.cmd('info', self.repository_location + '::test2')
+
+        self.cmd('comment', self.repository_location + '::test1', 'added comment')
+        self.cmd('comment', self.repository_location + '::test2', 'modified comment')
+        assert 'Comment: added comment' in self.cmd('info', self.repository_location + '::test1')
+        assert 'Comment: modified comment' in self.cmd('info', self.repository_location + '::test2')
+
     def test_delete(self):
     def test_delete(self):
         self.create_regular_file('file1', size=1024 * 80)
         self.create_regular_file('file1', size=1024 * 80)
         self.create_regular_file('dir2/file2', size=1024 * 80)
         self.create_regular_file('dir2/file2', size=1024 * 80)

+ 21 - 1
docs/usage.rst

@@ -335,6 +335,26 @@ Examples
     newname                              Mon, 2016-02-15 19:50:19
     newname                              Mon, 2016-02-15 19:50:19
 
 
 
 
+.. include:: usage/comment.rst.inc
+
+Examples
+~~~~~~~~
+::
+
+    $ borg create --comment "This is a comment" /mnt/backup::archivename ~
+    $ borg info /mnt/backup::archivename
+    Name: archivename
+    Fingerprint: ...
+    Comment: This is a comment
+    ...
+    $ borg comment /mnt/backup::archivename "This is a better comment"
+    $ borg info /mnt/backup::archivename
+    Name: archivename
+    Fingerprint: ...
+    Comment: This is a better comment
+    ...
+
+
 .. include:: usage/list.rst.inc
 .. include:: usage/list.rst.inc
 
 
 Examples
 Examples
@@ -825,4 +845,4 @@ for e.g. regular pruning.
 
 
 Further protections can be implemented, but are outside of Borgs scope. For example,
 Further protections can be implemented, but are outside of Borgs scope. For example,
 file system snapshots or wrapping ``borg serve`` to set special permissions or ACLs on
 file system snapshots or wrapping ``borg serve`` to set special permissions or ACLs on
-new data files.
+new data files.

+ 35 - 0
docs/usage/comment.rst.inc

@@ -0,0 +1,35 @@
+.. _borg_comment:
+
+borg comment
+------------
+::
+
+    usage: borg comment [-h] [-v] [--debug] [--lock-wait N] [--show-version]
+                        [--show-rc] [--no-files-cache] [--umask M]
+                        [--remote-path PATH]
+                        ARCHIVE COMMENT
+    
+    Set the archive comment
+    
+    positional arguments:
+      ARCHIVE               archive to rename
+      COMMENT               the new archive comment
+    
+    optional arguments:
+      -h, --help            show this help message and exit
+      -v, --verbose, --info
+                            enable informative (verbose) output, work on log level
+                            INFO
+      --debug               enable debug output, work on log level DEBUG
+      --lock-wait N         wait for the lock, but max. N seconds (default: 1).
+      --show-version        show/log the borg version
+      --show-rc             show/log the return code (rc)
+      --no-files-cache      do not load/update the file metadata cache used to
+                            detect unchanged files
+      --umask M             set umask to M (local and remote, default: 0077)
+      --remote-path PATH    set remote path to executable (default: "borg")
+    
+Description
+~~~~~~~~~~~
+
+This command sets the archive comment.

+ 2 - 1
docs/usage/create.rst.inc

@@ -6,7 +6,7 @@ borg create
 
 
     usage: borg create [-h] [-v] [--debug] [--lock-wait N] [--show-version]
     usage: borg create [-h] [-v] [--debug] [--lock-wait N] [--show-version]
                        [--show-rc] [--no-files-cache] [--umask M]
                        [--show-rc] [--no-files-cache] [--umask M]
-                       [--remote-path PATH] [-s] [-p] [--list]
+                       [--remote-path PATH] [--comment COMMENT] [-s] [-p] [--list]
                        [--filter STATUSCHARS] [-e PATTERN]
                        [--filter STATUSCHARS] [-e PATTERN]
                        [--exclude-from EXCLUDEFILE] [--exclude-caches]
                        [--exclude-from EXCLUDEFILE] [--exclude-caches]
                        [--exclude-if-present FILENAME] [--keep-tag-files]
                        [--exclude-if-present FILENAME] [--keep-tag-files]
@@ -36,6 +36,7 @@ borg create
                             detect unchanged files
                             detect unchanged files
       --umask M             set umask to M (local and remote, default: 0077)
       --umask M             set umask to M (local and remote, default: 0077)
       --remote-path PATH    set remote path to executable (default: "borg")
       --remote-path PATH    set remote path to executable (default: "borg")
+      --comment COMMENT     add a comment text to the archive
       -s, --stats           print statistics for the created archive
       -s, --stats           print statistics for the created archive
       -p, --progress        show progress display while creating the archive,
       -p, --progress        show progress display while creating the archive,
                             showing Original, Compressed and Deduplicated sizes,
                             showing Original, Compressed and Deduplicated sizes,