Browse Source

#1: Add support for "borg check --last N" to Borg backend.

Dan Helfman 10 năm trước cách đây
mục cha
commit
2444c4b372

+ 4 - 0
NEWS

@@ -1,3 +1,7 @@
+0.1.3
+
+ * #1: Add support for "borg check --last N" to Borg backend.
+
 0.1.2
 
  * As a convenience to new users, allow a missing default excludes file.

+ 1 - 1
atticmatic/backends/attic.py

@@ -5,7 +5,7 @@ from atticmatic.backends import shared
 # An atticmatic backend that supports Attic for actually handling backups.
 
 COMMAND = 'attic'
-
+CONFIG_FORMAT = shared.CONFIG_FORMAT
 
 create_archive = partial(shared.create_archive, command=COMMAND)
 prune_archives = partial(shared.prune_archives, command=COMMAND)

+ 12 - 0
atticmatic/backends/borg.py

@@ -1,10 +1,22 @@
 from functools import partial
 
+from atticmatic.config import Section_format, option
 from atticmatic.backends import shared
 
 # An atticmatic backend that supports Borg for actually handling backups.
 
 COMMAND = 'borg'
+CONFIG_FORMAT = (
+    shared.CONFIG_FORMAT[0],  # location
+    shared.CONFIG_FORMAT[1],  # retention
+    Section_format(
+        'consistency',
+        (
+            option('checks', required=False),
+            option('check_last', required=False),
+        ),
+    )
+)
 
 
 create_archive = partial(shared.create_archive, command=COMMAND)

+ 38 - 4
atticmatic/backends/shared.py

@@ -3,6 +3,7 @@ import os
 import platform
 import subprocess
 
+from atticmatic.config import Section_format, option
 from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
 
 
@@ -12,6 +13,34 @@ from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
 # atticmatic.backends.borg.
 
 
+CONFIG_FORMAT = (
+    Section_format(
+        'location',
+        (
+            option('source_directories'),
+            option('repository'),
+        ),
+    ),
+    Section_format(
+        'retention',
+        (
+            option('keep_within', required=False),
+            option('keep_hourly', int, required=False),
+            option('keep_daily', int, required=False),
+            option('keep_weekly', int, required=False),
+            option('keep_monthly', int, required=False),
+            option('keep_yearly', int, required=False),
+            option('prefix', required=False),
+        ),
+    ),
+    Section_format(
+        'consistency',
+        (
+            option('checks', required=False),
+        ),
+    )
+)
+
 def create_archive(excludes_filename, verbosity, source_directories, repository, command):
     '''
     Given an excludes filename (or None), a vebosity flag, a space-separated list of source
@@ -110,7 +139,7 @@ def _parse_checks(consistency_config):
     )
 
 
-def _make_check_flags(checks):
+def _make_check_flags(checks, check_last=None):
     '''
     Given a parsed sequence of checks, transform it into tuple of command-line flags.
 
@@ -121,13 +150,17 @@ def _make_check_flags(checks):
     This will be returned as:
     
         ('--repository-only',)
+
+    Additionally, if a check_last value is given, a "--last" flag will be added. Note that only
+    Borg supports this flag.
     '''
+    last_flag = ('--last', check_last) if check_last else ()
     if checks == DEFAULT_CHECKS:
-        return ()
+        return last_flag
 
     return tuple(
         '--{}-only'.format(check) for check in checks
-    )
+    ) + last_flag
 
 
 def check_archives(verbosity, repository, consistency_config, command):
@@ -138,6 +171,7 @@ def check_archives(verbosity, repository, consistency_config, command):
     If there are no consistency checks to run, skip running them.
     '''
     checks = _parse_checks(consistency_config)
+    check_last = consistency_config.get('check_last', None)
     if not checks:
         return
 
@@ -149,7 +183,7 @@ def check_archives(verbosity, repository, consistency_config, command):
     full_command = (
         command, 'check',
         repository,
-    ) + _make_check_flags(checks) + verbosity_flags
+    ) + _make_check_flags(checks, check_last) + verbosity_flags
 
     # The check command spews to stdout even without the verbose flag. Suppress it.
     stdout = None if verbosity_flags else open(os.devnull, 'w')

+ 2 - 2
atticmatic/command.py

@@ -60,9 +60,9 @@ def main():
     try:
         command_name = os.path.basename(sys.argv[0])
         args = parse_arguments(command_name, *sys.argv[1:])
-        config = parse_configuration(args.config_filename)
-        repository = config.location['repository']
         backend = load_backend(command_name)
+        config = parse_configuration(args.config_filename, backend.CONFIG_FORMAT)
+        repository = config.location['repository']
 
         backend.create_archive(args.excludes_filename, args.verbosity, **config.location)
         backend.prune_archives(args.verbosity, repository, config.retention)

+ 9 - 39
atticmatic/config.py

@@ -20,35 +20,6 @@ def option(name, value_type=str, required=True):
     return Config_option(name, value_type, required)
 
 
-CONFIG_FORMAT = (
-    Section_format(
-        'location',
-        (
-            option('source_directories'),
-            option('repository'),
-        ),
-    ),
-    Section_format(
-        'retention',
-        (
-            option('keep_within', required=False),
-            option('keep_hourly', int, required=False),
-            option('keep_daily', int, required=False),
-            option('keep_weekly', int, required=False),
-            option('keep_monthly', int, required=False),
-            option('keep_yearly', int, required=False),
-            option('prefix', required=False),
-        ),
-    ),
-    Section_format(
-        'consistency',
-        (
-            option('checks', required=False),
-        ),
-    )
-)
-
-
 def validate_configuration_format(parser, config_format):
     '''
     Given an open ConfigParser and an expected config file format, validate that the parsed
@@ -110,11 +81,6 @@ def validate_configuration_format(parser, config_format):
             )
 
 
-# Describes a parsed configuration, where each attribute is the name of a configuration file section 
-# and each value is a dict of that section's parsed options.
-Parsed_config = namedtuple('Config', (section_format.name for section_format in CONFIG_FORMAT))
-
-
 def parse_section_options(parser, section_format):
     '''
     Given an open ConfigParser and an expected section format, return the option values from that
