Borg 2 changes: Default the "archive_name_format" option to just "{hostname}". Update the "--match-archives"/"--archive" flags to support series names / archive hashes. Add a "--match-archives" flag to the "prune" action.
All checks were successful
build / test (push) Successful in 5m38s
build / docs (push) Successful in 2m1s

This commit is contained in:
Dan Helfman 2024-10-25 14:26:31 -07:00
parent ad21eb41ae
commit 83bc737185
14 changed files with 332 additions and 52 deletions

9
NEWS
View File

@ -21,6 +21,15 @@
all repositories in the configuration file.
* Add support for Borg 2's "rclone://" repository URLs, so you can backup to 70+ cloud storage
services whether or not they support Borg explicitly.
* When using Borg 2, default the "archive_name_format" option to just "{hostname}", as Borg 2 does
not require unique archive names; identical archive names form a common "series" that can be
targeted together. See the Borg 2 documentation for more information:
https://borgbackup.readthedocs.io/en/2.0.0b12/changes.html#borg-1-2-x-1-4-x-to-borg-2-0
* Update the "--match-archives" flag in all actions (and the "--archive" flag in select actions) to
support a Borg 2 series name as its value.
* Update the "--match-archives" and "--archive" flags in all actions to support a Borg 2 archive
hash as its value.
* Add a "--match-archives" flag to the "prune" action.
1.8.14
* #896: Fix an error in borgmatic rcreate/init on an empty repository directory with Borg 1.4.

View File

