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.
This commit is contained in:
parent
ad21eb41ae
commit
83bc737185
9
NEWS
9
NEWS
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 == '*':
|
||||
|
@ -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 ())
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -11,7 +11,7 @@ services:
|
||||
ENVIRONMENT: development
|
||||
message:
|
||||
image: alpine
|
||||
container_name: message
|
||||
container_name: borgmatic-docs-message
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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(())
|
||||
|
@ -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(
|
||||
|
@ -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')
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user