瀏覽代碼

Add borg config command (fixes #3304)

This command works similarly to "git config" - it parses repo and
cache configs to get, set, and delete values. It only works on local
repos so a malicious client can't e.g. override their storage quota
or reset the append_only flag.

Add tests for borg config

Add documentation for borg config

Change manual config edits -> borg config

There were a couple places in the documentation where it was advised
to edit the repository or cache config file, a process that is stream-
lined by borg config.
Milkey Mouse 7 年之前
父節點
當前提交
2ac0bf4980
共有 8 個文件被更改,包括 124 次插入11 次删除
  1. 2 3
      docs/faq.rst
  2. 5 4
      docs/quickstart.rst
  3. 1 0
      docs/usage.rst
  4. 22 0
      docs/usage/config.rst
  5. 3 2
      docs/usage/notes.rst
  6. 67 0
      src/borg/archiver.py
  7. 7 2
      src/borg/helpers.py
  8. 17 0
      src/borg/testsuite/archiver.py

+ 2 - 3
docs/faq.rst

@@ -499,8 +499,7 @@ space for chunks.archive.d (see :issue:`235` for details):
 ::
 
     # this assumes you are working with the same user as the backup.
-    # you can get the REPOID from the "config" file inside the repository.
-    cd ~/.cache/borg/<REPOID>
+    cd ~/.cache/borg/$(borg config /path/to/repo id)
     rm -rf chunks.archive.d ; touch chunks.archive.d
 
 This deletes all the cached archive chunk indexes and replaces the directory
@@ -817,7 +816,7 @@ There are some caveats:
 - If the repository is in "keyfile" encryption mode, the keyfile must
   exist locally or it must be manually moved after performing the upgrade:
 
-  1. Locate the repository ID, contained in the ``config`` file in the repository.
+  1. Get the repository ID with ``borg config /path/to/repo id``.
   2. Locate the attic key file at ``~/.attic/keys/``. The correct key for the
      repository starts with the line ``ATTIC_KEY <repository id>``.
   3. Copy the attic key file to ``~/.config/borg/keys/``

+ 5 - 4
docs/quickstart.rst

@@ -22,10 +22,11 @@ a good amount of free space on the filesystem that has your backup repository
 repositories. See also :ref:`cache-memory-usage`.
 
 Borg doesn't use space reserved for root on repository disks (even when run as root),
-on file systems which do not support this mechanism (e.g. XFS) we recommend to
-reserve some space in Borg itself just to be safe by adjusting the
-``additional_free_space`` setting in the ``[repository]`` section of a repositories
-``config`` file. A good starting point is ``2G``.
+on file systems which do not support this mechanism (e.g. XFS) we recommend to reserve
+some space in Borg itself just to be safe by adjusting the ``additional_free_space``
+setting (a good starting point is ``2G``)::
+
+    borg config /path/to/repo additional_free_space 2G
 
 If |project_name| runs out of disk space, it tries to free as much space as it
 can while aborting the current operation safely, which allows to free more space

+ 1 - 0
docs/usage.rst

@@ -51,6 +51,7 @@ Usage
    usage/serve
    usage/lock
    usage/benchmark
+   usage/config
 
    usage/help
    usage/debug

+ 22 - 0
docs/usage/config.rst

@@ -0,0 +1,22 @@
+.. include:: config.rst.inc
+
+.. note::
+
+   The repository & cache config files are some of the only directly manipulable
+   parts of a repository that aren't versioned or backed up, so be careful when
+   making changes\!
+
+Examples
+~~~~~~~~
+::
+
+    # find cache directory
+    $ cd ~/.cache/borg/$(borg config /path/to/repo id)
+
+    # reserve some space
+    $ borg config /path/to/repo additional_free_space 2G
+
+    # make a repo append-only
+    $ borg config /path/to/repo append_only 1
+
+

+ 3 - 2
docs/usage/notes.rst

@@ -149,8 +149,9 @@ reject to delete the repository completely). This is useful for scenarios where
 backup client machine backups remotely to a backup server using ``borg serve``, since
 a hacked client machine cannot delete backups on the server permanently.
 
-To activate append-only mode, edit the repository ``config`` file and add a line
-``append_only=1`` to the ``[repository]`` section (or edit the line if it exists).
+To activate append-only mode, set ``append_only`` to 1 in the repository config::
+
+    borg config /path/to/repo append_only 1
 
 In append-only mode Borg will create a transaction log in the ``transactions`` file,
 where each line is a transaction and a UTC timestamp.

+ 67 - 0
src/borg/archiver.py

@@ -1,5 +1,6 @@
 import argparse
 import collections
+import configparser
 import faulthandler
 import functools
 import hashlib
@@ -1709,6 +1710,41 @@ class Archiver:
             # see issue #1867.
             repository.commit()
 