@ -399,7 +399,9 @@ def make_base_create_command(
lock_wait = config.get('lock_wait', None)
list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
files_cache = config.get('files_cache')
archive_name_format = config.get('archive_name_format', flags.DEFAULT_ARCHIVE_NAME_FORMAT)
archive_name_format = config.get(
'archive_name_format', flags.get_default_archive_name_format(local_borg_version)
)
extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
if feature.available(feature.Feature.ATIME, local_borg_version):

View File

@ -16,6 +16,7 @@ class Feature(Enum):
REPO_DELETE = 10
MATCH_ARCHIVES = 11
EXCLUDED_FILES_MINUS = 12
ARCHIVE_SERIES = 13
FEATURE_TO_MINIMUM_BORG_VERSION = {
@ -31,6 +32,7 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.REPO_DELETE: parse('2.0.0a2'), # borg repo-delete
Feature.MATCH_ARCHIVES: parse('2.0.0b3'), # borg --match-archives
Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'), # --list --filter uses "-" for excludes
Feature.ARCHIVE_SERIES: parse('2.0.0b11'), # identically named archives form a series
}

View File

@ -50,6 +50,9 @@ def make_repository_flags(repository_path, local_borg_version):
) + (repository_path,)
ARCHIVE_HASH_PATTERN = re.compile('[0-9a-fA-F]{8,}$')
def make_repository_archive_flags(repository_path, archive, local_borg_version):
'''
Given the path of a Borg repository, an archive name or pattern, and the local Borg version,
@ -57,20 +60,41 @@ def make_repository_archive_flags(repository_path, archive, local_borg_version):
and archive.
'''
return (
('--repo', repository_path, archive)
(
'--repo',
repository_path,
(
f'aid:{archive}'
if feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version)
and ARCHIVE_HASH_PATTERN.match(archive)
and not archive.startswith('aid:')
else archive
),
)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else (f'{repository_path}::{archive}',)
)
DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES = '{hostname}' # noqa: FS003
def get_default_archive_name_format(local_borg_version):
'''
Given the local Borg version, return the corresponding default archive name format.
'''
if feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version):
return DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES
return DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES
def make_match_archives_flags(
match_archives,
archive_name_format,
local_borg_version,
default_archive_name_format=DEFAULT_ARCHIVE_NAME_FORMAT,
default_archive_name_format=None,
):
'''
Return match archives flags based on the given match archives value, if any. If it isn't set,
@ -83,12 +107,23 @@ def make_match_archives_flags(
return ()
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
if (
feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version)
and ARCHIVE_HASH_PATTERN.match(match_archives)
and not match_archives.startswith('aid:')
):
return ('--match-archives', f'aid:{match_archives}')
return ('--match-archives', match_archives)
else:
return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
derived_match_archives = re.sub(
r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or default_archive_name_format
r'\{(now|utcnow|pid)([:%\w\.-]*)\}',
'*',
archive_name_format
or default_archive_name_format
or get_default_archive_name_format(local_borg_version),
)
if derived_match_archives == '*':

View File

@ -8,9 +8,10 @@ from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def make_prune_flags(config, local_borg_version):
def make_prune_flags(config, prune_arguments, local_borg_version):
'''
Given a configuration dict mapping from option name to value, transform it into an sequence of
Given a configuration dict mapping from option name to value, prune arguments as an
argparse.Namespace instance, and the local Borg version, produce a corresponding sequence of
command-line flags.
For example, given a retention config of:
@ -40,7 +41,7 @@ def make_prune_flags(config, local_borg_version):
if prefix
else (
flags.make_match_archives_flags(
config.get('match_archives'),
prune_arguments.match_archives or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
@ -69,7 +70,7 @@ def prune_archives(
full_command = (
(local_path, 'prune')
+ make_prune_flags(config, local_borg_version)
+ make_prune_flags(config, prune_arguments, local_borg_version)
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--log-json',) if global_arguments.log_json else ())
@ -78,7 +79,7 @@ def prune_archives(
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ flags.make_flags_from_arguments(
prune_arguments,
excludes=('repository', 'stats', 'list_archives'),
excludes=('repository', 'match_archives', 'stats', 'list_archives'),
)
+ (('--list',) if prune_arguments.list_archives else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())

View File

@ -469,7 +469,7 @@ def make_parsers():
)
transfer_group.add_argument(
'--archive',
help='Name of single archive to transfer (or "latest"), defaults to transferring all archives',
help='Name or hash of a single archive to transfer (or "latest"), defaults to transferring all archives',
)
transfer_group.add_argument(
'--upgrader',
@ -486,7 +486,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only transfer archives with names matching this pattern',
help='Only transfer archives with names, hashes, or series matching this pattern',
)
transfer_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@ -535,6 +535,13 @@ def make_parsers():
'--repository',
help='Path of specific existing repository to prune (must be already specified in a borgmatic configuration file), quoted globs supported',
)
prune_group.add_argument(
'-a',
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='When pruning, only consider archives with names, hashes, or series matching this pattern',
)
prune_group.add_argument(
'--stats',
dest='stats',
@ -673,7 +680,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only check archives with names matching this pattern',
help='Only check archives with names, hashes, or series matching this pattern',
)
check_group.add_argument(
'--only',
@ -705,7 +712,7 @@ def make_parsers():
)
delete_group.add_argument(
'--archive',
help='Archive to delete',
help='Archive name, hash, or series to delete',
)
delete_group.add_argument(
'--list',
@ -749,7 +756,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only delete archives matching this pattern',
help='Only delete archives with names, hashes, or series matching this pattern',
)
delete_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@ -795,7 +802,7 @@ def make_parsers():
help='Path of repository to extract, defaults to the configured repository if there is only one, quoted globs supported',
)
extract_group.add_argument(
'--archive', help='Name of archive to extract (or "latest")', required=True
'--archive', help='Name or hash of a single archive to extract (or "latest")', required=True
)
extract_group.add_argument(
'--path',
@ -863,7 +870,7 @@ def make_parsers():
)
config_bootstrap_group.add_argument(
'--archive',
help='Name of archive to extract config files from, defaults to "latest"',
help='Name or hash of a single archive to extract config files from, defaults to "latest"',
default='latest',
)
config_bootstrap_group.add_argument(
@ -955,7 +962,7 @@ def make_parsers():
help='Path of repository to export from, defaults to the configured repository if there is only one, quoted globs supported',
)
export_tar_group.add_argument(
'--archive', help='Name of archive to export (or "latest")', required=True
'--archive', help='Name or hash of a single archive to export (or "latest")', required=True
)
export_tar_group.add_argument(
'--path',
@ -1000,7 +1007,9 @@ def make_parsers():
'--repository',
help='Path of repository to use, defaults to the configured repository if there is only one, quoted globs supported',
)
mount_group.add_argument('--archive', help='Name of archive to mount (or "latest")')
mount_group.add_argument(
'--archive', help='Name or hash of a single archive to mount (or "latest")'
)
mount_group.add_argument(
'--mount-point',
metavar='PATH',
@ -1120,7 +1129,9 @@ def make_parsers():
help='Path of repository to restore from, defaults to the configured repository if there is only one, quoted globs supported',
)
restore_group.add_argument(
'--archive', help='Name of archive to restore from (or "latest")', required=True
'--archive',
help='Name or hash of a single archive to restore from (or "latest")',
required=True,
)
restore_group.add_argument(
'--data-source',
@ -1188,7 +1199,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only list archive names matching this pattern',
help='Only list archive names, hashes, or series matching this pattern',
)
repo_list_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@ -1235,7 +1246,9 @@ def make_parsers():
'--repository',
help='Path of repository containing archive to list, defaults to the configured repositories, quoted globs supported',
)
list_group.add_argument('--archive', help='Name of the archive to list (or "latest")')
list_group.add_argument(
'--archive', help='Name or hash of a single archive to list (or "latest")'
)
list_group.add_argument(
'--path',
metavar='PATH',
@ -1321,7 +1334,9 @@ def make_parsers():
'--repository',
help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one, quoted globs supported',
)
info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
info_group.add_argument(
'--archive', help='Archive name, hash, or series to show info for (or "latest")'
)
info_group.add_argument(
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
)
@ -1335,7 +1350,7 @@ def make_parsers():
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only show info for archive names matching this pattern',
help='Only show info for archive names, hashes, or series matching this pattern',
)
info_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@ -1460,7 +1475,9 @@ def make_parsers():
'--repository',
help='Path of repository to pass to Borg, defaults to the configured repositories, quoted globs supported',
)
borg_group.add_argument('--archive', help='Name of archive to pass to Borg (or "latest")')
borg_group.add_argument(
'--archive', help='Archive name, hash, or series to pass to Borg (or "latest")'
)
borg_group.add_argument(
'--',
metavar='OPTION',

View File

@ -397,11 +397,14 @@ properties:
archive_name_format:
type: string
description: |
Name of the archive. Borg placeholders can be used. See the output
of "borg help placeholders" for details. Defaults to
"{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". When running actions like
repo-list, info, or check, borgmatic automatically tries to match
only archives created with this name format.
Name of the archive to create. Borg placeholders can be used. See
the output of "borg help placeholders" for details. Defaults to
"{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}" with Borg 1 and
"{hostname}" with Borg 2, as Borg 2 does not require unique
archive names; identical archive names form a common "series" that
can be targeted together. When running actions like repo-list,
info, or check, borgmatic automatically tries to match only
archives created with this name format.
example: "{hostname}-documents-{now}"
match_archives:
type: string

View File

@ -11,7 +11,7 @@ services:
ENVIRONMENT: development
message:
image: alpine
container_name: message
container_name: borgmatic-docs-message
command:
- sh
- -c

View File

@ -336,15 +336,15 @@ borgmatic restore --archive host-2023-01-02T04:06:07.080910
(No borgmatic `restore` action? Upgrade borgmatic!)
With newer versions of borgmatic, you can simplify this to:
Or you can simplify this to:
```bash
borgmatic restore --archive latest
```
The `--archive` value is the name of the archive to restore from. This
restores all databases dumps that borgmatic originally backed up to that
archive.
The `--archive` value is the name of the archive or archive hash to restore
from. This restores all databases dumps that borgmatic originally backed up to
that archive.
This is a destructive action! `borgmatic restore` replaces live databases by
restoring dumps from the selected archive. So be very careful when and where

View File

@ -40,10 +40,10 @@ Or simplify this to:
borgmatic extract --archive latest
```
The `--archive` value is the name of the archive to extract. This extracts the
entire contents of the archive to the current directory, so make sure you're
in the right place before running the command—or see below about the
`--destination` flag.
The `--archive` value is the name of the archive or archive hash to extract.
This extracts the entire contents of the archive to the current directory, so
make sure you're in the right place before running the command—or see below
about the `--destination` flag.
## Repository selection
@ -131,6 +131,14 @@ Or use the "latest" value for the archive to mount the latest archive:
borgmatic mount --archive latest --mount-point /mnt
```
<span class="minilink minilink-addedin">With Borg version 2.x</span>You can
provide a series name for the `--archive` value to mount multiple archives in
that series:
```bash
borgmatic mount --archive seriesname --mount-point /mnt
```
If you'd like to restrict the mounted filesystem to only particular paths from
your archive, use the `--path` flag, similar to the `extract` action above.
For instance:

View File

@ -82,10 +82,14 @@ this option in the `storage:` section of your configuration.
This example means that when borgmatic creates an archive, its name will start
with the string `home-directories-` and end with a timestamp for its creation
time. If `archive_name_format` is unspecified, the default is
time. If `archive_name_format` is unspecified, the default with Borg 1 is
`{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}`, meaning your system hostname plus a
timestamp in a particular format.
<span class="minilink minilink-addedin">With Borg version 2.x</span>The default
is just `{hostname}`, as Borg 2 does not require unique archive names; identical
archive names form a common "series" that can be targeted together.
### Archive filtering
@ -129,10 +133,13 @@ archive_name_format: {hostname}-user-data-{now}
match_archives: sh:myhost-user-data-*
```
For Borg 1.x, use a shell pattern for the `match_archives` value and see the
[Borg patterns
<span class="minilink minilink-addedin">With Borg version 1.x</span>Use a shell
pattern for the `match_archives` value and see the [Borg patterns
documentation](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns)
for more information. For Borg 2.x, see the [match archives
for more information.
<span class="minilink minilink-addedin">With Borg version 2.x</span>See the
[match archives
documentation](https://borgbackup.readthedocs.io/en/2.0.0b12/usage/help.html#borg-help-match-archives).
Some borgmatic command-line actions also have a `--match-archives` flag that

View File

@ -581,6 +581,9 @@ def test_make_base_create_command_includes_patterns_file_in_borg_command():
None
)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
pattern_flags = ('--patterns-from', mock_pattern_file.name)
@ -631,6 +634,9 @@ def test_make_base_create_command_includes_sources_and_config_paths_in_borg_comm
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -676,6 +682,9 @@ def test_make_base_create_command_with_store_config_false_omits_config_files():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -719,6 +728,9 @@ def test_make_base_create_command_includes_exclude_patterns_in_borg_command():
mock_exclude_file = flexmock(name='/tmp/excludes')
flexmock(module).should_receive('write_pattern_file').and_return(mock_exclude_file)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -795,6 +807,9 @@ def test_make_base_create_command_includes_configuration_option_as_command_flag(
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(feature_available)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -837,6 +852,9 @@ def test_make_base_create_command_includes_dry_run_in_borg_command():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -879,6 +897,9 @@ def test_make_base_create_command_includes_local_path_in_borg_command():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -921,6 +942,9 @@ def test_make_base_create_command_includes_remote_path_in_borg_command():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -963,6 +987,9 @@ def test_make_base_create_command_includes_log_json_in_borg_command():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1004,6 +1031,9 @@ def test_make_base_create_command_includes_list_flags_in_borg_command():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1046,6 +1076,9 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1095,6 +1128,9 @@ def test_make_base_create_command_with_stream_processes_and_read_special_true_sk
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1141,6 +1177,9 @@ def test_make_base_create_command_with_non_matching_source_directories_glob_pass
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1183,6 +1222,9 @@ def test_make_base_create_command_expands_glob_in_source_directories():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1225,6 +1267,9 @@ def test_make_base_create_command_includes_archive_name_format_in_borg_command()
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1255,7 +1300,52 @@ def test_make_base_create_command_includes_archive_name_format_in_borg_command()
assert not exclude_file
def test_base_create_command_includes_archive_name_format_with_placeholders_in_borg_command():
def test_make_base_create_command_includes_default_archive_name_format_in_borg_command():
flexmock(module.borgmatic.config.options).should_receive('get_working_directory').and_return(
None
)
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('map_directories_to_devices').and_return({})
flexmock(module).should_receive('expand_directories').and_return(())
flexmock(module).should_receive('pattern_root_directories').and_return([])
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::{hostname}',)
)
(create_flags, create_positional_arguments, pattern_file, exclude_file) = (
module.make_base_create_command(
dry_run=False,
repository_path='repo',
config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
},
config_paths=['/tmp/test.yaml'],
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_source_directories=(),
)
)
assert create_flags == ('borg', 'create')
assert create_positional_arguments == ('repo::{hostname}', 'foo', 'bar')
assert not pattern_file
assert not exclude_file
def test_make_base_create_command_includes_archive_name_format_with_placeholders_in_borg_command():
repository_archive_pattern = 'repo::Documents_{hostname}-{now}' # noqa: FS003
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('map_directories_to_devices').and_return({})
@ -1265,6 +1355,9 @@ def test_base_create_command_includes_archive_name_format_with_placeholders_in_b
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1295,7 +1388,7 @@ def test_base_create_command_includes_archive_name_format_with_placeholders_in_b
assert not exclude_file
def test_base_create_command_includes_repository_and_archive_name_format_with_placeholders_in_borg_command():
def test_make_base_create_command_includes_repository_and_archive_name_format_with_placeholders_in_borg_command():
repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}' # noqa: FS003
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('map_directories_to_devices').and_return({})
@ -1305,6 +1398,9 @@ def test_base_create_command_includes_repository_and_archive_name_format_with_pl
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
@ -1347,6 +1443,9 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
flexmock(module).should_receive('expand_home_directories').and_return(())
flexmock(module).should_receive('write_pattern_file').and_return(None)
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
'{hostname}'
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())

View File

@ -85,6 +85,24 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
) == ('repo::archive',)
def test_get_default_archive_name_format_with_archive_series_feature_uses_series_archive_name_format():
flexmock(module.feature).should_receive('available').and_return(True)
assert (
module.get_default_archive_name_format(local_borg_version='1.2.3')
== module.DEFAULT_ARCHIVE_NAME_FORMAT_WITH_SERIES
)
def test_get_default_archive_name_format_without_archive_series_feature_uses_non_series_archive_name_format():
flexmock(module.feature).should_receive('available').and_return(False)
assert (
module.get_default_archive_name_format(local_borg_version='1.2.3')
== module.DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES
)
@pytest.mark.parametrize(
'match_archives,archive_name_format,feature_available,expected_result',
(
@ -175,12 +193,27 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
True,
(),
),
(
'abcdefabcdef',
None,
True,
('--match-archives', 'aid:abcdefabcdef'),
),
(
'aid:abcdefabcdef',
None,
True,
('--match-archives', 'aid:abcdefabcdef'),
),
),
)
def test_make_match_archives_flags_makes_flags_with_globs(
match_archives, archive_name_format, feature_available, expected_result
):
flexmock(module.feature).should_receive('available').and_return(feature_available)
flexmock(module).should_receive('get_default_archive_name_format').and_return(
module.DEFAULT_ARCHIVE_NAME_FORMAT_WITHOUT_SERIES
)
assert (
module.make_match_archives_flags(

View File

@ -36,7 +36,9 @@ def test_make_prune_flags_returns_flags_from_config():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
result = module.make_prune_flags(config, local_borg_version='1.2.3')
result = module.make_prune_flags(
config, flexmock(match_archives=None), local_borg_version='1.2.3'
)
assert result == BASE_PRUNE_FLAGS
@ -49,7 +51,9 @@ def test_make_prune_flags_accepts_prefix_with_placeholders():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
result = module.make_prune_flags(config, local_borg_version='1.2.3')
result = module.make_prune_flags(
config, flexmock(match_archives=None), local_borg_version='1.2.3'
)
expected = (
'--keep-daily',
@ -69,7 +73,9 @@ def test_make_prune_flags_with_prefix_without_borg_features_uses_glob_archives()
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
result = module.make_prune_flags(config, local_borg_version='1.2.3')
result = module.make_prune_flags(
config, flexmock(match_archives=None), local_borg_version='1.2.3'
)
expected = (
'--keep-daily',
@ -90,7 +96,9 @@ def test_make_prune_flags_prefers_prefix_to_archive_name_format():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').never()
result = module.make_prune_flags(config, local_borg_version='1.2.3')
result = module.make_prune_flags(
config, flexmock(match_archives=None), local_borg_version='1.2.3'
)
expected = (
'--keep-daily',
@ -111,9 +119,63 @@ def test_make_prune_flags_without_prefix_uses_archive_name_format_instead():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None, 'bar-{now}', '1.2.3' # noqa: FS003
).and_return(('--match-archives', 'sh:bar-*'))
).and_return(('--match-archives', 'sh:bar-*')).once()
result = module.make_prune_flags(config, local_borg_version='1.2.3')
result = module.make_prune_flags(
config, flexmock(match_archives=None), local_borg_version='1.2.3'
)
expected = (
'--keep-daily',
'1',
'--match-archives',
'sh:bar-*', # noqa: FS003
)
assert result == expected
def test_make_prune_flags_without_prefix_uses_match_archives_flag_instead_of_option():
config = {
'archive_name_format': 'bar-{now}', # noqa: FS003
'match_archives': 'foo*',
'keep_daily': 1,
'prefix': None,
}
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
'baz*', 'bar-{now}', '1.2.3' # noqa: FS003
).and_return(('--match-archives', 'sh:bar-*')).once()
result = module.make_prune_flags(
config, flexmock(match_archives='baz*'), local_borg_version='1.2.3'
)
expected = (
'--keep-daily',
'1',
'--match-archives',
'sh:bar-*', # noqa: FS003
)
assert result == expected
def test_make_prune_flags_without_prefix_uses_match_archives_option():
config = {
'archive_name_format': 'bar-{now}', # noqa: FS003
'match_archives': 'foo*',
'keep_daily': 1,
'prefix': None,
}
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
'foo*', 'bar-{now}', '1.2.3' # noqa: FS003
).and_return(('--match-archives', 'sh:bar-*')).once()
result = module.make_prune_flags(
config, flexmock(match_archives=None), local_borg_version='1.2.3'
)
expected = (
'--keep-daily',
@ -133,7 +195,9 @@ def test_make_prune_flags_ignores_keep_exclude_tags_in_config():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
result = module.make_prune_flags(config, local_borg_version='1.2.3')
result = module.make_prune_flags(
config, flexmock(match_archives=None), local_borg_version='1.2.3'
)
assert result == ('--keep-daily', '1')