diff --git a/NEWS b/NEWS index 4e05e072..154e6f3c 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,8 @@ * #304: Run any command-line actions in the order specified instead of using a fixed ordering. * #628: Add a Healthchecks "log" state to send borgmatic logs to Healthchecks without signalling success or failure. + * #647: Add "--strip-components all" feature on the "extract" action to remove leading path + components of files you extract. Must be used with the "--path" flag. 1.7.8 * #620: With the "create" action and the "--list" ("--files") flag, only show excluded files at diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index 8ea8bbbd..bbf36edf 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -87,6 +87,13 @@ def extract_archive( else: numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else () + if strip_components == 'all': + if not paths: + raise ValueError('The --strip-components flag with "all" requires at least one --path') + + # Calculate the maximum number of leading path components of the given paths. + strip_components = max(0, *(len(path.split(os.path.sep)) - 1 for path in paths)) + full_command = ( (local_path, 'extract') + (('--remote-path', remote_path) if remote_path else ()) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index cc9431db..3f56b7cd 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -476,10 +476,9 @@ def make_parsers(): ) extract_group.add_argument( '--strip-components', - type=int, + type=lambda number: number if number == 'all' else int(number), metavar='NUMBER', - dest='strip_components', - help='Number of leading path components to remove from each extracted path. Skip paths with fewer elements', + help='Number of leading path components to remove from each extracted path or "all" to strip all leading path components. Skip paths with fewer elements', ) extract_group.add_argument( '--progress', diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index 64d16d35..d27026e4 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -312,6 +312,57 @@ def test_extract_archive_calls_borg_with_strip_components(): ) +def test_extract_archive_calls_borg_with_strip_components_calculated_from_all(): + flexmock(module.os.path).should_receive('abspath').and_return('repo') + insert_execute_command_mock( + ( + 'borg', + 'extract', + '--strip-components', + '2', + 'repo::archive', + 'foo/bar/baz.txt', + 'foo/bar.txt', + ) + ) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + + module.extract_archive( + dry_run=False, + repository='repo', + archive='archive', + paths=['foo/bar/baz.txt', 'foo/bar.txt'], + location_config={}, + storage_config={}, + local_borg_version='1.2.3', + strip_components='all', + ) + + +def test_extract_archive_with_strip_components_all_and_no_paths_raises(): + flexmock(module.os.path).should_receive('abspath').and_return('repo') + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) + flexmock(module).should_receive('execute_command').never() + + with pytest.raises(ValueError): + module.extract_archive( + dry_run=False, + repository='repo', + archive='archive', + paths=None, + location_config={}, + storage_config={}, + local_borg_version='1.2.3', + strip_components='all', + ) + + def test_extract_archive_calls_borg_with_progress_parameter(): flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.environment).should_receive('make_environment')