+    @with_repository(exclusive=True, cache=True, compatibility=(Manifest.Operation.WRITE,))
+    def do_config(self, args, repository, manifest, key, cache):
+        """get, set, and delete values in a repository or cache config file"""
+        try:
+            section, name = args.name.split('.')
+        except ValueError:
+            section = args.cache and "cache" or "repository"
+            name = args.name
+
+        if args.cache:
+            cache.cache_config.load()
+            config = cache.cache_config._config
+            save = cache.cache_config.save
+        else:
+            config = repository.config
+            save = lambda: repository.save_config(repository.path, repository.config)
+
+        if args.delete:
+            config.remove_option(section, name)
+            if len(config.options(section)) == 0:
+                config.remove_section(section)
+            save()
+        elif args.value:
+            if section not in config.sections():
+                config.add_section(section)
+            config.set(section, name, args.value)
+            save()
+        else:
+            try:
+                print(config.get(section, name))
+            except (configparser.NoOptionError, configparser.NoSectionError) as e:
+                print(e, file=sys.stderr)
+                return EXIT_WARNING
+        return EXIT_SUCCESS
+
     def do_debug_info(self, args):
         """display system information for debugging / bug reports"""
         print(sysinfo())
@@ -3681,6 +3717,37 @@ class Archiver:
         subparser.add_argument('args', metavar='ARGS', nargs=argparse.REMAINDER,
                                help='command arguments')
 
+        config_epilog = process_epilog("""
+        This command gets and sets options in a local repository or cache config file.
+        For security reasons, this command only works on local repositories.
+
+        To delete a config value entirely, use ``--delete``. To get an existing key, pass
+        only the key name. To set a key, pass both the key name and the new value. Keys
+        can be specified in the format "section.name" or simply "name"; the section will
+        default to "repository" and "cache" for the repo and cache configs, respectively.
+
+        By default, borg config manipulates the repository config file. Using ``--cache``
+        edits the repository cache's config file instead.
+        """)
+        subparser = subparsers.add_parser('config', parents=[common_parser], add_help=False,
+                                          description=self.do_config.__doc__,
+                                          epilog=config_epilog,
+                                          formatter_class=argparse.RawDescriptionHelpFormatter,
+                                          help='get and set repository config options')
+        subparser.set_defaults(func=self.do_config)
+        subparser.add_argument('-c', '--cache', dest='cache', action='store_true',
+                               help='get and set values from the repo cache')
+        subparser.add_argument('-d', '--delete', dest='delete', action='store_true',
+                               help='delete the key from the config file')
+
+        subparser.add_argument('location', metavar='REPOSITORY',
+                               type=location_validator(archive=False, proto='file'),
+                               help='repository to configure')
+        subparser.add_argument('name', metavar='NAME',
+                               help='name of config key')
+        subparser.add_argument('value', metavar='VALUE', nargs='?',
+                               help='new value for key')
+
         subparser = subparsers.add_parser('help', parents=[common_parser], add_help=False,
                                           description='Extra help')
         subparser.add_argument('--epilog-only', dest='epilog_only', action='store_true')

+ 7 - 2
src/borg/helpers.py

@@ -1139,7 +1139,7 @@ class Location:
                                            path)
 
 
-def location_validator(archive=None):
+def location_validator(archive=None, proto=None):
     def validator(text):
         try:
             loc = Location(text)
@@ -1148,7 +1148,12 @@ def location_validator(archive=None):
         if archive is True and not loc.archive:
             raise argparse.ArgumentTypeError('"%s": No archive specified' % text)
         elif archive is False and loc.archive:
-            raise argparse.ArgumentTypeError('"%s" No archive can be specified' % text)
+            raise argparse.ArgumentTypeError('"%s": No archive can be specified' % text)
+        if proto is not None and loc.proto != proto:
+            if proto == 'file':
+                raise argparse.ArgumentTypeError('"%s": Repository must be local' % text)
+            else:
+                raise argparse.ArgumentTypeError('"%s": Repository must be remote' % text)
         return loc
     return validator
 

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

@@ -2778,6 +2778,19 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
         with environment_variable(_BORG_BENCHMARK_CRUD_TEST='YES'):
             self.cmd('benchmark', 'crud', self.repository_location, self.input_path)
 
+    def test_config(self):
+        self.create_test_files()
+        os.unlink('input/flagfile')
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        for flags in [[], ['--cache']]:
+            for key in {'testkey', 'testsection.testkey'}:
+                self.cmd('config', self.repository_location, *flags, key, exit_code=1)
+                self.cmd('config', self.repository_location, *flags, key, 'testcontents')
+                output = self.cmd('config', self.repository_location, *flags, key)
+                assert output == 'testcontents\n'
+                self.cmd('config', self.repository_location, *flags, '--delete', key)
+                self.cmd('config', self.repository_location, *flags, key, exit_code=1)
+
     requires_gnutar = pytest.mark.skipif(not have_gnutar(), reason='GNU tar must be installed for this test.')
     requires_gzip = pytest.mark.skipif(not shutil.which('gzip'), reason='gzip must be installed for this test.')
 
@@ -3260,6 +3273,10 @@ class RemoteArchiverTestCase(ArchiverTestCase):
     def test_debug_put_get_delete_obj(self):
         pass
 
+    @unittest.skip('only works locally')
+    def test_config(self):
+        pass
+
     def test_strip_components_doesnt_leak(self):
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.create_regular_file('dir/file', contents=b"test file contents 1")