@@ -135,21 +101,25 @@ def parse_section_options(parser, section_format):
     )
 
 
-def parse_configuration(config_filename):
+def parse_configuration(config_filename, config_format):
     '''
-    Given a config filename of the expected format, return the parsed configuration as Parsed_config
-    data structure.
+    Given a config filename and an expected config file format, return the parsed configuration
+    as a namedtuple with one attribute for each parsed section.
 
     Raise IOError if the file cannot be read, or ValueError if the format is not as expected.
     '''
     parser = ConfigParser()
     parser.read(config_filename)
 
-    validate_configuration_format(parser, CONFIG_FORMAT)
+    validate_configuration_format(parser, config_format)
+
+    # Describes a parsed configuration, where each attribute is the name of a configuration file
+    # section and each value is a dict of that section's parsed options.
+    Parsed_config = namedtuple('Parsed_config', (section_format.name for section_format in config_format))
 
     return Parsed_config(
         *(
             parse_section_options(parser, section_format)
-            for section_format in CONFIG_FORMAT
+            for section_format in config_format
         )
     )

+ 20 - 6
atticmatic/tests/unit/backends/test_shared.py

@@ -196,10 +196,24 @@ def test_make_check_flags_with_default_checks_returns_no_flags():
     assert flags == ()
 
 
+def test_make_check_flags_with_checks_and_last_returns_flags_including_last():
+    flags = module._make_check_flags(('foo', 'bar'), check_last=3)
+
+    assert flags == ('--foo-only', '--bar-only', '--last', 3)
+
+
+def test_make_check_flags_with_last_returns_last_flag():
+    flags = module._make_check_flags(module.DEFAULT_CHECKS, check_last=3)
+
+    assert flags == ('--last', 3)
+
+
 def test_check_archives_should_call_attic_with_parameters():
-    consistency_config = flexmock()
-    flexmock(module).should_receive('_parse_checks').and_return(flexmock())
-    flexmock(module).should_receive('_make_check_flags').and_return(())
+    checks = flexmock()
+    check_last = flexmock()
+    consistency_config = flexmock().should_receive('get').and_return(check_last).mock
+    flexmock(module).should_receive('_parse_checks').and_return(checks)
+    flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
     stdout = flexmock()
     insert_subprocess_mock(
         ('attic', 'check', 'repo'),
@@ -219,7 +233,7 @@ def test_check_archives_should_call_attic_with_parameters():
 
 
 def test_check_archives_with_verbosity_some_should_call_attic_with_verbose_parameter():
-    consistency_config = flexmock()
+    consistency_config = flexmock().should_receive('get').and_return(None).mock
     flexmock(module).should_receive('_parse_checks').and_return(flexmock())
     flexmock(module).should_receive('_make_check_flags').and_return(())
     insert_subprocess_mock(
@@ -238,7 +252,7 @@ def test_check_archives_with_verbosity_some_should_call_attic_with_verbose_param
 
 
 def test_check_archives_with_verbosity_lots_should_call_attic_with_verbose_parameter():
-    consistency_config = flexmock()
+    consistency_config = flexmock().should_receive('get').and_return(None).mock
     flexmock(module).should_receive('_parse_checks').and_return(flexmock())
     flexmock(module).should_receive('_make_check_flags').and_return(())
     insert_subprocess_mock(
@@ -257,7 +271,7 @@ def test_check_archives_with_verbosity_lots_should_call_attic_with_verbose_param
 
 
 def test_check_archives_without_any_checks_should_bail():
-    consistency_config = flexmock()
+    consistency_config = flexmock().should_receive('get').and_return(None).mock
     flexmock(module).should_receive('_parse_checks').and_return(())
     insert_subprocess_never()
 

+ 6 - 5
atticmatic/tests/unit/test_config.py

@@ -205,17 +205,18 @@ def insert_mock_parser():
 
 def test_parse_configuration_should_return_section_configs():
     parser = insert_mock_parser()
+    config_format = (flexmock(name='items'), flexmock(name='things'))
     mock_module = flexmock(module)
     mock_module.should_receive('validate_configuration_format').with_args(
-        parser, module.CONFIG_FORMAT,
+        parser, config_format,
     ).once()
-    mock_section_configs = (flexmock(),) * len(module.CONFIG_FORMAT)
+    mock_section_configs = (flexmock(), flexmock())
 
-    for section_format, section_config in zip(module.CONFIG_FORMAT, mock_section_configs):
+    for section_format, section_config in zip(config_format, mock_section_configs):
         mock_module.should_receive('parse_section_options').with_args(
             parser, section_format,
         ).and_return(section_config).once()
 
-    parsed_config = module.parse_configuration('filename')
+    parsed_config = module.parse_configuration('filename', config_format)
 
-    assert parsed_config == module.Parsed_config(*mock_section_configs)
+    assert parsed_config == type(parsed_config)(*mock_section_configs)

+ 2 - 0
sample/config

@@ -23,3 +23,5 @@ keep_yearly: 1
 # checks. See https://attic-backup.org/usage.html#attic-check or
 # https://borgbackup.github.io/borgbackup/usage.html#borg-check for details.
 checks: repository archives
+# For Borg only, you can restrict the number of checked archives to the last n.
+#check_last: 3