Compare commits

...

32 Commits

Author SHA1 Message Date
5cea1e1b72 Fix flake error (#262). 2025-03-29 22:52:17 -07:00
5716e61f8f Code formatting (#262). 2025-03-29 19:54:40 -07:00
65d1b9235d Add "default_actions" to NEWS (#262). 2025-03-29 19:02:11 -07:00
a8362f2618 borgmatic without arguments/parameters should show usage help instead of starting a backup (#262).
Reviewed-on: borgmatic-collective/borgmatic#1046
2025-03-30 01:57:11 +00:00
36265eea7d Docs update 2025-03-30 01:34:30 +00:00
da324ebeb7 Add "recreate" action to NEWS and docs (#610). 2025-03-29 15:15:36 -07:00
59f9d56aae Add a recreate action (#1030).
Reviewed-on: borgmatic-collective/borgmatic#1030
2025-03-29 22:07:52 +00:00
Vandal
dbf2e78f62 help changes 2025-03-30 03:05:46 +05:30
Vandal
2716d9d0b0 add to schema 2025-03-29 23:25:50 +05:30
0182dbd914 Added 2 new unit tests and updated docs 2025-03-29 03:43:58 +00:00
Vandal
8b3a682edf add tests and minor fixes 2025-03-29 01:26:20 +05:30
Vandal
7020f0530a update existing tests 2025-03-28 22:22:19 +05:30
Vandal
26fd41da92 add rest of flags 2025-03-27 22:18:34 +05:30
088da19012 Added Unit Tests 2025-03-27 11:26:56 +00:00
486bec698d Add "key import" to reference documentation (#345). 2025-03-26 22:13:30 -07:00
7a766c717e 2nd Draft 2025-03-27 02:55:16 +00:00
520fb78a00 Clarify Btrfs documentation: borgmatic expects subvolume mount points in "source_directories" (#1043). 2025-03-26 11:39:16 -07:00
Vandal
acc2814f11 add archive timestamp filter 2025-03-26 23:39:06 +05:30
996b037946 1st 2025-03-26 17:39:10 +00:00
Vandal
9356924418 add archive options 2025-03-26 22:30:11 +05:30
Vandal
203e84b91f hotfix 2025-03-25 21:57:06 +05:30
Vandal
ea5a2d8a46 add tests for the flags 2025-03-25 20:39:02 +05:30
Vandal
a8726c408a add tests 2025-03-25 19:35:15 +05:30
Vandal
3542673446 add test recreate with skip action 2025-03-25 11:36:06 +05:30
Vandal
b60cf2449a add recreate to schema 2025-03-25 00:48:27 +05:30
Vandal
e7f14bca87 add tests and requested changes 2025-03-25 00:16:20 +05:30
Vandal
fa3b140590 add patterns 2025-03-24 12:09:08 +05:30
Vandal
a1d2f7f221 add path 2025-03-24 11:51:33 +05:30
Vandal
a750d58a2d add recreate action 2025-03-22 21:18:28 +05:30
2045706faa merge upstream 2025-03-22 13:00:07 +00:00
Vandal
4e2805918d update borg/recreate.py 2025-03-18 23:19:33 +05:30
Vandal
6adb0fd44c add borg recreate 2025-03-17 22:24:53 +05:30
12 changed files with 1067 additions and 17 deletions

4
NEWS
View File

@@ -1,6 +1,10 @@
2.0.0.dev0
* #262: Add a "default_actions" option that supports disabling default actions when borgmatic is
run without any command-line arguments.
* #345: Add a "key import" action to import a repository key from backup.
* #422: Add home directory expansion to file-based and KeePassXC credential hooks.
* #610: Add a "recreate" action for recreating archives, for instance for retroactively excluding
particular files from existing archives.
* #790, #821: Deprecate all "before_*", "after_*" and "on_error" command hooks in favor of more
flexible "commands:". See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/

View File

@@ -0,0 +1,53 @@
import logging
import borgmatic.borg.recreate
import borgmatic.config.validate
from borgmatic.actions.create import collect_patterns, process_patterns
logger = logging.getLogger(__name__)
def run_recreate(
repository,
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "recreate" action for the given repository.
'''
if recreate_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, recreate_arguments.repository
):
if recreate_arguments.archive:
logger.answer(f'Recreating archive {recreate_arguments.archive}')
else:
logger.answer('Recreating repository')
# Collect and process patterns.
processed_patterns = process_patterns(
collect_patterns(config), borgmatic.config.paths.get_working_directory(config)
)
borgmatic.borg.recreate.recreate_archive(
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
recreate_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
patterns=processed_patterns,
)

100
borgmatic/borg/recreate.py Normal file
View File

@@ -0,0 +1,100 @@
import logging
import shlex
import borgmatic.borg.environment
import borgmatic.config.paths
import borgmatic.execute
from borgmatic.borg import flags
from borgmatic.borg.create import make_exclude_flags, make_list_filter_flags, write_patterns_file
logger = logging.getLogger(__name__)
def recreate_archive(
repository,
archive,
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path,
remote_path=None,
patterns=None,
):
'''
Given a local or remote repository path, an archive name, a configuration dict,
the local Borg version string, an argparse.Namespace of recreate arguments,
an argparse.Namespace of global arguments, optional local and remote Borg paths.
Executes the recreate command with the given arguments.
'''
lock_wait = config.get('lock_wait', None)
exclude_flags = make_exclude_flags(config)
compression = config.get('compression', None)
chunker_params = config.get('chunker_params', None)
# Available recompress MODES: 'if-different' (default), 'always', 'never'
recompress = config.get('recompress', None)
# Write patterns to a temporary file and use that file with --patterns-from.
patterns_file = write_patterns_file(
patterns, borgmatic.config.paths.get_working_directory(config)
)
recreate_command = (
(local_path, 'recreate')
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait is not None else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--patterns-from', patterns_file.name) if patterns_file else ())
+ (
(
'--list',
'--filter',
make_list_filter_flags(local_borg_version, global_arguments.dry_run),
)
if recreate_arguments.list
else ()
)
# Flag --target works only for a single archive
+ (('--target', recreate_arguments.target) if recreate_arguments.target and archive else ())
+ (
('--comment', shlex.quote(recreate_arguments.comment))
if recreate_arguments.comment
else ()
)
+ (('--timestamp', recreate_arguments.timestamp) if recreate_arguments.timestamp else ())
+ (('--compression', compression) if compression else ())
+ (('--chunker-params', chunker_params) if chunker_params else ())
+ (
flags.make_match_archives_flags(
recreate_arguments.match_archives or archive or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
if recreate_arguments.match_archives
else ()
)
+ (('--recompress', recompress) if recompress else ())
+ exclude_flags
+ (
flags.make_repository_archive_flags(repository, archive, local_borg_version)
if archive
else flags.make_repository_flags(repository, local_borg_version)
)
)
if global_arguments.dry_run:
logger.info('Skipping the archive recreation (dry run)')
return
borgmatic.execute.execute_command(
full_command=recreate_command,
output_log_level=logging.INFO,
environment=borgmatic.borg.environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View File

@@ -27,6 +27,7 @@ ACTION_ALIASES = {
'break-lock': [],
'key': [],
'borg': [],
'recreate': [],
}
@@ -1521,6 +1522,52 @@ def make_parsers():
'-h', '--help', action='help', help='Show this help message and exit'
)
recreate_parser = action_parsers.add_parser(
'recreate',
aliases=ACTION_ALIASES['recreate'],
help='Recreate an archive in a repository',
description='Recreate an archive in a repository',
add_help=False,
)
recreate_group = recreate_parser.add_argument_group('recreate arguments')
recreate_group.add_argument(
'--repository',
help='Path of repository containing archive to recreate, defaults to the configured repository if there is only one, quoted globs supported',
)
recreate_group.add_argument(
'--archive',
help='Archive name, hash, or series to recreate',
)
recreate_group.add_argument(
'--list', dest='list', action='store_true', help='Show per-file details'
)
recreate_group.add_argument(
'--target',
metavar='TARGET',
help='Create a new archive from the specified archive (via --archive), without replacing it',
)
recreate_group.add_argument(
'--comment',
metavar='COMMENT',
help='Add a comment text to the archive or, if an archive is not provided, to all matching archives',
)
recreate_group.add_argument(
'--timestamp',
metavar='TIMESTAMP',
help='Manually override the archive creation date/time (UTC)',
)
recreate_group.add_argument(
'-a',
'--match-archives',
'--glob-archives',
dest='match_archives',
metavar='PATTERN',
help='Only consider archive names, hashes, or series matching this pattern',
)
recreate_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
borg_parser = action_parsers.add_parser(
'borg',
aliases=ACTION_ALIASES['borg'],

View File

@@ -26,6 +26,7 @@ import borgmatic.actions.info
import borgmatic.actions.list
import borgmatic.actions.mount
import borgmatic.actions.prune
import borgmatic.actions.recreate
import borgmatic.actions.repo_create
import borgmatic.actions.repo_delete
import borgmatic.actions.repo_info
@@ -397,6 +398,16 @@ def run_actions(
local_path,
remote_path,
)
elif action_name == 'recreate' and action_name not in skip_actions:
borgmatic.actions.recreate.run_recreate(
repository,
config,
local_borg_version,
action_arguments,
global_arguments,
local_path,
remote_path,
)
elif action_name == 'prune' and action_name not in skip_actions:
borgmatic.actions.prune.run_prune(
config_filename,
@@ -943,6 +954,19 @@ def exit_with_help_link(): # pragma: no cover
sys.exit(1)
def check_and_show_help_on_no_args(configs):
'''
Check if the borgmatic command is run without any arguments. If the configuration option
"default_actions" is set to False, show the help message. Otherwise, trigger the default backup
behavior.
'''
if len(sys.argv) == 1: # No arguments provided
default_actions = any(config.get('default_actions', True) for config in configs.values())
if not default_actions:
parse_arguments('--help')
sys.exit(0)
def main(extra_summary_logs=[]): # pragma: no cover
configure_signals()
configure_delayed_logging()
@@ -978,6 +1002,10 @@ def main(extra_summary_logs=[]): # pragma: no cover
global_arguments.overrides,
resolve_env=global_arguments.resolve_env and not validate,
)
# Use the helper function to check and show help on no arguments, passing the preloaded configs
check_and_show_help_on_no_args(configs)
configuration_parse_errors = (
(max(log.levelno for log in parse_logs) >= logging.CRITICAL) if parse_logs else False
)

View File

@@ -284,6 +284,22 @@ properties:
http://borgbackup.readthedocs.io/en/stable/usage/create.html for
details. Defaults to "lz4".
example: lz4
recompress:
type: string
enum: ['if-different', 'always', 'never']
description: |
Mode for recompressing data chunks according to MODE.
Possible modes are:
* "if-different": Recompress if the current compression
is with a different compression algorithm.
* "always": Recompress even if the current compression
is with the same compression algorithm. Use this to change
the compression level.
* "never": Do not recompress. Use this option to explicitly
prevent recompression.
See https://borgbackup.readthedocs.io/en/stable/usage/recreate.html
for details. Defaults to "never".
example: if-different
upload_rate_limit:
type: integer
description: |
@@ -767,6 +783,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@@ -982,6 +999,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@@ -1046,6 +1064,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@@ -2666,3 +2685,11 @@ properties:
example: /usr/local/bin/keepassxc-cli
description: |
Configuration for integration with the KeePassXC password manager.
default_actions:
type: boolean
description: |
Whether to apply default actions (e.g., backup) when no arguments
are supplied to the borgmatic command. If set to true, borgmatic
triggers the default actions (create, prune, compact and check). If
set to false, borgmatic displays the help message instead.
example: true

View File

@@ -4,7 +4,7 @@ COPY . /app
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
RUN pip install --break-system-packages --no-cache /app && borgmatic config generate && chmod +r /etc/borgmatic/config.yaml
RUN borgmatic --help > /command-line.txt \
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key change-passphrase" borg; do \
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate borg; do \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic $action --help >> /command-line.txt; done
RUN /app/docs/fetch-contributors >> /contributors.html

View File

@@ -296,6 +296,20 @@ skip_actions:
- compact
```
### Disabling default actions
By default, running `borgmatic` without any arguments will perform the default
backup actions (create, prune, compact and check). If you want to disable this
behavior and require explicit actions to be specified, add the following to
your configuration:
```yaml
default_actions: false
```
With this setting, running `borgmatic` without arguments will show the help
message instead of performing any actions.
## Autopilot

View File

@@ -148,9 +148,9 @@ feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
#### Subvolume discovery
For any read-write subvolume you'd like backed up, add its path to borgmatic's
`source_directories` option. Btrfs does not support snapshotting read-only
subvolumes.
For any read-write subvolume you'd like backed up, add its mount point path to
borgmatic's `source_directories` option. Btrfs does not support snapshotting
read-only subvolumes.
<span class="minilink minilink-addedin">New in version 1.9.6</span> Or include
the mount point as a root pattern with borgmatic's `patterns` or `patterns_from`
@@ -161,27 +161,27 @@ includes the snapshotted files in the paths sent to Borg. borgmatic is also
responsible for cleaning up (deleting) these snapshots after a backup completes.
borgmatic is smart enough to look at the parent (and grandparent, etc.)
directories of each of your `source_directories` to discover any subvolumes.
For instance, let's say you add `/var/log` and `/var/lib` to your source
directories, but `/var` is a subvolume. borgmatic will discover that and
snapshot `/var` accordingly. This also works even with nested subvolumes;
directories of each of your `source_directories` to discover any subvolumes. For
instance, let's say you add `/var/log` and `/var/lib` to your source
directories, but `/var` is a subvolume mount point. borgmatic will discover that
and snapshot `/var` accordingly. This also works even with nested subvolumes;
borgmatic selects the subvolume that's the "closest" parent to your source
directories.
<span class="minilink minilink-addedin">New in version 1.9.6</span> When using
[patterns](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns),
the initial portion of a pattern's path that you intend borgmatic to match
against a subvolume can't have globs or other non-literal characters in it—or it
won't actually match. For instance, a subvolume of `/var` would match a pattern
of `+ fm:/var/*/data`, but borgmatic isn't currently smart enough to match
`/var` to a pattern like `+ fm:/v*/lib/data`.
against a subvolume mount point can't have globs or other non-literal characters
in it—or it won't actually match. For instance, a subvolume mount point of
`/var` would match a pattern of `+ fm:/var/*/data`, but borgmatic isn't
currently smart enough to match `/var` to a pattern like `+ fm:/v*/lib/data`.
Additionally, borgmatic rewrites the snapshot file paths so that they appear
at their original subvolume locations in a Borg archive. For instance, if your
subvolume exists at `/var/subvolume`, then the snapshotted files will appear
Additionally, borgmatic rewrites the snapshot file paths so that they appear at
their original subvolume locations in a Borg archive. For instance, if your
subvolume is mounted at `/var/subvolume`, then the snapshotted files will appear
in an archive at `/var/subvolume` as well—even if borgmatic has to mount the
snapshot somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform
the backup.
snapshot somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform the
backup.
<span class="minilink minilink-addedin">With Borg version 1.2 and
earlier</span>Snapshotted files are instead stored at a path dependent on the

View File

@@ -0,0 +1,39 @@
from flexmock import flexmock
from borgmatic.actions import recreate as module
def test_run_recreate_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive')
recreate_arguments = flexmock(repository=flexmock(), archive=None)
module.run_recreate(
repository={'path': 'repo'},
config={},
local_borg_version=None,
recreate_arguments=recreate_arguments,
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)
def test_run_recreate_with_archive_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive')
recreate_arguments = flexmock(repository=flexmock(), archive='test-archive')
module.run_recreate(
repository={'path': 'repo'},
config={},
local_borg_version=None,
recreate_arguments=recreate_arguments,
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)

View File

@@ -0,0 +1,644 @@
import logging
import shlex
from flexmock import flexmock
from borgmatic.borg import recreate as module
from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
flexmock(module.borgmatic.borg.environment).should_receive('make_environment')
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
full_command=command,
output_log_level=module.logging.INFO,
environment=None,
working_directory=working_directory,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
).once()
def mock_dependencies():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
def test_recreate_archive_dry_run_skips_execution():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
recreate_arguments = flexmock(
repository=flexmock(),
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
)
result = module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=recreate_arguments,
global_arguments=flexmock(log_json=False, dry_run=True),
local_path='borg',
)
assert result is None
def test_recreate_calls_borg_with_required_flags():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
patterns=None,
)
def test_recreate_with_remote_path():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--remote-path', 'borg1', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path='borg1',
patterns=None,
)
def test_recreate_with_lock_wait():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--lock-wait', '5', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'lock_wait': '5'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_log_info():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--info', 'repo::archive'))
insert_logging_mock(logging.INFO)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_log_debug():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--debug', '--show-rc', 'repo::archive'))
insert_logging_mock(logging.DEBUG)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_log_json():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--log-json', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=True),
local_path='borg',
patterns=None,
)
def test_recreate_with_list_filter_flags():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
flexmock(module).should_receive('make_list_filter_flags').and_return('AME+-')
insert_execute_command_mock(
('borg', 'recreate', '--list', '--filter', 'AME+-', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=True,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_patterns_from_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
mock_patterns_file = flexmock(name='patterns_file')
flexmock(module).should_receive('write_patterns_file').and_return(mock_patterns_file)
insert_execute_command_mock(
('borg', 'recreate', '--patterns-from', 'patterns_file', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=['pattern1', 'pattern2'],
)
def test_recreate_with_exclude_flags():
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
flexmock(module).should_receive('make_exclude_flags').and_return(('--exclude', 'pattern'))
insert_execute_command_mock(('borg', 'recreate', '--exclude', 'pattern', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'exclude_patterns': ['pattern']},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_target_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--target', 'new-archive', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target='new-archive',
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_comment_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(
('borg', 'recreate', '--comment', shlex.quote('This is a test comment'), 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment='This is a test comment',
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_timestamp_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(
('borg', 'recreate', '--timestamp', '2023-10-01T12:00:00', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp='2023-10-01T12:00:00',
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_compression_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--compression', 'lz4', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'compression': 'lz4'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_chunker_params_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(
('borg', 'recreate', '--chunker-params', '19,23,21,4095', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={'chunker_params': '19,23,21,4095'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_recompress_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--recompress', 'always', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'recompress': 'always'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_star():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_regex():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='re:.*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_shell():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='sh:*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_glob_archives_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(
('--glob-archives', 'foo-*')
)
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--glob-archives', 'foo-*', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='foo-*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(
('--match-archives', 'sh:foo-*')
)
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('--repo', 'repo', 'archive'))
insert_execute_command_mock(
('borg', 'recreate', '--match-archives', 'sh:foo-*', '--repo', 'repo', 'archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='2.0.0b3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='sh:foo-*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)

View File

@@ -1039,6 +1039,47 @@ def test_run_actions_with_skip_actions_skips_create():
)
def test_run_actions_runs_recreate():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
flexmock(borgmatic.actions.recreate).should_receive('run_recreate').once()
tuple(
module.run_actions(
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'recreate': flexmock()},
config_filename=flexmock(),
config={'repositories': []},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
)
)
def test_run_actions_with_skip_actions_skips_recreate():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return(['recreate'])
flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
flexmock(borgmatic.actions.recreate).should_receive('run_recreate').never()
tuple(
module.run_actions(
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'recreate': flexmock()},
config_filename=flexmock(),
config={'repositories': [], 'skip_actions': ['recreate']},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
)
)
def test_run_actions_runs_prune():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
@@ -2079,3 +2120,56 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
arguments=arguments,
)
)
def test_check_and_show_help_on_no_args_shows_help_when_no_args_and_default_actions_false():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
flexmock(module.sys).should_receive('exit').with_args(0).once()
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
def test_check_and_show_help_on_no_args_does_not_show_help_when_no_args_and_default_actions_true():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
flexmock(module).should_receive('parse_arguments').never()
flexmock(module.sys).should_receive('exit').never()
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': True}})
def test_check_and_show_help_on_no_args_does_not_show_help_when_args_provided():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic', '--create'])
flexmock(module).should_receive('parse_arguments').never()
flexmock(module.sys).should_receive('exit').never()
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
def test_check_and_show_help_on_no_args_with_no_default_actions_in_all_configs():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
# Both configs have default_actions set to False, so help should be shown
configs = {
'config1.yaml': {'default_actions': False},
'config2.yaml': {'default_actions': False},
}
# Expect help to be shown
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
flexmock(module.sys).should_receive('exit').with_args(0).once()
module.check_and_show_help_on_no_args(configs)
def test_check_and_show_help_on_no_args_with_conflicting_configs():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
# Simulate two config files with conflicting 'default_actions' values
configs = {
'config1.yaml': {'default_actions': True},
'config2.yaml': {'default_actions': False},
}
# Expect help not to be shown because at least one config enables default actions
flexmock(module).should_receive('parse_arguments').never()
flexmock(module.sys).should_receive('exit').never()
module.check_and_show_help_on_no_args(configs)