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:
2024-10-25 14:26:31 -07:00
parent ad21eb41ae
commit 83bc737185
14 changed files with 332 additions and 52 deletions

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