diff --git a/NEWS b/NEWS
index b0da2ca1..6a464f94 100644
--- a/NEWS
+++ b/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.
diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py
index 5df80358..113c8ca8 100644
--- a/borgmatic/borg/create.py
+++ b/borgmatic/borg/create.py
@@ -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):
diff --git a/borgmatic/borg/feature.py b/borgmatic/borg/feature.py
index eb73c009..a6462e13 100644
--- a/borgmatic/borg/feature.py
+++ b/borgmatic/borg/feature.py
@@ -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
}
diff --git a/borgmatic/borg/flags.py b/borgmatic/borg/flags.py
index 23b9c356..97bda27d 100644
--- a/borgmatic/borg/flags.py
+++ b/borgmatic/borg/flags.py
@@ -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 == '*':
diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py
index 39db1285..c79a6a7b 100644
--- a/borgmatic/borg/prune.py
+++ b/borgmatic/borg/prune.py
@@ -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 ())
diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py
index b01632da..76531bab 100644
--- a/borgmatic/commands/arguments.py
+++ b/borgmatic/commands/arguments.py
@@ -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',
diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml
index d2b080fc..9e901a32 100644
--- a/borgmatic/config/schema.yaml
+++ b/borgmatic/config/schema.yaml
@@ -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
diff --git a/docs/docker-compose.yaml b/docs/docker-compose.yaml
index b90622f2..99b3d87e 100644
--- a/docs/docker-compose.yaml
+++ b/docs/docker-compose.yaml
@@ -11,7 +11,7 @@ services:
ENVIRONMENT: development
message:
image: alpine
- container_name: message
+ container_name: borgmatic-docs-message
command:
- sh
- -c
diff --git a/docs/how-to/backup-your-databases.md b/docs/how-to/backup-your-databases.md
index 93ce0a7d..fe9afd61 100644
--- a/docs/how-to/backup-your-databases.md
+++ b/docs/how-to/backup-your-databases.md
@@ -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
diff --git a/docs/how-to/extract-a-backup.md b/docs/how-to/extract-a-backup.md
index 5a9f6836..7bfdd8da 100644
--- a/docs/how-to/extract-a-backup.md
+++ b/docs/how-to/extract-a-backup.md
@@ -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
```
+With Borg version 2.xYou 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:
diff --git a/docs/how-to/make-per-application-backups.md b/docs/how-to/make-per-application-backups.md
index a1a9381c..9ca2d07f 100644
--- a/docs/how-to/make-per-application-backups.md
+++ b/docs/how-to/make-per-application-backups.md
@@ -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.
+With Borg version 2.xThe 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
+With Borg version 1.xUse 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.
+
+With Borg version 2.xSee 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
diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py
index 6b381d49..55a65aea 100644
--- a/tests/unit/borg/test_create.py
+++ b/tests/unit/borg/test_create.py
@@ -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(())
diff --git a/tests/unit/borg/test_flags.py b/tests/unit/borg/test_flags.py
index 2204f497..38228e1d 100644
--- a/tests/unit/borg/test_flags.py
+++ b/tests/unit/borg/test_flags.py
@@ -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(
diff --git a/tests/unit/borg/test_prune.py b/tests/unit/borg/test_prune.py
index 7352ba70..d540ec69 100644
--- a/tests/unit/borg/test_prune.py
+++ b/tests/unit/borg/test_prune.py
@@ -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')