瀏覽代碼

Specify "--archive latest" to all actions that accept an archive (#289).

Dan Helfman 5 年之前
父節點
當前提交
55141bda67
共有 6 個文件被更改,包括 177 次插入13 次删除
  1. 5 0
      NEWS
  2. 36 0
      borgmatic/borg/list.py
  3. 9 5
      borgmatic/commands/arguments.py
  4. 22 7
      borgmatic/commands/borgmatic.py
  5. 1 1
      setup.py
  6. 104 0
      tests/unit/borg/test_list.py

+ 5 - 0
NEWS

@@ -1,3 +1,8 @@
+1.5.1.dev0
+ * #289: Tired of looking up the latest successful archive name in order to pass it to borgmatic
+   actions? Me too. Now you can specify "--archive latest" to all actions that accept an archive
+   flag.
+
 1.5.0
  * #245: Monitor backups with PagerDuty hook integration. See the documentation for more
    information: https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook

+ 36 - 0
borgmatic/borg/list.py

@@ -11,6 +11,42 @@ logger = logging.getLogger(__name__)
 BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[0123456789]'
 
 
+def resolve_archive_name(repository, archive, storage_config, local_path='borg', remote_path=None):
+    '''
+    Given a local or remote repository path, an archive name, a storage config dict, a local Borg
+    path, and a remote Borg path, simply return the archive name. But if the archive name is
+    "latest", then instead introspect the repository for the latest successful (non-checkpoint)
+    archive, and return its name.
+
+    Raise ValueError if "latest" is given but there are no archives in the repository.
+    '''
+    if archive != "latest":
+        return archive
+
+    lock_wait = storage_config.get('lock_wait', None)
+
+    full_command = (
+        (local_path, 'list')
+        + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+        + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+        + make_flags('remote-path', remote_path)
+        + make_flags('lock-wait', lock_wait)
+        + make_flags('glob-archives', BORG_EXCLUDE_CHECKPOINTS_GLOB)
+        + make_flags('last', 1)
+        + ('--short', repository)
+    )
+
+    output = execute_command(full_command, output_log_level=None, error_on_warnings=False)
+    try:
+        latest_archive = output.strip().splitlines()[-1]
+    except IndexError:
+        raise ValueError('No archives found in the repository')
+
+    logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
+
+    return latest_archive
+
+
 def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None):
     '''
     Given a local or remote repository path, a storage config dict, and the arguments to the list

+ 9 - 5
borgmatic/commands/arguments.py

@@ -323,7 +323,9 @@ def parse_arguments(*unparsed_arguments):
         '--repository',
         help='Path of repository to extract, defaults to the configured repository if there is only one',
     )
-    extract_group.add_argument('--archive', help='Name of archive to extract', required=True)
+    extract_group.add_argument(
+        '--archive', help='Name of archive to extract (or "latest")', required=True
+    )
     extract_group.add_argument(
         '--path',
         '--restore-path',
@@ -361,7 +363,7 @@ def parse_arguments(*unparsed_arguments):
         '--repository',
         help='Path of repository to use, defaults to the configured repository if there is only one',
     )
-    mount_group.add_argument('--archive', help='Name of archive to mount')
+    mount_group.add_argument('--archive', help='Name of archive to mount (or "latest")')
     mount_group.add_argument(
         '--mount-point',
         metavar='PATH',
@@ -415,7 +417,9 @@ def parse_arguments(*unparsed_arguments):
         '--repository',
         help='Path of repository to restore from, defaults to the configured repository if there is only one',
     )
-    restore_group.add_argument('--archive', help='Name of archive to restore from', required=True)
+    restore_group.add_argument(
+        '--archive', help='Name of archive to restore from (or "latest")', required=True
+    )
     restore_group.add_argument(
         '--database',
         metavar='NAME',
@@ -446,7 +450,7 @@ def parse_arguments(*unparsed_arguments):
         '--repository',
         help='Path of repository to list, defaults to the configured repository if there is only one',
     )
-    list_group.add_argument('--archive', help='Name of archive to list')
+    list_group.add_argument('--archive', help='Name of archive to list (or "latest")')
     list_group.add_argument(
         '--path',
         metavar='PATH',
@@ -508,7 +512,7 @@ def parse_arguments(*unparsed_arguments):
         '--repository',
         help='Path of repository to show info for, defaults to the configured repository if there is only one',
     )
-    info_group.add_argument('--archive', help='Name of archive to show info for')
+    info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
     info_group.add_argument(
         '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
     )

+ 22 - 7
borgmatic/commands/borgmatic.py

@@ -1,4 +1,5 @@
 import collections
+import copy
 import json
 import logging
 import os
@@ -297,7 +298,9 @@ def run_actions(
             borg_extract.extract_archive(
                 global_arguments.dry_run,
                 repository,
-                arguments['extract'].archive,
+                borg_list.resolve_archive_name(
+                    repository, arguments['extract'].archive, storage, local_path, remote_path
+                ),
                 arguments['extract'].paths,
                 location,
                 storage,
@@ -319,7 +322,9 @@ def run_actions(
 
             borg_mount.mount_archive(
                 repository,
-                arguments['mount'].archive,
+                borg_list.resolve_archive_name(
+                    repository, arguments['mount'].archive, storage, local_path, remote_path
+                ),
                 arguments['mount'].mount_point,
                 arguments['mount'].paths,
                 arguments['mount'].foreground,
@@ -355,7 +360,9 @@ def run_actions(
             borg_extract.extract_archive(
                 global_arguments.dry_run,
                 repository,
-                arguments['restore'].archive,
+                borg_list.resolve_archive_name(
+                    repository, arguments['restore'].archive, storage, local_path, remote_path
+                ),
                 dump.convert_glob_patterns_to_borg_patterns(
                     dump.flatten_dump_patterns(dump_patterns, restore_names)
                 ),
@@ -395,12 +402,16 @@ def run_actions(
         if arguments['list'].repository is None or validate.repositories_match(
             repository, arguments['list'].repository
         ):
-            if not arguments['list'].json:
+            list_arguments = copy.copy(arguments['list'])
+            if not list_arguments.json:
                 logger.warning('{}: Listing archives'.format(repository))
+            list_arguments.archive = borg_list.resolve_archive_name(
+                repository, list_arguments.archive, storage, local_path, remote_path
+            )
             json_output = borg_list.list_archives(
                 repository,
                 storage,
-                list_arguments=arguments['list'],
+                list_arguments=list_arguments,
                 local_path=local_path,
                 remote_path=remote_path,
             )
@@ -410,12 +421,16 @@ def run_actions(
         if arguments['info'].repository is None or validate.repositories_match(
             repository, arguments['info'].repository
         ):
-            if not arguments['info'].json:
+            info_arguments = copy.copy(arguments['info'])
+            if not info_arguments.json:
                 logger.warning('{}: Displaying summary info for archives'.format(repository))
+            info_arguments.archive = borg_list.resolve_archive_name(
+                repository, info_arguments.archive, storage, local_path, remote_path
+            )
             json_output = borg_info.display_archives_info(
                 repository,
                 storage,
-                info_arguments=arguments['info'],
+                info_arguments=info_arguments,
                 local_path=local_path,
                 remote_path=remote_path,
             )

+ 1 - 1
setup.py

@@ -1,6 +1,6 @@
 from setuptools import find_packages, setup
 
-VERSION = '1.5.0'
+VERSION = '1.5.1.dev0'
 
 
 setup(

+ 104 - 0
tests/unit/borg/test_list.py

@@ -7,6 +7,110 @@ from borgmatic.borg import list as module
 
 from ..test_verbosity import insert_logging_mock
 
+BORG_LIST_LATEST_ARGUMENTS = (
+    '--glob-archives',
+    module.BORG_EXCLUDE_CHECKPOINTS_GLOB,
+    '--last',
+    '1',
+    '--short',
+    'repo',
+)
+
+
+def test_resolve_archive_name_passes_through_non_latest_archive_name():
+    archive = 'myhost-2030-01-01T14:41:17.647620'
+
+    assert module.resolve_archive_name('repo', archive, storage_config={}) == archive
+
+
+def test_resolve_archive_name_calls_borg_with_parameters():
+    expected_archive = 'archive-name'
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        error_on_warnings=False,
+    ).and_return(expected_archive + '\n')
+
+    assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
+
+
+def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter():
+    expected_archive = 'archive-name'
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        error_on_warnings=False,
+    ).and_return(expected_archive + '\n')
+    insert_logging_mock(logging.INFO)
+
+    assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
+
+
+def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter():
+    expected_archive = 'archive-name'
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        error_on_warnings=False,
+    ).and_return(expected_archive + '\n')
+    insert_logging_mock(logging.DEBUG)
+
+    assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
+
+
+def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
+    expected_archive = 'archive-name'
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        error_on_warnings=False,
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name('repo', 'latest', storage_config={}, local_path='borg1')
+        == expected_archive
+    )
+
+
+def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters():
+    expected_archive = 'archive-name'
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        error_on_warnings=False,
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name('repo', 'latest', storage_config={}, remote_path='borg1')
+        == expected_archive
+    )
+
+
+def test_resolve_archive_name_without_archives_raises():
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        error_on_warnings=False,
+    ).and_return('')
+
+    with pytest.raises(ValueError):
+        module.resolve_archive_name('repo', 'latest', storage_config={})
+
+
+def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters():
+    expected_archive = 'archive-name'
+
+    flexmock(module).should_receive('execute_command').with_args(
+        ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
+        output_log_level=None,
+        error_on_warnings=False,
+    ).and_return(expected_archive + '\n')
+
+    assert (
+        module.resolve_archive_name('repo', 'latest', storage_config={'lock_wait': 'okay'})
+        == expected_archive
+    )
+
 
 def test_list_archives_calls_borg_with_parameters():
     flexmock(module).should_receive('execute_command').with_args(