Compare commits

...

100 commits

Author SHA1 Message Date
929d343214 Add CLI flags for every config option and add config options for many action flags (#303).
Reviewed-on: borgmatic-collective/borgmatic#1040
2025-04-03 23:48:49 +00:00
9ea55d9aa3 Add a documentation note about a limitation: You can't pass flags as values to flags (#303). 2025-04-03 16:38:17 -07:00
3eabda45f2 If a boolean option name already starts with "no_", don't add a "--no-no-..." CLI flag (#303). 2025-04-03 16:21:22 -07:00
09212961a4 Add action "--help" note about running compact after recreate (#1053). 2025-04-03 12:55:26 -07:00
3f25f3f0ff Merge branch 'main' into config-command-line. 2025-04-03 11:47:29 -07:00
e8542f3613 Fix KeePassXC error when "keepassxc:" option is not present, add new options to NEWS (#1047). 2025-04-03 11:41:58 -07:00
9407f24674 Fix setting of "--checks" on the command-line (#303). 2025-04-03 11:28:32 -07:00
1c9d25b892 Add "key-file" and "yubikey" options to KeePassXC credential hook (#1047).
Reviewed-on: borgmatic-collective/borgmatic#1049
2025-04-03 18:28:08 +00:00
248999c23e Final 2025-04-03 17:10:52 +00:00
d0a5aa63be Add a TL;DR to NEWS since 2.0.0 is such a huge release and ain't nobody got time for reading a huge changelog. 2025-04-03 09:24:47 -07:00
d2c3ed26a9 Make a CLI flag for any config option that's a list of scalars (#303). 2025-04-02 23:15:21 -07:00
bbf6f27715 For boolean configuration options, add separate "--foo" and "--no-foo" CLI flags (#303). 2025-04-02 17:08:04 -07:00
9301ab13cc Merge branch 'main' into config-command-line. 2025-04-02 09:55:33 -07:00
d5d04b89dc Add configuration filename to "Successfully ran configuration file" log message (#1051). 2025-04-02 09:50:31 -07:00
364200c65a Fix incorrect matching of non-zero array index flags with dashed names (#303). 2025-04-02 09:37:52 -07:00
4e55547235 Command Restructuring 2025-04-02 15:35:12 +00:00
96ec66de79 Applied changes 2025-04-02 10:50:25 +00:00
7a0c56878b Applied changes 2025-04-02 10:47:35 +00:00
4065c5d0f7 Fix use of dashed command-line flags like "--repositories[2].append-only" generated from configuration (#303). 2025-04-01 23:04:53 -07:00
affe7cdc1b Expose propertyless YAML objects from configuration (e.g. "constants") as command-line flags (#303). 2025-04-01 21:05:44 -07:00
017cbae4f9 Fix for the example not showing up in generated config for empty YAML objects (#303). 2025-04-01 19:44:47 -07:00
e96db2e100 Fix "progress" option with the "transfer" action (#303). 2025-04-01 19:43:56 -07:00
af97b95e2b Merge branch 'main' into config-command-line. 2025-04-01 12:09:54 -07:00
6a61259f1a Fix a failure in the "spot" check when the archive contains a symlink (#1050). 2025-04-01 11:49:47 -07:00
5490a83d77 Merge branch 'main' into config-command-line. 2025-03-31 17:13:20 -07:00
8c907bb5a3 Fix broken "recreate" action with Borg 1.4 (#610). 2025-03-31 17:11:37 -07:00
f166111b9b Fix new "repositories:" sub-options ("append_only", "make_parent_directories", etc.) (#303). 2025-03-31 15:26:24 -07:00
10fb02c40a Fix bootstrap --progress flag (#303). 2025-03-31 13:33:39 -07:00
cf477bdc1c Fix broken list_details, progress, and statistics options (#303). 2025-03-31 11:33:56 -07:00
6f07402407 Fix end-to-end tests and don't stat() directories that don't exist (#1048). 2025-03-30 19:04:36 -07:00
ab01e97a5e Fix a "no such file or directory" error in ZFS, Btrfs, and LVM hooks with nested directories that reside on separate devices/filesystems (#1048). 2025-03-30 14:55:54 -07:00
92ebc77597 2nd Draft 2025-03-30 16:19:56 +00:00
863c954144 added schema.yaml 2025-03-30 15:57:42 +00:00
f7e4d38762 First Draft 2025-03-30 14:02:56 +00:00
de4d7af507 Merge branch 'main' into config-command-line. 2025-03-29 22:52:40 -07:00
5cea1e1b72 Fix flake error (#262). 2025-03-29 22:52:17 -07:00
fd8c11eb0a Add documentation for "native" command-line overrides without --override (#303). 2025-03-29 21:59:47 -07:00
92de539bf9 Merge branch 'main' into config-command-line. 2025-03-29 19:55:03 -07:00
5716e61f8f Code formatting (#262). 2025-03-29 19:54:40 -07:00
3e05eeb4de Merge branch 'main' into config-command-line. 2025-03-29 19:03:29 -07:00
65d1b9235d Add "default_actions" to NEWS (#262). 2025-03-29 19:02:11 -07:00
cffb8e88da Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic into config-command-line 2025-03-29 18:58:12 -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
8101e5c56f Add "list_details" config option support to new "recreate" action (#303). 2025-03-29 15:24:37 -07:00
c7feb16ab5 Merge branch 'main' into config-command-line. 2025-03-29 15:16:29 -07: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
f6929f8891 Add last couple of missing tests after audit (#303). 2025-03-29 14:26:54 -07:00
Vandal
2716d9d0b0 add to schema 2025-03-29 23:25:50 +05:30
668f767bfc Adding some missing tests and fixing related flag vs. config logic (#303). 2025-03-28 23:11:15 -07:00
0182dbd914 Added 2 new unit tests and updated docs 2025-03-29 03:43:58 +00:00
1c27e0dadc Add an end-to-end test for command-line flags of configuration options (#303). 2025-03-28 13:46:58 -07:00
Vandal
8b3a682edf add tests and minor fixes 2025-03-29 01:26:20 +05:30
975a6e4540 Add additional tests for complete coverage (#303). 2025-03-28 11:37:48 -07:00
Vandal
7020f0530a update existing tests 2025-03-28 22:22:19 +05:30
5bf2f546b9 More automated tests (#303). 2025-03-27 21:01:56 -07:00
b4c558d013 Add tests for CLI arguments from schema logic (#303). 2025-03-27 16:49:14 -07:00
79bf641668 Set the action type when cloning an argument for a list index flag (#303). 2025-03-27 12:42:49 -07:00
50beb334dc Add tests for adding array element arguments and fix the code under test (#303). 2025-03-27 11:07:25 -07:00
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
4c6674e0ad Merge branch 'main' into config-command-line. 2025-03-26 22:14:36 -07: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
79e4e089ee Fix typo in NEWS (#1044). 2025-03-26 09:57:53 -07:00
d2714cb706 Fix an error in the systemd credential hook when the credential name contains a "." chararcter (#1044). 2025-03-26 09:53:52 -07:00
5a0430b9c8 Merge branch 'main' into config-command-line. 2025-03-25 22:39:51 -07:00
23efbb8df3 Fix line wrapping / code style (#837). 2025-03-25 22:31:50 -07:00
9e694e4df9 Add MongoDB custom command options to NEWS (#837). 2025-03-25 22:28:14 -07:00
76f7c53a1c Add custom command options for MongoDB hook (#837).
Reviewed-on: borgmatic-collective/borgmatic#1041
2025-03-26 05:27:03 +00:00
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
532a97623c Added test_build_restore_command_prevents_shell_injection() 2025-03-25 04:50:45 +00:00
e1fdfe4c2f Add credential hook directory expansion to NEWS (#422). 2025-03-24 13:00:38 -07:00
83a56a3fef Add directory expansion for file-based and KeyPassXC credential hooks (#1042).
Reviewed-on: borgmatic-collective/borgmatic#1042
2025-03-24 19:57:18 +00:00
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
Nish_
4bca7bb198 add directory expansion for file-based and KeyPassXC credentials
Signed-off-by: Nish_ <120EE0980@nitrkl.ac.in>
2025-03-24 21:04:55 +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
6a470be924 Made some changes in test file 2025-03-24 03:53:42 +00:00
d651813601 Custom command options for MongoDB hook #837 2025-03-24 03:39:26 +00:00
65b1d8e8b2 Clarify NEWS items (#303). 2025-03-23 19:13:07 -07:00
16a1121649 Get existing end-to-end tests passing (#303). 2025-03-23 18:45:49 -07:00
423627e67b Get existing unit/integration tests passing (#303). 2025-03-23 17:00:04 -07:00
9f7c71265e Add Bash completion for completing flags like "--foo[3].bar". 2025-03-23 16:32:31 -07:00
ba75958a2f Fix missing argument descriptions (#303). 2025-03-23 11:26:49 -07:00
57721937a3 Factor out schema type comparion in config generation and get several tests passing (#303). 2025-03-23 11:24:36 -07:00
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
86 changed files with 4436 additions and 734 deletions

26
NEWS
View file

@ -1,12 +1,20 @@
2.0.0.dev0
* #303: Add flags for setting any borgmatic configuration option from the command-line. See the
documentation for more information:
* TL;DR: More flexible, completely revamped command hooks. All config options settable on the
command-line. Config option defaults for many command-line flags. New "key import" and "recreate"
actions. Almost everything is backwards compatible.
* #262: Add a "default_actions" option that supports disabling default actions when borgmatic is
run without any command-line arguments.
* #303: Deprecate the "--override" flag in favor of direct command-line flags for every borgmatic
configuration option. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-overrides
* #303: Add configuration options that serve as defaults for some (but not all) borgmatic
* #303: Add configuration options that serve as defaults for some (but not all) command-line
action flags. For example, each entry in "repositories:" now has an "encryption" option that
applies to the "repo-create" action. See the documentation for more information:
https://torsion.org/borgmatic/docs/reference/configuration/
applies to the "repo-create" action, serving as a default for the "--encryption" flag. See the
documentation for more information: https://torsion.org/borgmatic/docs/reference/configuration/
* #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/
@ -16,11 +24,19 @@
* #790: BREAKING: Run all command hooks (both new and deprecated) respecting the
"working_directory" option if configured, meaning that hook commands are run in that directory.
* #836: Add a custom command option for the SQLite hook.
* #837: Add custom command options for the MongoDB hook.
* #1010: When using Borg 2, don't pass the "--stats" flag to "borg prune".
* #1020: Document a database use case involving a temporary database client container:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
* #1037: Fix an error with the "extract" action when both a remote repository and a
"working_directory" are used.
* #1044: Fix an error in the systemd credential hook when the credential name contains a "."
character.
* #1047: Add "key-file" and "yubikey" options to the KeePassXC credential hook.
* #1048: Fix a "no such file or directory" error in ZFS, Btrfs, and LVM hooks with nested
directories that reside on separate devices/filesystems.
* #1050: Fix a failure in the "spot" check when the archive contains a symlink.
* #1051: Add configuration filename to the "Successfully ran configuration file" log message.
1.9.14
* #409: With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the

View file

@ -170,7 +170,7 @@ def filter_checks_on_frequency(
if calendar.day_name[datetime_now().weekday()] not in days:
logger.info(
f"Skipping {check} check due to day of the week; check only runs on {'/'.join(days)} (use --force to check anyway)"
f"Skipping {check} check due to day of the week; check only runs on {'/'.join(day.title() for day in days)} (use --force to check anyway)"
)
filtered_checks.remove(check)
continue
@ -372,7 +372,7 @@ def collect_spot_check_source_paths(
borgmatic.borg.create.make_base_create_command(
dry_run=True,
repository_path=repository['path'],
config=config,
config=dict(config, list_details=True),
patterns=borgmatic.actions.create.process_patterns(
borgmatic.actions.create.collect_patterns(config),
working_directory,
@ -382,7 +382,6 @@ def collect_spot_check_source_paths(
borgmatic_runtime_directory=borgmatic_runtime_directory,
local_path=local_path,
remote_path=remote_path,
list_files=True,
stream_processes=stream_processes,
)
)
@ -483,10 +482,12 @@ def compare_spot_check_hashes(
)
source_sample_paths = tuple(random.sample(source_paths, sample_count))
working_directory = borgmatic.config.paths.get_working_directory(config)
existing_source_sample_paths = {
hashable_source_sample_path = {
source_path
for source_path in source_sample_paths
if os.path.exists(os.path.join(working_directory or '', source_path))
for full_source_path in (os.path.join(working_directory or '', source_path),)
if os.path.exists(full_source_path)
if not os.path.islink(full_source_path)
}
logger.debug(
f'Sampling {sample_count} source paths (~{spot_check_config["data_sample_percentage"]}%) for spot check'
@ -509,7 +510,7 @@ def compare_spot_check_hashes(
hash_output = borgmatic.execute.execute_command_and_capture_output(
(spot_check_config.get('xxh64sum_command', 'xxh64sum'),)
+ tuple(
path for path in source_sample_paths_subset if path in existing_source_sample_paths
path for path in source_sample_paths_subset if path in hashable_source_sample_path
),
working_directory=working_directory,
)
@ -517,11 +518,13 @@ def compare_spot_check_hashes(
source_hashes.update(
**dict(
(reversed(line.split(' ', 1)) for line in hash_output.splitlines()),
# Represent non-existent files as having empty hashes so the comparison below still works.
# Represent non-existent files as having empty hashes so the comparison below still
# works. Same thing for filesystem links, since Borg produces empty archive hashes
# for them.
**{
path: ''
for path in source_sample_paths_subset
if path not in existing_source_sample_paths
if path not in hashable_source_sample_path
},
)
)

View file

@ -37,9 +37,7 @@ def run_compact(
global_arguments,
local_path=local_path,
remote_path=remote_path,
progress=compact_arguments.progress or config.get('progress'),
cleanup_commits=compact_arguments.cleanup_commits,
threshold=compact_arguments.threshold or config.get('compact_threshold'),
)
else: # pragma: nocover
logger.info('Skipping compact (only available/needed in Borg 1.2+)')

View file

@ -119,7 +119,9 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
bootstrap_arguments.repository,
archive_name,
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
config,
# Only add progress here and not the extract_archive() call above, because progress
# conflicts with extract_to_stdout.
dict(config, progress=bootstrap_arguments.progress or False),
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
@ -127,5 +129,4 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
extract_to_stdout=False,
destination_path=bootstrap_arguments.destination,
strip_components=bootstrap_arguments.strip_components,
progress=bootstrap_arguments.progress,
)

View file

@ -289,6 +289,16 @@ def run_create(
):
return
if config.get('list_details') and config.get('progress'):
raise ValueError(
'With the create action, only one of --list/--files/list_details and --progress/progress can be used.'
)
if config.get('list_details') and create_arguments.json:
raise ValueError(
'With the create action, only one of --list/--files/list_details and --json can be used.'
)
logger.info(f'Creating archive{dry_run_label}')
working_directory = borgmatic.config.paths.get_working_directory(config)
@ -327,10 +337,7 @@ def run_create(
borgmatic_runtime_directory,
local_path=local_path,
remote_path=remote_path,
progress=create_arguments.progress or config.get('progress'),
stats=create_arguments.stats or config.get('stats'),
json=create_arguments.json,
list_files=create_arguments.list_files or config.get('list'),
stream_processes=stream_processes,
)

View file

@ -43,6 +43,5 @@ def run_export_tar(
local_path=local_path,
remote_path=remote_path,
tar_filter=export_tar_arguments.tar_filter,
list_files=export_tar_arguments.list_files or config.get('list'),
strip_components=export_tar_arguments.strip_components,
)

View file

@ -45,5 +45,4 @@ def run_extract(
remote_path=remote_path,
destination_path=extract_arguments.destination,
strip_components=extract_arguments.strip_components,
progress=extract_arguments.progress or config.get('progress'),
)

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,
)

View file

@ -41,9 +41,21 @@ def run_repo_create(
encryption_mode,
repo_create_arguments.source_repository,
repo_create_arguments.copy_crypt_key,
repo_create_arguments.append_only or repository.get('append_only'),
repo_create_arguments.storage_quota or repository.get('storage_quota'),
repo_create_arguments.make_parent_dirs or repository.get('make_parent_dirs'),
(
repository.get('append_only')
if repo_create_arguments.append_only is None
else repo_create_arguments.append_only
),
(
repository.get('storage_quota')
if repo_create_arguments.storage_quota is None
else repo_create_arguments.storage_quota
),
(
repository.get('make_parent_directories')
if repo_create_arguments.make_parent_directories is None
else repo_create_arguments.make_parent_directories
),
local_path=local_path,
remote_path=remote_path,
)

View file

@ -17,7 +17,13 @@ def run_transfer(
'''
Run the "transfer" action for the given repository.
'''
if transfer_arguments.archive and config.get('match_archives'):
raise ValueError(
'With the transfer action, only one of --archive and --match-archives/match_archives can be used.'
)
logger.info('Transferring archives to repository')
borgmatic.borg.transfer.transfer_archives(
global_arguments.dry_run,
repository['path'],

View file

@ -32,7 +32,7 @@ def make_archive_filter_flags(local_borg_version, config, checks, check_argument
if prefix
else (
flags.make_match_archives_flags(
check_arguments.match_archives or config.get('match_archives'),
config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
@ -143,7 +143,6 @@ def check_archives(
umask = config.get('umask')
borg_exit_codes = config.get('borg_exit_codes')
working_directory = borgmatic.config.paths.get_working_directory(config)
progress = check_arguments.progress or config.get('progress')
if 'data' in checks:
checks.add('archives')
@ -171,7 +170,7 @@ def check_archives(
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ verbosity_flags
+ (('--progress',) if progress else ())
+ (('--progress',) if config.get('progress') else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
@ -180,7 +179,9 @@ def check_archives(
full_command,
# The Borg repair option triggers an interactive prompt, which won't work when output is
# captured. And progress messes with the terminal directly.
output_file=(DO_NOT_CAPTURE if check_arguments.repair or progress else None),
output_file=(
DO_NOT_CAPTURE if check_arguments.repair or config.get('progress') else None
),
environment=environment.make_environment(config),
working_directory=working_directory,
borg_local_path=local_path,

View file

@ -15,9 +15,7 @@ def compact_segments(
global_arguments,
local_path='borg',
remote_path=None,
progress=False,
cleanup_commits=False,
threshold=None,
):
'''
Given dry-run flag, a local or remote repository path, a configuration dict, and the local Borg
@ -26,6 +24,7 @@ def compact_segments(
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
extra_borg_options = config.get('extra_borg_options', {}).get('compact', '')
threshold = config.get('compact_threshold')
full_command = (
(local_path, 'compact')
@ -33,7 +32,7 @@ def compact_segments(
+ (('--umask', str(umask)) if umask else ())
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--progress',) if progress else ())
+ (('--progress',) if config.get('progress') else ())
+ (('--cleanup-commits',) if cleanup_commits else ())
+ (('--threshold', str(threshold)) if threshold else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())

View file

@ -196,7 +196,7 @@ def check_all_root_patterns_exist(patterns):
if missing_paths:
raise ValueError(
f"Source directories / root pattern paths do not exist: {', '.join(missing_paths)}"
f"Source directories or root pattern paths do not exist: {', '.join(missing_paths)}"
)
@ -213,9 +213,7 @@ def make_base_create_command(
borgmatic_runtime_directory,
local_path='borg',
remote_path=None,
progress=False,
json=False,
list_files=False,
stream_processes=None,
):
'''
@ -293,7 +291,7 @@ def make_base_create_command(
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (
('--list', '--filter', list_filter_flags)
if list_files and not json and not progress
if config.get('list_details') and not json and not config.get('progress')
else ()
)
+ (('--dry-run',) if dry_run else ())
@ -361,10 +359,7 @@ def create_archive(
borgmatic_runtime_directory,
local_path='borg',
remote_path=None,
progress=False,
stats=False,
json=False,
list_files=False,
stream_processes=None,
):
'''
@ -389,28 +384,26 @@ def create_archive(
borgmatic_runtime_directory,
local_path,
remote_path,
progress,
json,
list_files,
stream_processes,
)
if json:
output_log_level = None
elif list_files or (stats and not dry_run):
elif config.get('list_details') or (config.get('statistics') and not dry_run):
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
output_file = DO_NOT_CAPTURE if progress else None
output_file = DO_NOT_CAPTURE if config.get('progress') else None
create_flags += (
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--stats',) if stats and not json and not dry_run else ())
+ (('--stats',) if config.get('statistics') and not json and not dry_run else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--progress',) if progress else ())
+ (('--progress',) if config.get('progress') else ())
+ (('--json',) if json else ())
)
borg_exit_codes = config.get('borg_exit_codes')

View file

@ -34,9 +34,7 @@ def make_delete_command(
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
+ borgmatic.borg.flags.make_flags('log-json', global_arguments.log_json)
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
+ borgmatic.borg.flags.make_flags(
'list', delete_arguments.list_archives or config.get('list')
)
+ borgmatic.borg.flags.make_flags('list', config.get('list_details'))
+ (
(('--force',) + (('--force',) if delete_arguments.force >= 2 else ()))
if delete_arguments.force
@ -50,9 +48,17 @@ def make_delete_command(
local_borg_version=local_borg_version,
default_archive_name_format='*',
)
+ (('--stats',) if config.get('statistics') else ())
+ borgmatic.borg.flags.make_flags_from_arguments(
delete_arguments,
excludes=('list_archives', 'force', 'match_archives', 'archive', 'repository'),
excludes=(
'list_details',
'statistics',
'force',
'match_archives',
'archive',
'repository',
),
)
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
)
@ -100,7 +106,7 @@ def delete_archives(
repo_delete_arguments = argparse.Namespace(
repository=repository['path'],
list_archives=delete_arguments.list_archives,
list_details=delete_arguments.list_details,
force=delete_arguments.force,
cache_only=delete_arguments.cache_only,
keep_security_info=delete_arguments.keep_security_info,

View file

@ -20,7 +20,6 @@ def export_tar_archive(
local_path='borg',
remote_path=None,
tar_filter=None,
list_files=False,
strip_components=None,
):
'''
@ -43,7 +42,7 @@ def export_tar_archive(
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--list',) if list_files else ())
+ (('--list',) if config.get('list_details') else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--tar-filter', tar_filter) if tar_filter else ())
@ -57,7 +56,7 @@ def export_tar_archive(
+ (tuple(paths) if paths else ())
)
if list_files:
if config.get('list_details'):
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO

View file

@ -77,7 +77,6 @@ def extract_archive(
remote_path=None,
destination_path=None,
strip_components=None,
progress=False,
extract_to_stdout=False,
):
'''
@ -92,8 +91,8 @@ def extract_archive(
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
if progress and extract_to_stdout:
raise ValueError('progress and extract_to_stdout cannot both be set')
if config.get('progress') and extract_to_stdout:
raise ValueError('progress and extract to stdout cannot both be set')
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
numeric_ids_flags = ('--numeric-ids',) if config.get('numeric_ids') else ()
@ -128,7 +127,7 @@ def extract_archive(
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--strip-components', str(strip_components)) if strip_components else ())
+ (('--progress',) if progress else ())
+ (('--progress',) if config.get('progress') else ())
+ (('--stdout',) if extract_to_stdout else ())
+ flags.make_repository_archive_flags(
# Make the repository path absolute so the destination directory used below via changing
@ -148,7 +147,7 @@ def extract_archive(
# The progress output isn't compatible with captured and logged output, as progress messes with
# the terminal directly.
if progress:
if config.get('progress'):
return execute_command(
full_command,
output_file=DO_NOT_CAPTURE,

View file

@ -48,9 +48,7 @@ def make_info_command(
if info_arguments.prefix
else (
flags.make_match_archives_flags(
info_arguments.match_archives
or info_arguments.archive
or config.get('match_archives'),
info_arguments.archive or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)

View file

@ -41,7 +41,7 @@ def make_prune_flags(config, prune_arguments, local_borg_version):
if prefix
else (
flags.make_match_archives_flags(
prune_arguments.match_archives or config.get('match_archives'),
config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
@ -66,7 +66,6 @@ def prune_archives(
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
stats = prune_arguments.stats or config.get('stats')
extra_borg_options = config.get('extra_borg_options', {}).get('prune', '')
full_command = (
@ -78,7 +77,7 @@ def prune_archives(
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (
('--stats',)
if stats
if config.get('statistics')
and not dry_run
and not feature.available(feature.Feature.NO_PRUNE_STATS, local_borg_version)
else ()
@ -86,16 +85,16 @@ def prune_archives(
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ flags.make_flags_from_arguments(
prune_arguments,
excludes=('repository', 'match_archives', 'stats', 'list_archives'),
excludes=('repository', 'match_archives', 'statistics', 'list_details'),
)
+ (('--list',) if prune_arguments.list_archives or config.get('list') else ())
+ (('--list',) if config.get('list_details') else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository_path, local_borg_version)
)
if stats or prune_arguments.list_archives:
if config.get('statistics') or config.get('list_details'):
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO

103
borgmatic/borg/recreate.py Normal file
View file

@ -0,0 +1,103 @@
import logging
import shlex
import borgmatic.borg.environment
import borgmatic.borg.feature
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", "always", "never" (default)
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 config.get('list_details')
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 ())
+ (('--recompress', recompress) if recompress else ())
+ exclude_flags
+ (
(
flags.make_repository_flags(repository, local_borg_version)
+ flags.make_match_archives_flags(
archive or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
)
if borgmatic.borg.feature.available(
borgmatic.borg.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version
)
else (
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

@ -24,7 +24,7 @@ def create_repository(
copy_crypt_key=False,
append_only=None,
storage_quota=None,
make_parent_dirs=False,
make_parent_directories=False,
local_path='borg',
remote_path=None,
):
@ -79,7 +79,7 @@ def create_repository(
+ (('--copy-crypt-key',) if copy_crypt_key else ())
+ (('--append-only',) if append_only else ())
+ (('--storage-quota', storage_quota) if storage_quota else ())
+ (('--make-parent-dirs',) if make_parent_dirs else ())
+ (('--make-parent-dirs',) if make_parent_directories else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--log-json',) if global_arguments.log_json else ())

View file

@ -39,14 +39,14 @@ def make_repo_delete_command(
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
+ borgmatic.borg.flags.make_flags('log-json', global_arguments.log_json)
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
+ borgmatic.borg.flags.make_flags('list', repo_delete_arguments.list_archives)
+ borgmatic.borg.flags.make_flags('list', config.get('list_details'))
+ (
(('--force',) + (('--force',) if repo_delete_arguments.force >= 2 else ()))
if repo_delete_arguments.force
else ()
)
+ borgmatic.borg.flags.make_flags_from_arguments(
repo_delete_arguments, excludes=('list_archives', 'force', 'repository')
repo_delete_arguments, excludes=('list_details', 'force', 'repository')
)
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
)

View file

@ -113,7 +113,7 @@ def make_repo_list_command(
if repo_list_arguments.prefix
else (
flags.make_match_archives_flags(
repo_list_arguments.match_archives or config.get('match_archives'),
config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)

View file

@ -32,17 +32,22 @@ def transfer_archives(
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('umask', config.get('umask'))
+ flags.make_flags('log-json', global_arguments.log_json)
+ flags.make_flags('lock-wait', config.get('lock_wait', None))
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ flags.make_flags('progress', config.get('progress'))
+ (
flags.make_flags_from_arguments(
transfer_arguments,
excludes=('repository', 'source_repository', 'archive', 'match_archives'),
excludes=(
'repository',
'source_repository',
'archive',
'match_archives',
'progress',
),
)
or (
flags.make_match_archives_flags(
transfer_arguments.match_archives
or transfer_arguments.archive
or config.get('match_archives'),
transfer_arguments.archive or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
@ -56,9 +61,7 @@ def transfer_archives(
return execute_command(
full_command,
output_log_level=logging.ANSWER,
output_file=(
DO_NOT_CAPTURE if (transfer_arguments.progress or config.get('progress')) else None
),
output_file=DO_NOT_CAPTURE if config.get('progress') else None,
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,

View file

@ -1,7 +1,6 @@
import collections
import io
import itertools
import json
import re
import sys
from argparse import ArgumentParser
@ -33,6 +32,7 @@ ACTION_ALIASES = {
'break-lock': [],
'key': [],
'borg': [],
'recreate': [],
}
@ -69,9 +69,9 @@ def get_subactions_for_actions(action_parsers):
def omit_values_colliding_with_action_names(unparsed_arguments, parsed_arguments):
'''
Given a sequence of string arguments and a dict from action name to parsed argparse.Namespace
arguments, return the string arguments with any values omitted that happen to be the same as
the name of a borgmatic action.
Given unparsed arguments as a sequence of strings and a dict from action name to parsed
argparse.Namespace arguments, return the string arguments with any values omitted that happen to
be the same as the name of a borgmatic action.
This prevents, for instance, "check --only extract" from triggering the "extract" action.
'''
@ -288,7 +288,7 @@ def parse_arguments_for_actions(unparsed_arguments, action_parsers, global_parse
)
OMITTED_FLAG_NAMES = {'match_archives', 'progress', 'stats', 'list'}
OMITTED_FLAG_NAMES = {'match-archives', 'progress', 'statistics', 'list-details'}
def make_argument_description(schema, flag_name):
@ -299,34 +299,34 @@ def make_argument_description(schema, flag_name):
'''
description = schema.get('description')
schema_type = schema.get('type')
example = schema.get('example')
pieces = [description] if description else []
if not description:
return None
if '[0]' in flag_name:
pieces.append(
' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).'
)
if schema_type == 'array':
if example and schema_type in ('array', 'object'):
example_buffer = io.StringIO()
yaml = ruamel.yaml.YAML(typ='safe')
yaml.default_flow_style = True
yaml.dump(schema.get('example'), example_buffer)
yaml.dump(example, example_buffer)
description += f' Example value: "{example_buffer.getvalue().strip()}"'
pieces.append(f'Example value: "{example_buffer.getvalue().strip()}"')
if '[0]' in flag_name:
description += ' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).'
description = description.replace('%', '%%')
return ' '.join(pieces).replace('%', '%%')
def add_array_element_arguments_from_schema(arguments_group, schema, unparsed_arguments, flag_name):
'''
Given an argparse._ArgumentGroup instance, a configuration schema dict, a sequence of unparsed
argument strings, and a dotted flag name, convert the schema into corresponding command-line
array element flags that correspond to the given unparsed arguments.
def add_array_element_arguments(arguments_group, unparsed_arguments, flag_name):
r'''
Given an argparse._ArgumentGroup instance, a sequence of unparsed argument strings, and a dotted
flag name, add command-line array element flags that correspond to the given unparsed arguments.
Here's the background. We want to support flags that can have arbitrary indices like:
--foo.bar[1].baz
But argparse doesn't support that natively because the index can be an arbitrary number. We
won't let that stop us though, will we?
@ -334,45 +334,77 @@ def add_array_element_arguments_from_schema(arguments_group, schema, unparsed_ar
pattern that would match the flag name regardless of the number that's in it. The idea is that
we want to look for unparsed arguments that appear like the flag name, but instead of "[0]" they
have, say, "[1]" or "[123]".
Next, we check each unparsed argument against that pattern. If one of them matches, add an
argument flag for it to the argument parser group. Example:
Let's say flag_name is:
--foo.bar[0].baz
... then the regular expression pattern will be:
^--foo\.bar\[\d+\]\.baz
... and, if that matches an unparsed argument of:
--foo.bar[1].baz
... then an argument flag will get added equal to that unparsed argument. And the unparsed
... then an argument flag will get added equal to that unparsed argument. And so the unparsed
argument will match it when parsing is performed! In this manner, we're using the actual user
CLI input to inform what exact flags we support!
CLI input to inform what exact flags we support.
'''
if '[0]' not in flag_name or '--help' in unparsed_arguments:
if '[0]' not in flag_name or not unparsed_arguments or '--help' in unparsed_arguments:
return
pattern = re.compile(f'^--{flag_name.replace("[0]", r"\[\d+\]").replace(".", r"\.")}$')
existing_flags = set(
itertools.chain(
*(group_action.option_strings for group_action in arguments_group._group_actions)
pattern = re.compile(fr'^--{flag_name.replace("[0]", r"\[\d+\]").replace(".", r"\.")}$')
try:
# Find an existing list index flag (and its action) corresponding to the given flag name.
(argument_action, existing_flag_name) = next(
(action, action_flag_name)
for action in arguments_group._group_actions
for action_flag_name in action.option_strings
if pattern.match(action_flag_name)
if f'--{flag_name}'.startswith(action_flag_name)
)
)
# Based on the type of the action (e.g. argparse._StoreTrueAction), look up the corresponding
# action registry name (e.g., "store_true") to pass to add_argument(action=...) below.
action_registry_name = next(
registry_name
for registry_name, action_type in arguments_group._registries['action'].items()
# Not using isinstance() here because we only want an exact match—no parent classes.
if type(argument_action) is action_type
)
except StopIteration:
return
for unparsed in unparsed_arguments:
unparsed_flag_name = unparsed.split('=', 1)[0]
destination_name = unparsed_flag_name.lstrip('-').replace('-', '_')
if pattern.match(unparsed_flag_name) and unparsed_flag_name not in existing_flags:
if not pattern.match(unparsed_flag_name) or unparsed_flag_name == existing_flag_name:
continue
if action_registry_name in ('store_true', 'store_false'):
arguments_group.add_argument(
unparsed_flag_name,
type=argument_type,
metavar=metavar,
help=description,
action=action_registry_name,
default=argument_action.default,
dest=destination_name,
required=argument_action.nargs,
)
else:
arguments_group.add_argument(
unparsed_flag_name,
action=action_registry_name,
choices=argument_action.choices,
default=argument_action.default,
dest=destination_name,
nargs=argument_action.nargs,
required=argument_action.nargs,
type=argument_action.type,
)
@ -407,10 +439,15 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
And if names are also passed in, they are considered to be the name components of an option
(e.g. "foo" and "bar") and are used to construct a resulting flag.
Bail if the schema is not a dict.
'''
if names is None:
names = ()
if not isinstance(schema, dict):
return
schema_type = schema.get('type')
# If this option has multiple types, just use the first one (that isn't "null").
@ -424,18 +461,21 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
if schema_type == 'object':
properties = schema.get('properties')
# If there are child properties, recurse for each one. But if there are no child properties,
# fall through so that a flag gets added below for the (empty) object.
if properties:
for name, child in properties.items():
add_arguments_from_schema(
arguments_group, child, unparsed_arguments, names + (name,)
)
return
return
# If this is an "array" type, recurse for each child option of its items type. Don't return yet,
# so that a flag also gets added below for the array itself.
# If this is an "array" type, recurse for each items type child option. Don't return yet so that
# a flag also gets added below for the array itself.
if schema_type == 'array':
properties = borgmatic.config.schema.get_properties(schema.get('items', {}))
items = schema.get('items', {})
properties = borgmatic.config.schema.get_properties(items)
if properties:
for name, child in properties.items():
@ -445,52 +485,72 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
unparsed_arguments,
names[:-1] + (f'{names[-1]}[0]',) + (name,),
)
# If there aren't any children, then this is an array of scalars. Recurse accordingly.
else:
add_arguments_from_schema(
arguments_group, items, unparsed_arguments, names[:-1] + (f'{names[-1]}[0]',)
)
flag_name = '.'.join(names)
metavar = names[-1].upper()
flag_name = '.'.join(names).replace('_', '-')
# Certain options already have corresponding flags on individual actions (like "create
# --progress"), so don't bother adding them to the global flags.
if flag_name in OMITTED_FLAG_NAMES:
if not flag_name or flag_name in OMITTED_FLAG_NAMES:
return
metavar = names[-1].upper()
description = make_argument_description(schema, flag_name)
argument_type = borgmatic.config.schema.parse_type(schema_type)
full_flag_name = f"--{flag_name.replace('_', '-')}"
# As a UX nicety, allow boolean options that have a default of false to have command-line flags
# without values.
if schema_type == 'boolean' and schema.get('default') == False:
# The object=str and array=str given here is to support specifying an object or an array as a
# YAML string on the command-line.
argument_type = borgmatic.config.schema.parse_type(schema_type, object=str, array=str)
# As a UX nicety, add separate true and false flags for boolean options.
if schema_type == 'boolean':
arguments_group.add_argument(
full_flag_name,
f'--{flag_name}',
action='store_true',
default=None,
help=description,
)
if names[-1].startswith('no_'):
no_flag_name = '.'.join(names[:-1] + (names[-1][len('no_') :],)).replace('_', '-')
else:
no_flag_name = '.'.join(names[:-1] + ('no-' + names[-1],)).replace('_', '-')
arguments_group.add_argument(
f'--{no_flag_name}',
dest=flag_name.replace('-', '_'),
action='store_false',
default=None,
help=f'Set the --{flag_name} value to false.',
)
else:
arguments_group.add_argument(
full_flag_name,
f'--{flag_name}',
type=argument_type,
metavar=metavar,
help=description,
)
add_array_element_arguments_from_schema(arguments_group, schema, unparsed_arguments, flag_name)
add_array_element_arguments(arguments_group, unparsed_arguments, flag_name)
def make_parsers(schema, unparsed_arguments):
'''
Given a configuration schema dict, build a global arguments parser, individual action parsers,
and a combined parser containing both. Return them as a tuple. The global parser is useful for
parsing just global arguments while ignoring actions, and the combined parser is handy for
displaying help that includes everything: global flags, a list of actions, etc.
Given a configuration schema dict and unparsed arguments as a sequence of strings, build a
global arguments parser, individual action parsers, and a combined parser containing both.
Return them as a tuple. The global parser is useful for parsing just global arguments while
ignoring actions, and the combined parser is handy for displaying help that includes everything:
global flags, a list of actions, etc.
'''
config_paths = collect.get_default_config_paths(expand_home=True)
unexpanded_config_paths = collect.get_default_config_paths(expand_home=False)
# allow_abbrev=False prevents the global parser from erroring about "ambiguous" options like
# --encryption. Such options are intended for an action parser rather than the global parser,
# and so we don't want to error on them here.
# Using allow_abbrev=False here prevents the global parser from erroring about "ambiguous"
# options like --encryption. Such options are intended for an action parser rather than the
# global parser, and so we don't want to error on them here.
global_parser = ArgumentParser(allow_abbrev=False, add_help=False)
global_group = global_parser.add_argument_group('global arguments')
@ -508,9 +568,6 @@ def make_parsers(schema, unparsed_arguments):
action='store_true',
help='Go through the motions, but do not actually write to any repositories',
)
global_group.add_argument(
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
)
global_group.add_argument(
'-v',
'--verbosity',
@ -633,6 +690,7 @@ def make_parsers(schema, unparsed_arguments):
)
repo_create_group.add_argument(
'--append-only',
default=None,
action='store_true',
help='Create an append-only repository',
)
@ -642,6 +700,8 @@ def make_parsers(schema, unparsed_arguments):
)
repo_create_group.add_argument(
'--make-parent-dirs',
dest='make_parent_directories',
default=None,
action='store_true',
help='Create any missing parent directories of the repository directory',
)
@ -676,7 +736,7 @@ def make_parsers(schema, unparsed_arguments):
)
transfer_group.add_argument(
'--progress',
default=False,
default=None,
action='store_true',
help='Display progress as each archive is transferred',
)
@ -743,13 +803,17 @@ def make_parsers(schema, unparsed_arguments):
)
prune_group.add_argument(
'--stats',
dest='stats',
default=False,
dest='statistics',
default=None,
action='store_true',
help='Display statistics of the pruned archive [Borg 1 only]',
)
prune_group.add_argument(
'--list', dest='list_archives', action='store_true', help='List archives kept/pruned'
'--list',
dest='list_details',
default=None,
action='store_true',
help='List archives kept/pruned',
)
prune_group.add_argument(
'--oldest',
@ -787,8 +851,7 @@ def make_parsers(schema, unparsed_arguments):
)
compact_group.add_argument(
'--progress',
dest='progress',
default=False,
default=None,
action='store_true',
help='Display progress as each segment is compacted',
)
@ -802,7 +865,7 @@ def make_parsers(schema, unparsed_arguments):
compact_group.add_argument(
'--threshold',
type=int,
dest='threshold',
dest='compact_threshold',
help='Minimum saved space percentage threshold for compacting a segment, defaults to 10',
)
compact_group.add_argument(
@ -823,20 +886,24 @@ def make_parsers(schema, unparsed_arguments):
)
create_group.add_argument(
'--progress',
dest='progress',
default=False,
default=None,
action='store_true',
help='Display progress for each file as it is backed up',
)
create_group.add_argument(
'--stats',
dest='stats',
default=False,
dest='statistics',
default=None,
action='store_true',
help='Display statistics of archive',
)
create_group.add_argument(
'--list', '--files', dest='list_files', action='store_true', help='Show per-file details'
'--list',
'--files',
dest='list_details',
default=None,
action='store_true',
help='Show per-file details',
)
create_group.add_argument(
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
@ -857,8 +924,7 @@ def make_parsers(schema, unparsed_arguments):
)
check_group.add_argument(
'--progress',
dest='progress',
default=False,
default=None,
action='store_true',
help='Display progress for each file as it is checked',
)
@ -915,12 +981,15 @@ def make_parsers(schema, unparsed_arguments):
)
delete_group.add_argument(
'--list',
dest='list_archives',
dest='list_details',
default=None,
action='store_true',
help='Show details for the deleted archives',
)
delete_group.add_argument(
'--stats',
dest='statistics',
default=None,
action='store_true',
help='Display statistics for the deleted archives',
)
@ -1025,8 +1094,7 @@ def make_parsers(schema, unparsed_arguments):
)
extract_group.add_argument(
'--progress',
dest='progress',
default=False,
default=None,
action='store_true',
help='Display progress for each file as it is extracted',
)
@ -1101,8 +1169,7 @@ def make_parsers(schema, unparsed_arguments):
)
config_bootstrap_group.add_argument(
'--progress',
dest='progress',
default=False,
default=None,
action='store_true',
help='Display progress for each file as it is extracted',
)
@ -1195,7 +1262,12 @@ def make_parsers(schema, unparsed_arguments):
'--tar-filter', help='Name of filter program to pipe data through'
)
export_tar_group.add_argument(
'--list', '--files', dest='list_files', action='store_true', help='Show per-file details'
'--list',
'--files',
dest='list_details',
default=None,
action='store_true',
help='Show per-file details',
)
export_tar_group.add_argument(
'--strip-components',
@ -1306,7 +1378,8 @@ def make_parsers(schema, unparsed_arguments):
)
repo_delete_group.add_argument(
'--list',
dest='list_archives',
dest='list_details',
default=None,
action='store_true',
help='Show details for the archives in the given repository',
)
@ -1720,6 +1793,56 @@ def make_parsers(schema, unparsed_arguments):
'-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 (with Borg 1.2+, you must run compact afterwards to actually free space)',
description='Recreate an archive in a repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
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_details',
default=None,
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 [Borg 2.x+ only]',
)
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'],
@ -1750,8 +1873,8 @@ def make_parsers(schema, unparsed_arguments):
def parse_arguments(schema, *unparsed_arguments):
'''
Given a configuration schema dict and the command-line arguments with which this script was
invoked, parse the arguments and return them as a dict mapping from action name (or "global") to
an argparse.Namespace instance.
invoked and unparsed arguments as a sequence of strings, parse the arguments and return them as
a dict mapping from action name (or "global") to an argparse.Namespace instance.
Raise ValueError if the arguments cannot be parsed.
Raise SystemExit with an error code of 0 if "--help" was requested.
@ -1786,15 +1909,6 @@ def parse_arguments(schema, *unparsed_arguments):
f"Unrecognized argument{'s' if len(unknown_arguments) > 1 else ''}: {' '.join(unknown_arguments)}"
)
if 'create' in arguments and arguments['create'].list_files and arguments['create'].progress:
raise ValueError(
'With the create action, only one of --list (--files) and --progress flags can be used.'
)
if 'create' in arguments and arguments['create'].list_files and arguments['create'].json:
raise ValueError(
'With the create action, only one of --list (--files) and --json flags can be used.'
)
if (
('list' in arguments and 'repo-info' in arguments and arguments['list'].json)
or ('list' in arguments and 'info' in arguments and arguments['list'].json)
@ -1802,15 +1916,6 @@ def parse_arguments(schema, *unparsed_arguments):
):
raise ValueError('With the --json flag, multiple actions cannot be used together.')
if (
'transfer' in arguments
and arguments['transfer'].archive
and arguments['transfer'].match_archives
):
raise ValueError(
'With the transfer action, only one of --archive and --match-archives flags can be used.'
)
if 'list' in arguments and (arguments['list'].prefix and arguments['list'].match_archives):
raise ValueError(
'With the list action, only one of --prefix or --match-archives flags can be used.'

View file

@ -28,6 +28,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
@ -400,6 +401,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,
@ -589,14 +600,14 @@ def run_actions(
)
def load_configurations(config_filenames, global_arguments, overrides=None, resolve_env=True):
def load_configurations(config_filenames, arguments, overrides=None, resolve_env=True):
'''
Given a sequence of configuration filenames, global arguments as an argparse.Namespace, a
sequence of configuration file override strings in the form of "option.suboption=value", and
whether to resolve environment variables, load and validate each configuration file. Return the
results as a tuple of: dict of configuration filename to corresponding parsed configuration, a
sequence of paths for all loaded configuration files (including includes), and a sequence of
logging.LogRecord instances containing any parse errors.
Given a sequence of configuration filenames, arguments as a dict from action name to
argparse.Namespace, a sequence of configuration file override strings in the form of
"option.suboption=value", and whether to resolve environment variables, load and validate each
configuration file. Return the results as a tuple of: dict of configuration filename to
corresponding parsed configuration, a sequence of paths for all loaded configuration files
(including includes), and a sequence of logging.LogRecord instances containing any parse errors.
Log records are returned here instead of being logged directly because logging isn't yet
initialized at this point! (Although with the Delayed_logging_handler now in place, maybe this
@ -624,7 +635,7 @@ def load_configurations(config_filenames, global_arguments, overrides=None, reso
configs[config_filename], paths, parse_logs = validate.parse_configuration(
config_filename,
validate.schema_filename(),
global_arguments,
arguments,
overrides,
resolve_env,
)
@ -902,7 +913,7 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments):
dict(
levelno=logging.INFO,
levelname='INFO',
msg='Successfully ran configuration file',
msg=f'{config_filename}: Successfully ran configuration file',
)
)
if results:
@ -947,6 +958,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()
@ -986,10 +1010,14 @@ def main(extra_summary_logs=[]): # pragma: no cover
config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
configs, config_paths, parse_logs = load_configurations(
config_filenames,
global_arguments,
arguments,
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
)
@ -997,7 +1025,7 @@ def main(extra_summary_logs=[]): # pragma: no cover
any_json_flags = any(
getattr(sub_arguments, 'json', False) for sub_arguments in arguments.values()
)
color_enabled = should_do_markup(global_arguments.no_color or any_json_flags, configs)
color_enabled = should_do_markup(configs, any_json_flags)
try:
configure_logging(

View file

@ -1,5 +1,7 @@
import borgmatic.commands.arguments
import borgmatic.commands.completion.actions
import borgmatic.commands.completion.flag
import borgmatic.config.validate
def parser_flags(parser):
@ -7,7 +9,12 @@ def parser_flags(parser):
Given an argparse.ArgumentParser instance, return its argument flags in a space-separated
string.
'''
return ' '.join(option for action in parser._actions for option in action.option_strings)
return ' '.join(
flag_variant
for action in parser._actions
for flag_name in action.option_strings
for flag_variant in borgmatic.commands.completion.flag.variants(flag_name)
)
def bash_completion():
@ -19,7 +26,10 @@ def bash_completion():
unused_global_parser,
action_parsers,
global_plus_action_parser,
) = borgmatic.commands.arguments.make_parsers()
) = borgmatic.commands.arguments.make_parsers(
schema=borgmatic.config.validate.load_schema(borgmatic.config.validate.schema_filename()),
unparsed_arguments=(),
)
global_flags = parser_flags(global_plus_action_parser)
# Avert your eyes.

View file

@ -4,6 +4,7 @@ from textwrap import dedent
import borgmatic.commands.arguments
import borgmatic.commands.completion.actions
import borgmatic.config.validate
def has_file_options(action: Action):
@ -26,9 +27,11 @@ def has_choice_options(action: Action):
def has_unknown_required_param_options(action: Action):
'''
A catch-all for options that take a required parameter, but we don't know what the parameter is.
This should be used last. These are actions that take something like a glob, a list of numbers, or a string.
This should be used last. These are actions that take something like a glob, a list of numbers,
or a string.
Actions that match this pattern should not show the normal arguments, because those are unlikely to be valid.
Actions that match this pattern should not show the normal arguments, because those are unlikely
to be valid.
'''
return (
action.required is True
@ -52,9 +55,9 @@ def has_exact_options(action: Action):
def exact_options_completion(action: Action):
'''
Given an argparse.Action instance, return a completion invocation that forces file completions, options completion,
or just that some value follow the action, if the action takes such an argument and was the last action on the
command line prior to the cursor.
Given an argparse.Action instance, return a completion invocation that forces file completions,
options completion, or just that some value follow the action, if the action takes such an
argument and was the last action on the command line prior to the cursor.
Otherwise, return an empty string.
'''
@ -80,8 +83,9 @@ def exact_options_completion(action: Action):
def dedent_strip_as_tuple(string: str):
'''
Dedent a string, then strip it to avoid requiring your first line to have content, then return a tuple of the string.
Makes it easier to write multiline strings for completions when you join them with a tuple.
Dedent a string, then strip it to avoid requiring your first line to have content, then return a
tuple of the string. Makes it easier to write multiline strings for completions when you join
them with a tuple.
'''
return (dedent(string).strip('\n'),)
@ -95,7 +99,10 @@ def fish_completion():
unused_global_parser,
action_parsers,
global_plus_action_parser,
) = borgmatic.commands.arguments.make_parsers()
) = borgmatic.commands.arguments.make_parsers(
schema=borgmatic.config.validate.load_schema(borgmatic.config.validate.schema_filename()),
unparsed_arguments=(),
)
all_action_parsers = ' '.join(action for action in action_parsers.choices.keys())

View file

@ -0,0 +1,13 @@
def variants(flag_name):
'''
Given a flag name as a string, yield it and any variations that should be complete-able as well.
For instance, for a string like "--foo[0].bar", yield "--foo[0].bar", "--foo[1].bar", ...,
"--foo[9].bar".
'''
if '[0]' in flag_name:
for index in range(0, 10):
yield flag_name.replace('[0]', f'[{index}]')
return
yield flag_name

View file

@ -3,6 +3,8 @@ import re
import ruamel.yaml
import borgmatic.config.schema
LIST_INDEX_KEY_PATTERN = re.compile(r'^(?P<list_name>[a-zA-z-]+)\[(?P<index>\d+)\]$')
@ -34,18 +36,18 @@ def set_values(config, keys, value):
list_key = match.group('list_name')
list_index = int(match.group('index'))
if len(keys) == 1:
config[list_key][list_index] = value
return
if list_key not in config:
config[list_key] = []
try:
if len(keys) == 1:
config[list_key][list_index] = value
return
if list_key not in config:
config[list_key] = []
set_values(config[list_key][list_index], keys[1:], value)
except IndexError:
raise ValueError(f'The list index {first_key} is out of range')
except (IndexError, KeyError):
raise ValueError(f'Argument list index {first_key} is out of range')
return
@ -73,12 +75,13 @@ def type_for_option(schema, option_keys):
for key in option_keys:
# Support "name[0]"-style list index syntax.
match = LIST_INDEX_KEY_PATTERN.match(key)
properties = borgmatic.config.schema.get_properties(option_schema)
try:
if match:
option_schema = option_schema['properties'][match.group('list_name')]['items']
option_schema = properties[match.group('list_name')]['items']
else:
option_schema = option_schema['properties'][key]
option_schema = properties[key]
except KeyError:
return None
@ -99,6 +102,7 @@ def convert_value_type(value, option_type):
And if the source value isn't a string, return it as-is.
Raise ruamel.yaml.error.YAMLError if there's a parse issue with the YAML.
Raise ValueError if the parsed value doesn't match the option type.
'''
if not isinstance(value, str):
return value
@ -106,7 +110,15 @@ def convert_value_type(value, option_type):
if option_type == 'string':
return value
return ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
try:
parsed_value = ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
except ruamel.yaml.error.YAMLError as error:
raise ValueError(f'Argument value "{value}" is invalid: {error.problem}')
if not isinstance(parsed_value, borgmatic.config.schema.parse_type(option_type)):
raise ValueError(f'Argument value "{value}" is not of the expected type: {option_type}')
return parsed_value
def prepare_arguments_for_config(global_arguments, schema):
@ -122,48 +134,43 @@ def prepare_arguments_for_config(global_arguments, schema):
(
(('my_option', 'sub_option'), 'value1'),
(('other_option'), 'value2'),
(('other_option',), 'value2'),
)
Raise ValueError if an override can't be parsed.
'''
prepared_values = []
for argument_name, value in global_arguments.__dict__.items():
try:
if value is None:
continue
if value is None:
continue
keys = tuple(argument_name.split('.'))
option_type = type_for_option(schema, keys)
keys = tuple(argument_name.split('.'))
option_type = type_for_option(schema, keys)
# The argument doesn't correspond to any option in the schema, so ignore it. It's
# probably a flag that borgmatic has on the command-line but not in configuration.
if option_type is None:
continue
# The argument doesn't correspond to any option in the schema, so ignore it. It's
# probably a flag that borgmatic has on the command-line but not in configuration.
if option_type is None:
continue
prepared_values.append(
(
keys,
convert_value_type(value, option_type),
)
prepared_values.append(
(
keys,
convert_value_type(value, option_type),
)
except ruamel.yaml.error.YAMLError as error:
raise ValueError(f'Invalid override "{argument_name}": {error.problem}')
)
return tuple(prepared_values)
def apply_arguments_to_config(config, schema, global_arguments):
def apply_arguments_to_config(config, schema, arguments):
'''
Given a configuration dict, a corresponding configuration schema dict, and global arguments as
an argparse.Namespace, set those given argument values into their corresponding configuration
options in the configuration dict.
Given a configuration dict, a corresponding configuration schema dict, and arguments as a dict
from action name to argparse.Namespace, set those given argument values into their corresponding
configuration options in the configuration dict.
This supports argument flags of the from "--foo.bar.baz" where each dotted component is a nested
configuration object. Additionally, flags like "--foo.bar[0].baz" are supported to update a list
element in the configuration.
'''
for keys, value in prepare_arguments_for_config(global_arguments, schema):
set_values(config, keys, value)
for action_arguments in arguments.values():
for keys, value in prepare_arguments_for_config(action_arguments, schema):
set_values(config, keys, value)

View file

@ -36,10 +36,12 @@ def schema_to_sample_configuration(schema, source_config=None, level=0, parent_i
schema_type = schema.get('type')
example = schema.get('example')
if schema_type == 'array' or (isinstance(schema_type, list) and 'array' in schema_type):
if borgmatic.config.schema.compare_types(schema_type, {'array'}):
config = ruamel.yaml.comments.CommentedSeq(
example
if schema['items'].get('type') in SCALAR_SCHEMA_TYPES
if borgmatic.config.schema.compare_types(
schema['items'].get('type'), SCALAR_SCHEMA_TYPES
)
else [
schema_to_sample_configuration(
schema['items'], source_config, level, parent_is_sequence=True
@ -47,30 +49,31 @@ def schema_to_sample_configuration(schema, source_config=None, level=0, parent_i
]
)
add_comments_to_configuration_sequence(config, schema, indent=(level * INDENT))
elif schema_type == 'object' or (isinstance(schema_type, list) and 'object' in schema_type):
elif borgmatic.config.schema.compare_types(schema_type, {'object'}):
if source_config and isinstance(source_config, list) and isinstance(source_config[0], dict):
source_config = dict(collections.ChainMap(*source_config))
config = ruamel.yaml.comments.CommentedMap(
[
(
field_name,
schema_to_sample_configuration(
sub_schema, (source_config or {}).get(field_name, {}), level + 1
),
)
for field_name, sub_schema in borgmatic.config.schema.get_properties(schema).items()
]
config = (
ruamel.yaml.comments.CommentedMap(
[
(
field_name,
schema_to_sample_configuration(
sub_schema, (source_config or {}).get(field_name, {}), level + 1
),
)
for field_name, sub_schema in borgmatic.config.schema.get_properties(
schema
).items()
]
)
or example
)
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
add_comments_to_configuration_object(
config, schema, source_config, indent=indent, skip_first=parent_is_sequence
)
elif isinstance(schema_type, list) and all(
element_schema_type in SCALAR_SCHEMA_TYPES for element_schema_type in schema_type
):
return example
elif schema_type in SCALAR_SCHEMA_TYPES:
elif borgmatic.config.schema.compare_types(schema_type, SCALAR_SCHEMA_TYPES, match=all):
return example
else:
raise ValueError(f'Schema at level {level} is unsupported: {schema}')

View file

@ -23,19 +23,50 @@ def get_properties(schema):
return schema.get('properties', {})
def parse_type(schema_type):
SCHEMA_TYPE_TO_PYTHON_TYPE = {
'array': list,
'boolean': bool,
'integer': int,
'number': decimal.Decimal,
'object': dict,
'string': str,
}
def parse_type(schema_type, **overrides):
'''
Given a schema type as a string, return the corresponding Python type.
If any overrides are given in the from of a schema type string to a Python type, then override
the default type mapping with them.
Raise ValueError if the schema type is unknown.
'''
try:
return {
'string': str,
'integer': int,
'number': decimal.Decimal,
'boolean': bool,
'array': str,
}[schema_type]
return dict(
SCHEMA_TYPE_TO_PYTHON_TYPE,
**overrides,
)[schema_type]
except KeyError:
raise ValueError(f'Unknown type in configuration schema: {schema_type}')
def compare_types(schema_type, target_types, match=any):
'''
Given a schema type as a string or a list of strings (representing multiple types) and a set of
target type strings, return whether every schema type is in the set of target types.
If the schema type is a list of strings, use the given match function (such as any or all) to
compare elements. For instance, if match is given as all, then every element of the schema_type
list must be in the target types.
'''
if isinstance(schema_type, list):
if match(element_schema_type in target_types for element_schema_type in schema_type):
return True
return False
if schema_type in target_types:
return True
return False

View file

@ -33,11 +33,12 @@ properties:
type: object
required:
- path
additionalProperties: false
properties:
path:
type: string
description: The local path or Borg URL of the repository.
example: ssh://user@backupserver/./{fqdn}
example: ssh://user@backupserver/./sourcehostname.borg
label:
type: string
description: |
@ -58,7 +59,6 @@ properties:
description: |
Whether the repository should be created append-only,
only used for the repo-create action. Defaults to false.
default: false
example: true
storage_quota:
type: string
@ -67,13 +67,12 @@ properties:
only used for the repo-create action. Defaults to no
quota.
example: 5G
make_parent_dirs:
make_parent_directories:
type: boolean
description: |
Whether any missing parent directories of the repository
path should be created, only used for the repo-create
action. Defaults to false.
default: false
example: true
description: |
A required list of local or remote repositories with paths and
@ -103,14 +102,12 @@ properties:
description: |
Stay in same file system; do not cross mount points beyond the given
source directories. Defaults to false.
default: false
example: true
numeric_ids:
type: boolean
description: |
Only store/extract numeric user and group identifiers. Defaults to
false.
default: false
example: true
atime:
type: boolean
@ -121,13 +118,11 @@ properties:
ctime:
type: boolean
description: Store ctime into archive. Defaults to true.
default: true
example: false
birthtime:
type: boolean
description: |
Store birthtime (creation date) into archive. Defaults to true.
default: true
example: false
read_special:
type: boolean
@ -137,14 +132,12 @@ properties:
used when backing up special devices such as /dev/zero. Defaults to
false. But when a database hook is used, the setting here is ignored
and read_special is considered true.
default: false
example: true
flags:
type: boolean
description: |
Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive.
Defaults to true.
default: true
example: false
files_cache:
type: string
@ -218,7 +211,6 @@ properties:
Exclude directories that contain a CACHEDIR.TAG file. See
http://www.brynosaurus.com/cachedir/spec.html for details. Defaults
to false.
default: false
example: true
exclude_if_present:
type: array
@ -235,13 +227,11 @@ properties:
If true, the exclude_if_present filename is included in backups.
Defaults to false, meaning that the exclude_if_present filename is
omitted from backups.
default: false
example: true
exclude_nodump:
type: boolean
description: |
Exclude files with the NODUMP flag. Defaults to false.
default: false
example: true
borgmatic_source_directory:
type: string
@ -273,7 +263,6 @@ properties:
description: |
If true, then source directories (and root pattern paths) must
exist. If they don't, an error is raised. Defaults to false.
default: false
example: true
encryption_passcommand:
type: string
@ -328,6 +317,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: |
@ -471,21 +476,18 @@ properties:
description: |
Bypass Borg error about a repository that has been moved. Defaults
to false.
default: false
example: true
unknown_unencrypted_repo_access_is_ok:
type: boolean
description: |
Bypass Borg error about a previously unknown unencrypted repository.
Defaults to false.
default: false
example: true
check_i_know_what_i_am_doing:
type: boolean
description: |
Bypass Borg confirmation about check with repair option. Defaults to
false and an interactive prompt from Borg.
default: false
example: true
extra_borg_options:
type: object
@ -811,9 +813,7 @@ properties:
color:
type: boolean
description: |
Apply color to console output. Can be overridden with --no-color
command-line flag. Defaults to true.
default: true
Apply color to console output. Defaults to true.
example: false
progress:
type: boolean
@ -821,23 +821,20 @@ properties:
Display progress as each file or archive is processed when running
supported actions. Corresponds to the "--progress" flag on those
actions. Defaults to false.
default: false
example: true
stats:
statistics:
type: boolean
description: |
Display statistics for an archive when running supported actions.
Corresponds to the "--stats" flag on those actions. Defaults to
false.
default: false
example: true
list:
list_details:
type: boolean
description: |
Display details for each file or archive as it is processed when
running supported actions. Corresponds to the "--list" flag on those
actions. Defaults to false.
default: false
example: true
skip_actions:
type: array
@ -849,6 +846,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@ -1064,6 +1062,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@ -1128,6 +1127,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@ -1168,6 +1168,7 @@ properties:
run: [echo Backing up.]
bootstrap:
type: object
additionalProperties: false
properties:
store_config_files:
type: boolean
@ -1176,7 +1177,6 @@ properties:
backup itself. Defaults to true. Changing this to false
prevents "borgmatic bootstrap" from extracting configuration
files from the backup.
default: true
example: false
description: |
Support for the "borgmatic bootstrap" action, used to extract
@ -1261,7 +1261,6 @@ properties:
schema elements. These statements will fail unless the
initial connection to the database is made by a
superuser.
default: false
example: true
format:
type: string
@ -1500,7 +1499,6 @@ properties:
Use the "--add-drop-database" flag with mariadb-dump,
causing the database to be dropped right before restore.
Defaults to true.
default: true
example: false
options:
type: string
@ -1648,7 +1646,6 @@ properties:
Use the "--add-drop-database" flag with mysqldump,
causing the database to be dropped right before restore.
Defaults to true.
default: true
example: false
options:
type: string
@ -1834,6 +1831,25 @@ properties:
dump command, without performing any validation on them.
See mongorestore documentation for details.
example: --restoreDbUsersAndRoles
mongodump_command:
type: string
description: |
Command to use instead of "mongodump". This can be used
to run a specific mongodump version (e.g., one inside a
running container). If you run it from within a
container, make sure to mount the path in the
"user_runtime_directory" option from the host into the
container at the same location. Defaults to
"mongodump".
example: docker exec mongodb_container mongodump
mongorestore_command:
type: string
description: |
Command to run when restoring a database instead of
"mongorestore". This can be used to run a specific
mongorestore version (e.g., one inside a running
container). Defaults to "mongorestore".
example: docker exec mongodb_container mongorestore
description: |
List of one or more MongoDB databases to dump before creating a
backup, run once per configuration file. The database dumps are
@ -1880,6 +1896,7 @@ properties:
example: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
start:
type: object
additionalProperties: false
properties:
title:
type: string
@ -1903,6 +1920,7 @@ properties:
example: incoming_envelope
finish:
type: object
additionalProperties: false
properties:
title:
type: string
@ -1926,6 +1944,7 @@ properties:
example: incoming_envelope
fail:
type: object
additionalProperties: false
properties:
title:
type: string
@ -1984,6 +2003,7 @@ properties:
example: hwRwoWsXMBWwgrSecfa9EfPey55WSN
start:
type: object
additionalProperties: false
properties:
message:
type: string
@ -2059,6 +2079,7 @@ properties:
example: Pushover Link
finish:
type: object
additionalProperties: false
properties:
message:
type: string
@ -2134,6 +2155,7 @@ properties:
example: Pushover Link
fail:
type: object
additionalProperties: false
properties:
message:
type: string
@ -2273,6 +2295,7 @@ properties:
example: fakekey
start:
type: object
additionalProperties: false
properties:
value:
type: ["integer", "string"]
@ -2281,6 +2304,7 @@ properties:
example: STARTED
finish:
type: object
additionalProperties: false
properties:
value:
type: ["integer", "string"]
@ -2289,6 +2313,7 @@ properties:
example: FINISH
fail:
type: object
additionalProperties: false
properties:
value:
type: ["integer", "string"]
@ -2320,6 +2345,7 @@ properties:
type: array
items:
type: object
additionalProperties: false
required:
- url
- label
@ -2347,9 +2373,8 @@ properties:
send_logs:
type: boolean
description: |
Send borgmatic logs to Apprise services as part the
Send borgmatic logs to Apprise services as part of the
"finish", "fail", and "log" states. Defaults to true.
default: true
example: false
logs_size_limit:
type: integer
@ -2361,6 +2386,7 @@ properties:
start:
type: object
required: ['body']
additionalProperties: false
properties:
title:
type: string
@ -2376,6 +2402,7 @@ properties:
finish:
type: object
required: ['body']
additionalProperties: false
properties:
title:
type: string
@ -2391,6 +2418,7 @@ properties:
fail:
type: object
required: ['body']
additionalProperties: false
properties:
title:
type: string
@ -2406,6 +2434,7 @@ properties:
log:
type: object
required: ['body']
additionalProperties: false
properties:
title:
type: string
@ -2455,14 +2484,12 @@ properties:
description: |
Verify the TLS certificate of the ping URL host. Defaults to
true.
default: true
example: false
send_logs:
type: boolean
description: |
Send borgmatic logs to Healthchecks as part the "finish",
Send borgmatic logs to Healthchecks as part of the "finish",
"fail", and "log" states. Defaults to true.
default: true
example: false
ping_body_limit:
type: integer
@ -2495,7 +2522,6 @@ properties:
the slug URL scheme (https://hc-ping.com/<ping-key>/<slug>
as opposed to https://hc-ping.com/<uuid>).
Defaults to false.
default: false
example: true
description: |
Configuration for a monitoring integration with Healthchecks. Create
@ -2535,7 +2561,6 @@ properties:
description: |
Verify the TLS certificate of the push URL host. Defaults to
true.
default: true
example: false
description: |
Configuration for a monitoring integration with Uptime Kuma using
@ -2572,7 +2597,6 @@ properties:
description: |
Send borgmatic logs to PagerDuty when a backup errors.
Defaults to true.
default: true
example: false
description: |
Configuration for a monitoring integration with PagerDuty. Create an
@ -2766,5 +2790,27 @@ properties:
description: |
Command to use instead of "keepassxc-cli".
example: /usr/local/bin/keepassxc-cli
key_file:
type: string
description: |
Path to a key file for unlocking the KeePassXC database.
example: /path/to/keyfile
yubikey:
type: string
description: |
YubiKey slot and optional serial number used to access the
KeePassXC database. The format is "<slot[:serial]>", where:
* <slot> is the YubiKey slot number (e.g., `1` or `2`).
* <serial> (optional) is the YubiKey's serial number (e.g.,
`7370001`).
example: "1:7370001"
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

@ -21,6 +21,18 @@ def schema_filename():
return schema_path
def load_schema(schema_path): # pragma: no cover
'''
Given a schema filename path, load the schema and return it as a dict.
Raise Validation_error if the schema could not be parsed.
'''
try:
return load.load_configuration(schema_path)
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
raise Validation_error(schema_path, (str(error),))
def format_json_error_path_element(path_element):
'''
Given a path element into a JSON data structure, format it for display as a string.
@ -85,14 +97,14 @@ def apply_logical_validation(config_filename, parsed_configuration):
def parse_configuration(
config_filename, schema_filename, global_arguments, overrides=None, resolve_env=True
config_filename, schema_filename, arguments, overrides=None, resolve_env=True
):
'''
Given the path to a config filename in YAML format, the path to a schema filename in a YAML
rendition of JSON Schema format, global arguments as an argparse.Namespace, a sequence of
configuration file override strings in the form of "option.suboption=value", and whether to
resolve environment variables, return the parsed configuration as a data structure of nested
dicts and lists corresponding to the schema. Example return value.
rendition of JSON Schema format, arguments as dict from action name to argparse.Namespace, a
sequence of configuration file override strings in the form of "option.suboption=value", and
whether to resolve environment variables, return the parsed configuration as a data structure of
nested dicts and lists corresponding to the schema. Example return value.
Example return value:
@ -117,7 +129,7 @@ def parse_configuration(
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
raise Validation_error(config_filename, (str(error),))
borgmatic.config.arguments.apply_arguments_to_config(config, schema, global_arguments)
borgmatic.config.arguments.apply_arguments_to_config(config, schema, arguments)
override.apply_overrides(config, schema, overrides)
constants.apply_constants(config, config.get('constants') if config else {})

View file

@ -19,9 +19,11 @@ def load_credential(hook_config, config, credential_parameters):
raise ValueError(f'Cannot load invalid credential: "{name}"')
expanded_credential_path = os.path.expanduser(credential_path)
try:
with open(
os.path.join(config.get('working_directory', ''), credential_path)
os.path.join(config.get('working_directory', ''), expanded_credential_path)
) as credential_file:
return credential_file.read().rstrip(os.linesep)
except (FileNotFoundError, OSError) as error:

View file

@ -11,32 +11,35 @@ def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch
the corresponidng KeePassXC credential and return it.
the corresponding KeePassXC credential and return it.
Raise ValueError if keepassxc-cli can't retrieve the credential.
'''
try:
(database_path, attribute_name) = credential_parameters
except ValueError:
path_and_name = ' '.join(credential_parameters)
raise ValueError(f'Invalid KeePassXC credential parameters: {credential_parameters}')
raise ValueError(
f'Cannot load credential with invalid KeePassXC database path and attribute name: "{path_and_name}"'
)
expanded_database_path = os.path.expanduser(database_path)
if not os.path.exists(database_path):
raise ValueError(
f'Cannot load credential because KeePassXC database path does not exist: {database_path}'
)
if not os.path.exists(expanded_database_path):
raise ValueError(f'KeePassXC database path does not exist: {database_path}')
return borgmatic.execute.execute_command_and_capture_output(
# Build the keepassxc-cli command.
command = (
tuple(shlex.split((hook_config or {}).get('keepassxc_cli_command', 'keepassxc-cli')))
+ ('show', '--show-protected', '--attributes', 'Password')
+ (
'show',
'--show-protected',
'--attributes',
'Password',
database_path,
attribute_name,
('--key-file', hook_config['key_file'])
if hook_config and hook_config.get('key_file')
else ()
)
).rstrip(os.linesep)
+ (
('--yubikey', hook_config['yubikey'])
if hook_config and hook_config.get('yubikey')
else ()
)
+ (expanded_database_path, attribute_name) # Ensure database and entry are last.
)
return borgmatic.execute.execute_command_and_capture_output(command).rstrip(os.linesep)

View file

@ -5,7 +5,7 @@ import re
logger = logging.getLogger(__name__)
CREDENTIAL_NAME_PATTERN = re.compile(r'^\w+$')
CREDENTIAL_NAME_PATTERN = re.compile(r'^[\w.-]+$')
def load_credential(hook_config, config, credential_parameters):

View file

@ -114,14 +114,17 @@ def make_password_config_file(password):
def build_dump_command(database, config, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
Return the custom mongodump_command from a single database configuration.
'''
all_databases = database['name'] == 'all'
password = borgmatic.hooks.credential.parse.resolve_credential(database.get('password'), config)
dump_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mongodump_command') or 'mongodump')
)
return (
('mongodump',)
dump_command
+ (('--out', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
+ (('--host', shlex.quote(database['hostname'])) if 'hostname' in database else ())
+ (('--port', shlex.quote(str(database['port']))) if 'port' in database else ())
@ -230,7 +233,7 @@ def restore_data_source_dump(
def build_restore_command(extract_process, database, config, dump_filename, connection_params):
'''
Return the mongorestore command from a single database configuration.
Return the custom mongorestore_command from a single database configuration.
'''
hostname = connection_params['hostname'] or database.get(
'restore_hostname', database.get('hostname')
@ -251,7 +254,10 @@ def build_restore_command(extract_process, database, config, dump_filename, conn
config,
)
command = ['mongorestore']
command = list(
shlex.quote(part)
for part in shlex.split(database.get('mongorestore_command') or 'mongorestore')
)
if extract_process:
command.append('--archive')
else:

View file

@ -1,3 +1,4 @@
import os
import pathlib
IS_A_HOOK = False
@ -11,6 +12,10 @@ def get_contained_patterns(parent_directory, candidate_patterns):
paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at /var, then
/var is what we want to snapshot.
If a parent directory and a candidate pattern are on different devices, skip the pattern. That's
because any snapshot of a parent directory won't actually include "contained" directories if
they reside on separate devices.
For this function to work, a candidate pattern path can't have any globs or other non-literal
characters in the initial portion of the path that matches the parent directory. For instance, a
parent directory of /var would match a candidate pattern path of /var/log/*/data, but not a
@ -27,6 +32,8 @@ def get_contained_patterns(parent_directory, candidate_patterns):
if not candidate_patterns:
return ()
parent_device = os.stat(parent_directory).st_dev if os.path.exists(parent_directory) else None
contained_patterns = tuple(
candidate
for candidate in candidate_patterns
@ -35,6 +42,7 @@ def get_contained_patterns(parent_directory, candidate_patterns):
pathlib.PurePath(parent_directory) == candidate_path
or pathlib.PurePath(parent_directory) in candidate_path.parents
)
if candidate.device == parent_device
)
candidate_patterns -= set(contained_patterns)

View file

@ -29,12 +29,13 @@ def interactive_console():
return sys.stderr.isatty() and os.environ.get('TERM') != 'dumb'
def should_do_markup(no_color, configs):
def should_do_markup(configs, json_enabled):
'''
Given the value of the command-line no-color argument, and a dict of configuration filename to
corresponding parsed configuration, determine if we should enable color marking up.
Given a dict of configuration filename to corresponding parsed configuration (which already have
any command-line overrides applied) and whether json is enabled, determine if we should enable
color marking up.
'''
if no_color:
if json_enabled:
return False
if any(config.get('color', True) is False for config in configs.values()):

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

@ -482,16 +482,89 @@ applications, but then set the repository for each application at runtime. Or
you might want to try a variant of an option for testing purposes without
actually touching your configuration file.
<span class="minilink minilink-addedin">New in version 2.0.0</span>
Whatever the reason, you can override borgmatic configuration options at the
command-line via the `--override` flag. Here's an example:
command-line, as there's a command-line flag corresponding to every
configuration option (with its underscores converted to dashes).
For instance, to override the `compression` configuration option, use the
corresponding `--compression` flag on the command-line:
```bash
borgmatic create --compression zstd
```
What this does is load your given configuration files and for each one, disregard
the configured value for the `compression` option and use the value given on the
command-line instead—but just for the duration of the borgmatic run.
You can override nested configuration options too by separating such option
names with a period. For instance:
```bash
borgmatic create --bootstrap.store-config-files false
```
You can even set complex option data structures by using inline YAML syntax. For
example, set the `repositories` option with a YAML list of key/value pairs:
```bash
borgmatic create --repositories "[{path: /mnt/backup, label: local}]"
```
If your override value contains characters like colons or spaces, then you'll
need to use quotes for it to parse correctly.
You can also set individual nested options within existing list elements:
```bash
borgmatic create --repositories[0].path /mnt/backup
```
This updates the `path` option for the first repository in `repositories`.
Change the `[0]` index as needed to address different list elements. And note
that this only works for elements already set in configuration; you can't append
new list elements from the command-line.
See the [command-line reference
documentation](https://torsion.org/borgmatic/docs/reference/command-line/) for
the full set of available arguments, including examples of each for the complex
values.
There are a handful of configuration options that don't have corresponding
command-line flags at the global scope, but instead have flags within individual
borgmatic actions. For instance, the `list_details` option can be overridden by
the `--list` flag that's only present on particular actions. Similarly with
`progress` and `--progress`, `statistics` and `--stats`, and `match_archives`
and `--match-archives`.
Also note that if you want to pass a command-line flag itself as a value to one
of these override flags, that may not work. For instance, specifying
`--extra-borg-options.create --no-cache-sync` results in an error, because
`--no-cache-sync` gets interpreted as a borgmatic option (which in this case
doesn't exist) rather than a Borg option.
An alternate to command-line overrides is passing in your values via
[environment
variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
### Deprecated overrides
<span class="minilink minilink-addedin">Prior to version 2.0.0</span>
Configuration overrides were performed with an `--override` flag. You can still
use `--override` with borgmatic 2.0.0+, but it's deprecated in favor of the new
command-line flags described above.
Here's an example of `--override`:
```bash
borgmatic create --override remote_path=/usr/local/bin/borg1
```
What this does is load your configuration files and for each one, disregard
the configured value for the `remote_path` option and use the value of
`/usr/local/bin/borg1` instead.
What this does is load your given configuration files and for each one, disregard
the configured value for the `remote_path` option and use the value given on the
command-line instead—but just for the duration of the borgmatic run.
You can even override nested values or multiple values at once. For instance:
@ -540,10 +613,6 @@ reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
which options are list types. (YAML list values look like `- this` with an
indentation and a leading dash.)
An alternate to command-line overrides is passing in your values via
[environment
variables](https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/).
## Constant interpolation

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,55 @@
import os
import shlex
import shutil
import subprocess
import tempfile
def generate_configuration(config_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing (including injecting the given repository path and tacking on an encryption
passphrase). But don't actually set the repository path, as that's done on the command-line
below.
'''
subprocess.check_call(f'borgmatic config generate --destination {config_path}'.split(' '))
config = (
open(config_path)
.read()
.replace('- ssh://user@backupserver/./{fqdn}', '') # noqa: FS003
.replace('- /var/local/backups/local.borg', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', f'- {config_path}')
.replace('- /etc', '')
.replace('- /var/log/syslog*', '')
+ 'encryption_passphrase: "test"'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_config_flags_do_not_error():
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
original_working_directory = os.getcwd()
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path)
subprocess.check_call(
shlex.split(
f'borgmatic -v 2 --config {config_path} --repositories "[{{path: {repository_path}, label: repo}}]" repo-create --encryption repokey'
)
)
subprocess.check_call(
shlex.split(
f'borgmatic create --config {config_path} --repositories[0].path "{repository_path}"'
)
)
finally:
os.chdir(original_working_directory)
shutil.rmtree(temporary_directory)

View file

@ -53,7 +53,7 @@ def fuzz_argument(arguments, argument_name):
def test_transfer_archives_command_does_not_duplicate_flags_or_raise():
arguments = borgmatic.commands.arguments.parse_arguments(
'transfer', '--source-repository', 'foo'
{}, 'transfer', '--source-repository', 'foo'
)['transfer']
flexmock(borgmatic.borg.transfer).should_receive('execute_command').replace_with(
assert_command_does_not_duplicate_flags
@ -74,7 +74,7 @@ def test_transfer_archives_command_does_not_duplicate_flags_or_raise():
def test_prune_archives_command_does_not_duplicate_flags_or_raise():
arguments = borgmatic.commands.arguments.parse_arguments('prune')['prune']
arguments = borgmatic.commands.arguments.parse_arguments({}, 'prune')['prune']
flexmock(borgmatic.borg.prune).should_receive('execute_command').replace_with(
assert_command_does_not_duplicate_flags
)
@ -94,7 +94,7 @@ def test_prune_archives_command_does_not_duplicate_flags_or_raise():
def test_mount_archive_command_does_not_duplicate_flags_or_raise():
arguments = borgmatic.commands.arguments.parse_arguments('mount', '--mount-point', 'tmp')[
arguments = borgmatic.commands.arguments.parse_arguments({}, 'mount', '--mount-point', 'tmp')[
'mount'
]
flexmock(borgmatic.borg.mount).should_receive('execute_command').replace_with(
@ -116,7 +116,7 @@ def test_mount_archive_command_does_not_duplicate_flags_or_raise():
def test_make_list_command_does_not_duplicate_flags_or_raise():
arguments = borgmatic.commands.arguments.parse_arguments('list')['list']
arguments = borgmatic.commands.arguments.parse_arguments({}, 'list')['list']
for argument_name in dir(arguments):
if argument_name.startswith('_'):
@ -134,7 +134,7 @@ def test_make_list_command_does_not_duplicate_flags_or_raise():
def test_make_repo_list_command_does_not_duplicate_flags_or_raise():
arguments = borgmatic.commands.arguments.parse_arguments('repo-list')['repo-list']
arguments = borgmatic.commands.arguments.parse_arguments({}, 'repo-list')['repo-list']
for argument_name in dir(arguments):
if argument_name.startswith('_'):
@ -152,7 +152,7 @@ def test_make_repo_list_command_does_not_duplicate_flags_or_raise():
def test_display_archives_info_command_does_not_duplicate_flags_or_raise():
arguments = borgmatic.commands.arguments.parse_arguments('info')['info']
arguments = borgmatic.commands.arguments.parse_arguments({}, 'info')['info']
flexmock(borgmatic.borg.info).should_receive('execute_command_and_capture_output').replace_with(
assert_command_does_not_duplicate_flags
)

View file

@ -1,4 +1,5 @@
import borgmatic.commands.arguments
import borgmatic.config.validate
from borgmatic.commands.completion import actions as module
@ -7,7 +8,10 @@ def test_available_actions_uses_only_subactions_for_action_with_subactions():
unused_global_parser,
action_parsers,
unused_combined_parser,
) = borgmatic.commands.arguments.make_parsers()
) = borgmatic.commands.arguments.make_parsers(
schema=borgmatic.config.validate.load_schema(borgmatic.config.validate.schema_filename()),
unparsed_arguments=(),
)
actions = module.available_actions(action_parsers, 'config')
@ -20,7 +24,10 @@ def test_available_actions_omits_subactions_for_action_without_subactions():
unused_global_parser,
action_parsers,
unused_combined_parser,
) = borgmatic.commands.arguments.make_parsers()
) = borgmatic.commands.arguments.make_parsers(
schema=borgmatic.config.validate.load_schema(borgmatic.config.validate.schema_filename()),
unparsed_arguments=(),
)
actions = module.available_actions(action_parsers, 'list')

View file

@ -4,11 +4,144 @@ from flexmock import flexmock
from borgmatic.commands import arguments as module
def test_make_argument_description_with_object_adds_example():
assert (
module.make_argument_description(
schema={
'description': 'Thing.',
'type': 'object',
'example': {'bar': 'baz'},
},
flag_name='flag',
)
# Apparently different versions of ruamel.yaml serialize this
# differently.
in ('Thing. Example value: "bar: baz"' 'Thing. Example value: "{bar: baz}"')
)
def test_make_argument_description_with_array_adds_example():
assert (
module.make_argument_description(
schema={
'description': 'Thing.',
'type': 'array',
'example': [1, '- foo', {'bar': 'baz'}],
},
flag_name='flag',
)
# Apparently different versions of ruamel.yaml serialize this
# differently.
in (
'Thing. Example value: "[1, \'- foo\', bar: baz]"'
'Thing. Example value: "[1, \'- foo\', {bar: baz}]"'
)
)
def test_add_array_element_arguments_adds_arguments_for_array_index_flags():
parser = module.ArgumentParser(allow_abbrev=False, add_help=False)
arguments_group = parser.add_argument_group('arguments')
arguments_group.add_argument(
'--foo[0].val',
action='store_true',
dest='--foo[0].val',
)
flexmock(arguments_group).should_receive('add_argument').with_args(
'--foo[25].val',
action='store_true',
default=False,
dest='foo[25].val',
required=object,
).once()
module.add_array_element_arguments(
arguments_group=arguments_group,
unparsed_arguments=('--foo[25].val', 'fooval', '--bar[1].val', 'barval'),
flag_name='foo[0].val',
)
def test_add_arguments_from_schema_with_nested_object_adds_flag_for_each_option():
parser = module.ArgumentParser(allow_abbrev=False, add_help=False)
arguments_group = parser.add_argument_group('arguments')
flexmock(arguments_group).should_receive('add_argument').with_args(
'--foo.bar',
type=int,
metavar='BAR',
help='help 1',
).once()
flexmock(arguments_group).should_receive('add_argument').with_args(
'--foo.baz',
type=str,
metavar='BAZ',
help='help 2',
).once()
module.add_arguments_from_schema(
arguments_group=arguments_group,
schema={
'type': 'object',
'properties': {
'foo': {
'type': 'object',
'properties': {
'bar': {'type': 'integer', 'description': 'help 1'},
'baz': {'type': 'string', 'description': 'help 2'},
},
}
},
},
unparsed_arguments=(),
)
def test_add_arguments_from_schema_with_array_and_nested_object_adds_multiple_flags():
parser = module.ArgumentParser(allow_abbrev=False, add_help=False)
arguments_group = parser.add_argument_group('arguments')
flexmock(arguments_group).should_receive('add_argument').with_args(
'--foo[0].bar',
type=int,
metavar='BAR',
help=object,
).once()
flexmock(arguments_group).should_receive('add_argument').with_args(
'--foo',
type=str,
metavar='FOO',
help='help 2',
).once()
module.add_arguments_from_schema(
arguments_group=arguments_group,
schema={
'type': 'object',
'properties': {
'foo': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'bar': {
'type': 'integer',
'description': 'help 1',
}
},
},
'description': 'help 2',
}
},
},
unparsed_arguments=(),
)
def test_parse_arguments_with_no_arguments_uses_defaults():
config_paths = ['default']
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
arguments = module.parse_arguments()
arguments = module.parse_arguments({})
global_arguments = arguments['global']
assert global_arguments.config_paths == config_paths
@ -21,7 +154,7 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
def test_parse_arguments_with_multiple_config_flags_parses_as_list():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--config', 'myconfig', '--config', 'otherconfig')
arguments = module.parse_arguments({}, '--config', 'myconfig', '--config', 'otherconfig')
global_arguments = arguments['global']
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
@ -34,7 +167,7 @@ def test_parse_arguments_with_multiple_config_flags_parses_as_list():
def test_parse_arguments_with_action_after_config_path_omits_action():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--config', 'myconfig', 'list', '--json')
arguments = module.parse_arguments({}, '--config', 'myconfig', 'list', '--json')
global_arguments = arguments['global']
assert global_arguments.config_paths == ['myconfig']
@ -45,7 +178,9 @@ def test_parse_arguments_with_action_after_config_path_omits_action():
def test_parse_arguments_with_action_after_config_path_omits_aliased_action():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey')
arguments = module.parse_arguments(
{}, '--config', 'myconfig', 'init', '--encryption', 'repokey'
)
global_arguments = arguments['global']
assert global_arguments.config_paths == ['myconfig']
@ -56,7 +191,7 @@ def test_parse_arguments_with_action_after_config_path_omits_aliased_action():
def test_parse_arguments_with_action_and_positional_arguments_after_config_path_omits_action_and_arguments():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--config', 'myconfig', 'borg', 'key', 'export')
arguments = module.parse_arguments({}, '--config', 'myconfig', 'borg', 'key', 'export')
global_arguments = arguments['global']
assert global_arguments.config_paths == ['myconfig']
@ -68,7 +203,7 @@ def test_parse_arguments_with_verbosity_overrides_default():
config_paths = ['default']
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
arguments = module.parse_arguments('--verbosity', '1')
arguments = module.parse_arguments({}, '--verbosity', '1')
global_arguments = arguments['global']
assert global_arguments.config_paths == config_paths
@ -82,7 +217,7 @@ def test_parse_arguments_with_syslog_verbosity_overrides_default():
config_paths = ['default']
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
arguments = module.parse_arguments('--syslog-verbosity', '2')
arguments = module.parse_arguments({}, '--syslog-verbosity', '2')
global_arguments = arguments['global']
assert global_arguments.config_paths == config_paths
@ -96,7 +231,7 @@ def test_parse_arguments_with_log_file_verbosity_overrides_default():
config_paths = ['default']
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
arguments = module.parse_arguments('--log-file-verbosity', '-1')
arguments = module.parse_arguments({}, '--log-file-verbosity', '-1')
global_arguments = arguments['global']
assert global_arguments.config_paths == config_paths
@ -109,7 +244,7 @@ def test_parse_arguments_with_log_file_verbosity_overrides_default():
def test_parse_arguments_with_single_override_parses():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--override', 'foo.bar=baz')
arguments = module.parse_arguments({}, '--override', 'foo.bar=baz')
global_arguments = arguments['global']
assert global_arguments.overrides == ['foo.bar=baz']
@ -119,7 +254,7 @@ def test_parse_arguments_with_multiple_overrides_flags_parses():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments(
'--override', 'foo.bar=baz', '--override', 'foo.quux=7', '--override', 'this.that=8'
{}, '--override', 'foo.bar=baz', '--override', 'foo.quux=7', '--override', 'this.that=8'
)
global_arguments = arguments['global']
@ -127,7 +262,7 @@ def test_parse_arguments_with_multiple_overrides_flags_parses():
def test_parse_arguments_with_list_json_overrides_default():
arguments = module.parse_arguments('list', '--json')
arguments = module.parse_arguments({}, 'list', '--json')
assert 'list' in arguments
assert arguments['list'].json is True
@ -136,7 +271,7 @@ def test_parse_arguments_with_list_json_overrides_default():
def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments()
arguments = module.parse_arguments({})
assert 'prune' in arguments
assert 'create' in arguments
@ -146,14 +281,14 @@ def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
def test_parse_arguments_with_no_actions_passes_argument_to_relevant_actions():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--stats', '--list')
arguments = module.parse_arguments({}, '--stats', '--list')
assert 'prune' in arguments
assert arguments['prune'].stats
assert arguments['prune'].list_archives
assert arguments['prune'].statistics
assert arguments['prune'].list_details
assert 'create' in arguments
assert arguments['create'].stats
assert arguments['create'].list_files
assert arguments['create'].statistics
assert arguments['create'].list_details
assert 'check' in arguments
@ -161,7 +296,7 @@ def test_parse_arguments_with_help_and_no_actions_shows_global_help(capsys):
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit) as exit:
module.parse_arguments('--help')
module.parse_arguments({}, '--help')
assert exit.value.code == 0
captured = capsys.readouterr()
@ -173,7 +308,7 @@ def test_parse_arguments_with_help_and_action_shows_action_help(capsys):
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit) as exit:
module.parse_arguments('create', '--help')
module.parse_arguments({}, 'create', '--help')
assert exit.value.code == 0
captured = capsys.readouterr()
@ -185,7 +320,7 @@ def test_parse_arguments_with_help_and_action_shows_action_help(capsys):
def test_parse_arguments_with_action_before_global_options_parses_options():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('prune', '--verbosity', '2')
arguments = module.parse_arguments({}, 'prune', '--verbosity', '2')
assert 'prune' in arguments
assert arguments['global'].verbosity == 2
@ -194,7 +329,7 @@ def test_parse_arguments_with_action_before_global_options_parses_options():
def test_parse_arguments_with_global_options_before_action_parses_options():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--verbosity', '2', 'prune')
arguments = module.parse_arguments({}, '--verbosity', '2', 'prune')
assert 'prune' in arguments
assert arguments['global'].verbosity == 2
@ -203,7 +338,7 @@ def test_parse_arguments_with_global_options_before_action_parses_options():
def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('prune')
arguments = module.parse_arguments({}, 'prune')
assert 'prune' in arguments
assert 'create' not in arguments
@ -213,7 +348,7 @@ def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('create', 'check')
arguments = module.parse_arguments({}, 'create', 'check')
assert 'prune' not in arguments
assert 'create' in arguments
@ -224,60 +359,53 @@ def test_parse_arguments_disallows_invalid_argument():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--posix-me-harder')
module.parse_arguments({}, '--posix-me-harder')
def test_parse_arguments_disallows_encryption_mode_without_init():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--encryption', 'repokey')
module.parse_arguments({}, '--config', 'myconfig', '--encryption', 'repokey')
def test_parse_arguments_allows_encryption_mode_with_init():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey')
def test_parse_arguments_requires_encryption_mode_with_init():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--config', 'myconfig', 'init')
module.parse_arguments({}, '--config', 'myconfig', 'init', '--encryption', 'repokey')
def test_parse_arguments_disallows_append_only_without_init():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--append-only')
module.parse_arguments({}, '--config', 'myconfig', '--append-only')
def test_parse_arguments_disallows_storage_quota_without_init():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G')
module.parse_arguments({}, '--config', 'myconfig', '--storage-quota', '5G')
def test_parse_arguments_allows_init_and_prune():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'prune')
module.parse_arguments({}, '--config', 'myconfig', 'init', '--encryption', 'repokey', 'prune')
def test_parse_arguments_allows_init_and_create():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'create')
module.parse_arguments({}, '--config', 'myconfig', 'init', '--encryption', 'repokey', 'create')
def test_parse_arguments_allows_repository_with_extract():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments(
'--config', 'myconfig', 'extract', '--repository', 'test.borg', '--archive', 'test'
{}, '--config', 'myconfig', 'extract', '--repository', 'test.borg', '--archive', 'test'
)
@ -285,6 +413,7 @@ def test_parse_arguments_allows_repository_with_mount():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments(
{},
'--config',
'myconfig',
'mount',
@ -300,276 +429,247 @@ def test_parse_arguments_allows_repository_with_mount():
def test_parse_arguments_allows_repository_with_list():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'list', '--repository', 'test.borg')
module.parse_arguments({}, '--config', 'myconfig', 'list', '--repository', 'test.borg')
def test_parse_arguments_disallows_archive_unless_action_consumes_it():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--archive', 'test')
module.parse_arguments({}, '--config', 'myconfig', '--archive', 'test')
def test_parse_arguments_disallows_paths_unless_action_consumes_it():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--path', 'test')
module.parse_arguments({}, '--config', 'myconfig', '--path', 'test')
def test_parse_arguments_disallows_other_actions_with_config_bootstrap():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('config', 'bootstrap', '--repository', 'test.borg', 'list')
module.parse_arguments({}, 'config', 'bootstrap', '--repository', 'test.borg', 'list')
def test_parse_arguments_allows_archive_with_extract():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'extract', '--archive', 'test')
module.parse_arguments({}, '--config', 'myconfig', 'extract', '--archive', 'test')
def test_parse_arguments_allows_archive_with_mount():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments(
'--config', 'myconfig', 'mount', '--archive', 'test', '--mount-point', '/mnt'
{}, '--config', 'myconfig', 'mount', '--archive', 'test', '--mount-point', '/mnt'
)
def test_parse_arguments_allows_archive_with_restore():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'restore', '--archive', 'test')
module.parse_arguments({}, '--config', 'myconfig', 'restore', '--archive', 'test')
def test_parse_arguments_allows_archive_with_list():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'list', '--archive', 'test')
module.parse_arguments({}, '--config', 'myconfig', 'list', '--archive', 'test')
def test_parse_arguments_requires_archive_with_extract():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--config', 'myconfig', 'extract')
module.parse_arguments({}, '--config', 'myconfig', 'extract')
def test_parse_arguments_requires_archive_with_restore():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--config', 'myconfig', 'restore')
module.parse_arguments({}, '--config', 'myconfig', 'restore')
def test_parse_arguments_requires_mount_point_with_mount():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--config', 'myconfig', 'mount', '--archive', 'test')
module.parse_arguments({}, '--config', 'myconfig', 'mount', '--archive', 'test')
def test_parse_arguments_requires_mount_point_with_umount():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--config', 'myconfig', 'umount')
module.parse_arguments({}, '--config', 'myconfig', 'umount')
def test_parse_arguments_allows_progress_before_create():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--progress', 'create', 'list')
module.parse_arguments({}, '--progress', 'create', 'list')
def test_parse_arguments_allows_progress_after_create():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('create', '--progress', 'list')
module.parse_arguments({}, 'create', '--progress', 'list')
def test_parse_arguments_allows_progress_and_extract():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--progress', 'extract', '--archive', 'test', 'list')
module.parse_arguments({}, '--progress', 'extract', '--archive', 'test', 'list')
def test_parse_arguments_disallows_progress_without_create():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--progress', 'list')
module.parse_arguments({}, '--progress', 'list')
def test_parse_arguments_with_stats_and_create_flags_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--stats', 'create', 'list')
module.parse_arguments({}, '--stats', 'create', 'list')
def test_parse_arguments_with_stats_and_prune_flags_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--stats', 'prune', 'list')
module.parse_arguments({}, '--stats', 'prune', 'list')
def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_value_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--stats', 'list')
module.parse_arguments({}, '--stats', 'list')
def test_parse_arguments_with_list_and_create_flags_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--list', 'create')
module.parse_arguments({}, '--list', 'create')
def test_parse_arguments_with_list_and_prune_flags_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--list', 'prune')
module.parse_arguments({}, '--list', 'prune')
def test_parse_arguments_with_list_flag_but_no_relevant_action_raises_value_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--list', 'repo-create')
def test_parse_arguments_disallows_list_with_progress_for_create_action():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('create', '--list', '--progress')
def test_parse_arguments_disallows_list_with_json_for_create_action():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('create', '--list', '--json')
module.parse_arguments({}, '--list', 'repo-create')
def test_parse_arguments_allows_json_with_list_or_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('list', '--json')
module.parse_arguments('info', '--json')
module.parse_arguments({}, 'list', '--json')
module.parse_arguments({}, 'info', '--json')
def test_parse_arguments_disallows_json_with_both_list_and_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('list', 'info', '--json')
module.parse_arguments({}, 'list', 'info', '--json')
def test_parse_arguments_disallows_json_with_both_list_and_repo_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('list', 'repo-info', '--json')
module.parse_arguments({}, 'list', 'repo-info', '--json')
def test_parse_arguments_disallows_json_with_both_repo_info_and_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('repo-info', 'info', '--json')
def test_parse_arguments_disallows_transfer_with_both_archive_and_match_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments(
'transfer',
'--source-repository',
'source.borg',
'--archive',
'foo',
'--match-archives',
'sh:*bar',
)
module.parse_arguments({}, 'repo-info', 'info', '--json')
def test_parse_arguments_disallows_list_with_both_prefix_and_match_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('list', '--prefix', 'foo', '--match-archives', 'sh:*bar')
module.parse_arguments({}, 'list', '--prefix', 'foo', '--match-archives', 'sh:*bar')
def test_parse_arguments_disallows_repo_list_with_both_prefix_and_match_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('repo-list', '--prefix', 'foo', '--match-archives', 'sh:*bar')
module.parse_arguments({}, 'repo-list', '--prefix', 'foo', '--match-archives', 'sh:*bar')
def test_parse_arguments_disallows_info_with_both_archive_and_match_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('info', '--archive', 'foo', '--match-archives', 'sh:*bar')
module.parse_arguments({}, 'info', '--archive', 'foo', '--match-archives', 'sh:*bar')
def test_parse_arguments_disallows_info_with_both_archive_and_prefix():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('info', '--archive', 'foo', '--prefix', 'bar')
module.parse_arguments({}, 'info', '--archive', 'foo', '--prefix', 'bar')
def test_parse_arguments_disallows_info_with_both_prefix_and_match_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('info', '--prefix', 'foo', '--match-archives', 'sh:*bar')
module.parse_arguments({}, 'info', '--prefix', 'foo', '--match-archives', 'sh:*bar')
def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('check', '--only', 'extract')
module.parse_arguments({}, 'check', '--only', 'extract')
def test_parse_arguments_extract_archive_check_does_not_raise_check_subparser_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('extract', '--archive', 'check')
module.parse_arguments({}, 'extract', '--archive', 'check')
def test_parse_arguments_extract_with_check_only_extract_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('extract', '--archive', 'name', 'check', '--only', 'extract')
module.parse_arguments({}, 'extract', '--archive', 'name', 'check', '--only', 'extract')
def test_parse_arguments_bootstrap_without_config_errors():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('bootstrap')
module.parse_arguments({}, 'bootstrap')
def test_parse_arguments_config_with_no_subaction_errors():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('config')
module.parse_arguments({}, 'config')
def test_parse_arguments_config_with_help_shows_config_help(capsys):
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit) as exit:
module.parse_arguments('config', '--help')
module.parse_arguments({}, 'config', '--help')
assert exit.value.code == 0
captured = capsys.readouterr()
@ -582,7 +682,7 @@ def test_parse_arguments_config_with_subaction_but_missing_flags_errors():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit) as exit:
module.parse_arguments('config', 'bootstrap')
module.parse_arguments({}, 'config', 'bootstrap')
assert exit.value.code == 2
@ -591,7 +691,7 @@ def test_parse_arguments_config_with_subaction_and_help_shows_subaction_help(cap
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit) as exit:
module.parse_arguments('config', 'bootstrap', '--help')
module.parse_arguments({}, 'config', 'bootstrap', '--help')
assert exit.value.code == 0
captured = capsys.readouterr()
@ -601,26 +701,30 @@ def test_parse_arguments_config_with_subaction_and_help_shows_subaction_help(cap
def test_parse_arguments_config_with_subaction_and_required_flags_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('config', 'bootstrap', '--repository', 'repo.borg')
module.parse_arguments({}, 'config', 'bootstrap', '--repository', 'repo.borg')
def test_parse_arguments_config_with_subaction_and_global_flags_at_start_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--verbosity', '1', 'config', 'bootstrap', '--repository', 'repo.borg')
module.parse_arguments(
{}, '--verbosity', '1', 'config', 'bootstrap', '--repository', 'repo.borg'
)
def test_parse_arguments_config_with_subaction_and_global_flags_at_end_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('config', 'bootstrap', '--repository', 'repo.borg', '--verbosity', '1')
module.parse_arguments(
{}, 'config', 'bootstrap', '--repository', 'repo.borg', '--verbosity', '1'
)
def test_parse_arguments_config_with_subaction_and_explicit_config_file_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments(
'config', 'bootstrap', '--repository', 'repo.borg', '--config', 'test.yaml'
{}, 'config', 'bootstrap', '--repository', 'repo.borg', '--config', 'test.yaml'
)
@ -628,10 +732,23 @@ def test_parse_arguments_with_borg_action_and_dry_run_raises():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--dry-run', 'borg', 'list')
module.parse_arguments({}, '--dry-run', 'borg', 'list')
def test_parse_arguments_with_borg_action_and_no_dry_run_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('borg', 'list')
module.parse_arguments({}, 'borg', 'list')
def test_parse_arguments_with_argument_from_schema_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments(
{
'type': 'object',
'properties': {'foo': {'type': 'object', 'properties': {'bar': {'type': 'integer'}}}},
},
'--foo.bar',
'3',
)

View file

@ -0,0 +1,34 @@
import pytest
from borgmatic.config import arguments as module
def test_convert_value_type_passes_through_non_string_value():
assert module.convert_value_type([1, 2], 'array') == [1, 2]
def test_convert_value_type_passes_through_string_option_type():
assert module.convert_value_type('foo', 'string') == 'foo'
def test_convert_value_type_parses_array_option_type():
assert module.convert_value_type('[foo, bar]', 'array') == ['foo', 'bar']
def test_convert_value_type_with_array_option_type_and_no_array_raises():
with pytest.raises(ValueError):
module.convert_value_type('{foo, bar}', 'array')
def test_convert_value_type_parses_object_option_type():
assert module.convert_value_type('{foo: bar}', 'object') == {'foo': 'bar'}
def test_convert_value_type_with_invalid_value_raises():
with pytest.raises(ValueError):
module.convert_value_type('{foo, bar', 'object')
def test_convert_value_type_with_unknown_option_type_raises():
with pytest.raises(ValueError):
module.convert_value_type('{foo, bar}', 'thingy')

View file

@ -21,9 +21,9 @@ def test_schema_to_sample_configuration_comments_out_non_default_options():
'type': 'object',
'properties': dict(
[
('field1', {'example': 'Example 1'}),
('field2', {'example': 'Example 2'}),
('source_directories', {'example': 'Example 3'}),
('field1', {'type': 'string', 'example': 'Example 1'}),
('field2', {'type': 'string', 'example': 'Example 2'}),
('source_directories', {'type': 'string', 'example': 'Example 3'}),
]
),
}
@ -47,9 +47,9 @@ def test_schema_to_sample_configuration_comments_out_non_source_config_options()
'type': 'object',
'properties': dict(
[
('field1', {'example': 'Example 1'}),
('field2', {'example': 'Example 2'}),
('field3', {'example': 'Example 3'}),
('field1', {'type': 'string', 'example': 'Example 1'}),
('field2', {'type': 'string', 'example': 'Example 2'}),
('field3', {'type': 'string', 'example': 'Example 3'}),
]
),
}
@ -76,9 +76,9 @@ def test_schema_to_sample_configuration_comments_out_non_default_options_in_sequ
'type': 'object',
'properties': dict(
[
('field1', {'example': 'Example 1'}),
('field2', {'example': 'Example 2'}),
('source_directories', {'example': 'Example 3'}),
('field1', {'type': 'string', 'example': 'Example 1'}),
('field2', {'type': 'string', 'example': 'Example 2'}),
('source_directories', {'type': 'string', 'example': 'Example 3'}),
]
),
},
@ -105,9 +105,9 @@ def test_schema_to_sample_configuration_comments_out_non_source_config_options_i
'type': 'object',
'properties': dict(
[
('field1', {'example': 'Example 1'}),
('field2', {'example': 'Example 2'}),
('field3', {'example': 'Example 3'}),
('field1', {'type': 'string', 'example': 'Example 1'}),
('field2', {'type': 'string', 'example': 'Example 2'}),
('field3', {'type': 'string', 'example': 'Example 3'}),
]
),
},

View file

@ -58,7 +58,9 @@ def test_parse_configuration_transforms_file_into_mapping():
'''
)
config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, config_paths, logs = module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
assert config == {
'source_directories': ['/home', '/etc'],
@ -86,7 +88,9 @@ def test_parse_configuration_passes_through_quoted_punctuation():
'''
)
config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, config_paths, logs = module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
assert config == {
'source_directories': [f'/home/{string.punctuation}'],
@ -119,7 +123,9 @@ def test_parse_configuration_with_schema_lacking_examples_does_not_raise():
''',
)
module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
def test_parse_configuration_inlines_include_inside_deprecated_section():
@ -145,7 +151,9 @@ def test_parse_configuration_inlines_include_inside_deprecated_section():
include_file.name = 'include.yaml'
builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, config_paths, logs = module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
assert config == {
'source_directories': ['/home'],
@ -181,7 +189,9 @@ def test_parse_configuration_merges_include():
include_file.name = 'include.yaml'
builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, config_paths, logs = module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
assert config == {
'source_directories': ['/home'],
@ -196,7 +206,9 @@ def test_parse_configuration_merges_include():
def test_parse_configuration_raises_for_missing_config_file():
with pytest.raises(FileNotFoundError):
module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
def test_parse_configuration_raises_for_missing_schema_file():
@ -208,14 +220,18 @@ def test_parse_configuration_raises_for_missing_schema_file():
builtins.should_receive('open').with_args('/tmp/schema.yaml').and_raise(FileNotFoundError)
with pytest.raises(FileNotFoundError):
module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
def test_parse_configuration_raises_for_syntax_error():
mock_config_and_schema('foo:\nbar')
with pytest.raises(ValueError):
module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
def test_parse_configuration_raises_for_validation_error():
@ -228,7 +244,9 @@ def test_parse_configuration_raises_for_validation_error():
)
with pytest.raises(module.Validation_error):
module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
def test_parse_configuration_applies_overrides():
@ -245,7 +263,10 @@ def test_parse_configuration_applies_overrides():
)
config, config_paths, logs = module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', overrides=['local_path=borg2']
'/tmp/config.yaml',
'/tmp/schema.yaml',
arguments={'global': flexmock()},
overrides=['local_path=borg2'],
)
assert config == {
@ -273,7 +294,9 @@ def test_parse_configuration_applies_normalization_after_environment_variable_in
)
flexmock(os).should_receive('getenv').replace_with(lambda variable_name, default: default)
config, config_paths, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, config_paths, logs = module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', arguments={'global': flexmock()}
)
assert config == {
'source_directories': ['/home'],

View file

@ -105,7 +105,7 @@ def test_get_config_paths_translates_ssh_command_argument_to_config():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('/source')
config = flexmock()
config = {}
flexmock(module).should_receive('make_bootstrap_config').and_return(config)
bootstrap_arguments = flexmock(
repository='repo',
@ -267,11 +267,11 @@ def test_run_bootstrap_does_not_raise():
archive='archive',
destination='dest',
strip_components=1,
progress=False,
user_runtime_directory='/borgmatic',
ssh_command=None,
local_path='borg7',
remote_path='borg8',
progress=None,
)
global_arguments = flexmock(
dry_run=False,
@ -299,7 +299,7 @@ def test_run_bootstrap_does_not_raise():
def test_run_bootstrap_translates_ssh_command_argument_to_config():
config = flexmock()
config = {}
flexmock(module).should_receive('make_bootstrap_config').and_return(config)
flexmock(module).should_receive('get_config_paths').and_return(['/borgmatic/config.yaml'])
bootstrap_arguments = flexmock(
@ -307,11 +307,11 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
archive='archive',
destination='dest',
strip_components=1,
progress=False,
user_runtime_directory='/borgmatic',
ssh_command='ssh -i key',
local_path='borg7',
remote_path='borg8',
progress=None,
)
global_arguments = flexmock(
dry_run=False,
@ -333,13 +333,12 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
'repo',
'archive',
object,
config,
{'progress': False},
object,
object,
extract_to_stdout=False,
destination_path='dest',
strip_components=1,
progress=False,
local_path='borg7',
remote_path='borg8',
).and_return(extract_process).once()

View file

@ -577,7 +577,6 @@ def test_collect_spot_check_source_paths_parses_borg_output():
borgmatic_runtime_directory='/run/borgmatic',
local_path=object,
remote_path=object,
list_files=True,
stream_processes=True,
).and_return((('borg', 'create'), ('repo::archive',), flexmock()))
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -625,7 +624,6 @@ def test_collect_spot_check_source_paths_passes_through_stream_processes_false()
borgmatic_runtime_directory='/run/borgmatic',
local_path=object,
remote_path=object,
list_files=True,
stream_processes=False,
).and_return((('borg', 'create'), ('repo::archive',), flexmock()))
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -673,7 +671,6 @@ def test_collect_spot_check_source_paths_without_working_directory_parses_borg_o
borgmatic_runtime_directory='/run/borgmatic',
local_path=object,
remote_path=object,
list_files=True,
stream_processes=True,
).and_return((('borg', 'create'), ('repo::archive',), flexmock()))
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -721,7 +718,6 @@ def test_collect_spot_check_source_paths_skips_directories():
borgmatic_runtime_directory='/run/borgmatic',
local_path=object,
remote_path=object,
list_files=True,
stream_processes=True,
).and_return((('borg', 'create'), ('repo::archive',), flexmock()))
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -860,14 +856,13 @@ def test_collect_spot_check_source_paths_uses_working_directory():
flexmock(module.borgmatic.borg.create).should_receive('make_base_create_command').with_args(
dry_run=True,
repository_path='repo',
config=object,
config={'working_directory': '/working/dir', 'list_details': True},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version=object,
global_arguments=object,
borgmatic_runtime_directory='/run/borgmatic',
local_path=object,
remote_path=object,
list_files=True,
stream_processes=True,
).and_return((('borg', 'create'), ('repo::archive',), flexmock()))
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -903,6 +898,7 @@ def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
None,
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
@ -943,6 +939,7 @@ def test_compare_spot_check_hashes_returns_relative_paths_having_failing_hashes(
None,
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', 'foo', 'bar'), working_directory=None).and_return(
@ -983,6 +980,7 @@ def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100():
None,
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
@ -1023,6 +1021,7 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
None,
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('/usr/local/bin/xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
@ -1060,6 +1059,7 @@ def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_ma
None,
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
@ -1088,6 +1088,42 @@ def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_ma
) == ('/bar',)
def test_compare_spot_check_hashes_considers_symlink_path_as_not_matching():
flexmock(module.random).should_receive('sample').replace_with(
lambda population, count: population[:count]
)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
None,
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('islink').with_args('/foo').and_return(False)
flexmock(module.os.path).should_receive('islink').with_args('/bar').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', '/foo'), working_directory=None).and_return('hash1 /foo')
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
['hash1 foo', 'hash2 bar']
)
assert module.compare_spot_check_hashes(
repository={'path': 'repo'},
archive='archive',
config={
'checks': [
{
'name': 'spot',
'data_sample_percentage': 50,
},
]
},
local_borg_version=flexmock(),
global_arguments=flexmock(),
local_path=flexmock(),
remote_path=flexmock(),
source_paths=('/foo', '/bar', '/baz', '/quux'),
) == ('/bar',)
def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching():
flexmock(module.random).should_receive('sample').replace_with(
lambda population, count: population[:count]
@ -1097,6 +1133,7 @@ def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching()
)
flexmock(module.os.path).should_receive('exists').with_args('/foo').and_return(True)
flexmock(module.os.path).should_receive('exists').with_args('/bar').and_return(False)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', '/foo'), working_directory=None).and_return('hash1 /foo')
@ -1132,6 +1169,7 @@ def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in
None,
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', '/foo', '/bar'), working_directory=None).and_return(
@ -1178,6 +1216,7 @@ def test_compare_spot_check_hashes_uses_working_directory_to_access_source_paths
)
flexmock(module.os.path).should_receive('exists').with_args('/working/dir/foo').and_return(True)
flexmock(module.os.path).should_receive('exists').with_args('/working/dir/bar').and_return(True)
flexmock(module.os.path).should_receive('islink').and_return(False)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(('xxh64sum', 'foo', 'bar'), working_directory='/working/dir').and_return(

View file

@ -9,7 +9,10 @@ def test_compact_actions_calls_hooks_for_configured_repository():
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once()
compact_arguments = flexmock(
repository=None, progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
repository=None,
progress=flexmock(),
cleanup_commits=flexmock(),
compact_threshold=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
@ -34,7 +37,10 @@ def test_compact_runs_with_selected_repository():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once()
compact_arguments = flexmock(
repository=flexmock(), progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
repository=flexmock(),
progress=flexmock(),
cleanup_commits=flexmock(),
compact_threshold=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
@ -59,7 +65,10 @@ def test_compact_bails_if_repository_does_not_match():
).once().and_return(False)
flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').never()
compact_arguments = flexmock(
repository=flexmock(), progress=flexmock(), cleanup_commits=flexmock(), threshold=flexmock()
repository=flexmock(),
progress=flexmock(),
cleanup_commits=flexmock(),
compact_threshold=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)

View file

@ -443,9 +443,9 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
create_arguments = flexmock(
repository=None,
progress=flexmock(),
stats=flexmock(),
statistics=flexmock(),
json=False,
list_files=flexmock(),
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
@ -484,9 +484,9 @@ def test_run_create_runs_with_selected_repository():
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
stats=flexmock(),
statistics=flexmock(),
json=False,
list_files=flexmock(),
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
@ -516,9 +516,9 @@ def test_run_create_bails_if_repository_does_not_match():
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
stats=flexmock(),
statistics=flexmock(),
json=False,
list_files=flexmock(),
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
@ -538,6 +538,72 @@ def test_run_create_bails_if_repository_does_not_match():
)
def test_run_create_with_both_list_and_json_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=True,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
with pytest.raises(ValueError):
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'list_details': True},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_with_both_list_and_progress_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=False,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
with pytest.raises(ValueError):
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'list_details': True, 'progress': True},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
)
def test_run_create_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
@ -561,9 +627,9 @@ def test_run_create_produces_json():
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
stats=flexmock(),
statistics=flexmock(),
json=True,
list_files=flexmock(),
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)

View file

@ -13,7 +13,7 @@ def test_run_export_tar_does_not_raise():
paths=flexmock(),
destination=flexmock(),
tar_filter=flexmock(),
list_files=flexmock(),
list_details=flexmock(),
strip_components=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
@ -27,3 +27,81 @@ def test_run_export_tar_does_not_raise():
local_path=None,
remote_path=None,
)
def test_run_export_tar_favors_flags_over_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.export_tar).should_receive('export_tar_archive').with_args(
object,
object,
object,
object,
object,
object,
object,
object,
local_path=object,
remote_path=object,
tar_filter=object,
strip_components=object,
).once()
export_tar_arguments = flexmock(
repository=flexmock(),
archive=flexmock(),
paths=flexmock(),
destination=flexmock(),
tar_filter=flexmock(),
list_details=False,
strip_components=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_export_tar(
repository={'path': 'repo'},
config={'list_details': True},
local_borg_version=None,
export_tar_arguments=export_tar_arguments,
global_arguments=global_arguments,
local_path=None,
remote_path=None,
)
def test_run_export_tar_defaults_to_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.export_tar).should_receive('export_tar_archive').with_args(
object,
object,
object,
object,
object,
object,
object,
object,
local_path=object,
remote_path=object,
tar_filter=object,
strip_components=object,
).once()
export_tar_arguments = flexmock(
repository=flexmock(),
archive=flexmock(),
paths=flexmock(),
destination=flexmock(),
tar_filter=flexmock(),
list_details=None,
strip_components=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_export_tar(
repository={'path': 'repo'},
config={'list_details': True},
local_borg_version=None,
export_tar_arguments=export_tar_arguments,
global_arguments=global_arguments,
local_path=None,
remote_path=None,
)

View file

@ -27,3 +27,79 @@ def test_run_extract_calls_hooks():
local_path=None,
remote_path=None,
)
def test_run_extract_favors_flags_over_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').with_args(
object,
object,
object,
object,
object,
object,
object,
local_path=object,
remote_path=object,
destination_path=object,
strip_components=object,
).once()
extract_arguments = flexmock(
paths=flexmock(),
progress=False,
destination=flexmock(),
strip_components=flexmock(),
archive=flexmock(),
repository='repo',
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_extract(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'repositories': ['repo'], 'progress': True},
local_borg_version=None,
extract_arguments=extract_arguments,
global_arguments=global_arguments,
local_path=None,
remote_path=None,
)
def test_run_extract_defaults_to_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').with_args(
object,
object,
object,
object,
object,
object,
object,
local_path=object,
remote_path=object,
destination_path=object,
strip_components=object,
).once()
extract_arguments = flexmock(
paths=flexmock(),
progress=None,
destination=flexmock(),
strip_components=flexmock(),
archive=flexmock(),
repository='repo',
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_extract(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'repositories': ['repo'], 'progress': True},
local_borg_version=None,
extract_arguments=extract_arguments,
global_arguments=global_arguments,
local_path=None,
remote_path=None,
)

View file

@ -7,7 +7,7 @@ def test_run_prune_calls_hooks_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once()
prune_arguments = flexmock(repository=None, stats=flexmock(), list_archives=flexmock())
prune_arguments = flexmock(repository=None, statistics=flexmock(), list_details=flexmock())
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_prune(
@ -29,7 +29,9 @@ def test_run_prune_runs_with_selected_repository():
'repositories_match'
).once().and_return(True)
flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once()
prune_arguments = flexmock(repository=flexmock(), stats=flexmock(), list_archives=flexmock())
prune_arguments = flexmock(
repository=flexmock(), statistics=flexmock(), list_details=flexmock()
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_prune(
@ -51,7 +53,9 @@ def test_run_prune_bails_if_repository_does_not_match():
'repositories_match'
).once().and_return(False)
flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').never()
prune_arguments = flexmock(repository=flexmock(), stats=flexmock(), list_archives=flexmock())
prune_arguments = flexmock(
repository=flexmock(), statistics=flexmock(), list_details=flexmock()
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_prune(

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

@ -1,9 +1,10 @@
import pytest
from flexmock import flexmock
from borgmatic.actions import repo_create as module
def test_run_repo_create_does_not_raise():
def test_run_repo_create_with_encryption_mode_argument_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.repo_create).should_receive('create_repository')
@ -14,7 +15,7 @@ def test_run_repo_create_does_not_raise():
copy_crypt_key=flexmock(),
append_only=flexmock(),
storage_quota=flexmock(),
make_parent_dirs=flexmock(),
make_parent_directories=flexmock(),
)
module.run_repo_create(
@ -28,6 +29,57 @@ def test_run_repo_create_does_not_raise():
)
def test_run_repo_create_with_encryption_mode_option_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.repo_create).should_receive('create_repository')
arguments = flexmock(
encryption_mode=None,
source_repository=flexmock(),
repository=flexmock(),
copy_crypt_key=flexmock(),
append_only=flexmock(),
storage_quota=flexmock(),
make_parent_directories=flexmock(),
)
module.run_repo_create(
repository={'path': 'repo', 'encryption': flexmock()},
config={},
local_borg_version=None,
repo_create_arguments=arguments,
global_arguments=flexmock(dry_run=False),
local_path=None,
remote_path=None,
)
def test_run_repo_create_without_encryption_mode_raises():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository')
arguments = flexmock(
encryption_mode=None,
source_repository=flexmock(),
repository=flexmock(),
copy_crypt_key=flexmock(),
append_only=flexmock(),
storage_quota=flexmock(),
make_parent_directories=flexmock(),
)
with pytest.raises(ValueError):
module.run_repo_create(
repository={'path': 'repo'},
config={},
local_borg_version=None,
repo_create_arguments=arguments,
global_arguments=flexmock(dry_run=False),
local_path=None,
remote_path=None,
)
def test_run_repo_create_bails_if_repository_does_not_match():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(
@ -41,7 +93,7 @@ def test_run_repo_create_bails_if_repository_does_not_match():
copy_crypt_key=flexmock(),
append_only=flexmock(),
storage_quota=flexmock(),
make_parent_dirs=flexmock(),
make_parent_directories=flexmock(),
)
module.run_repo_create(
@ -53,3 +105,91 @@ def test_run_repo_create_bails_if_repository_does_not_match():
local_path=None,
remote_path=None,
)
def test_run_repo_create_favors_flags_over_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository').with_args(
object,
object,
object,
object,
object,
object,
object,
object,
append_only=False,
storage_quota=0,
make_parent_directories=False,
local_path=object,
remote_path=object,
).once()
arguments = flexmock(
encryption_mode=flexmock(),
source_repository=flexmock(),
repository=flexmock(),
copy_crypt_key=flexmock(),
append_only=False,
storage_quota=0,
make_parent_directories=False,
)
module.run_repo_create(
repository={
'path': 'repo',
'append_only': True,
'storage_quota': '10G',
'make_parent_directories': True,
},
config={},
local_borg_version=None,
repo_create_arguments=arguments,
global_arguments=flexmock(dry_run=False),
local_path=None,
remote_path=None,
)
def test_run_repo_create_defaults_to_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository').with_args(
object,
object,
object,
object,
object,
object,
object,
object,
append_only=True,
storage_quota='10G',
make_parent_directories=True,
local_path=object,
remote_path=object,
).once()
arguments = flexmock(
encryption_mode=flexmock(),
source_repository=flexmock(),
repository=flexmock(),
copy_crypt_key=flexmock(),
append_only=None,
storage_quota=None,
make_parent_directories=None,
)
module.run_repo_create(
repository={
'path': 'repo',
'append_only': True,
'storage_quota': '10G',
'make_parent_directories': True,
},
config={},
local_borg_version=None,
repo_create_arguments=arguments,
global_arguments=flexmock(dry_run=False),
local_path=None,
remote_path=None,
)

View file

@ -1,3 +1,4 @@
import pytest
from flexmock import flexmock
from borgmatic.actions import transfer as module
@ -6,7 +7,7 @@ from borgmatic.actions import transfer as module
def test_run_transfer_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.transfer).should_receive('transfer_archives')
transfer_arguments = flexmock()
transfer_arguments = flexmock(archive=None)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_transfer(
@ -18,3 +19,21 @@ def test_run_transfer_does_not_raise():
local_path=None,
remote_path=None,
)
def test_run_transfer_with_archive_and_match_archives_raises():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.transfer).should_receive('transfer_archives')
transfer_arguments = flexmock(archive='foo')
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
with pytest.raises(ValueError):
module.run_transfer(
repository={'path': 'repo'},
config={'match_archives': 'foo*'},
local_borg_version=None,
transfer_arguments=transfer_arguments,
global_arguments=global_arguments,
local_path=None,
remote_path=None,
)

View file

@ -155,22 +155,6 @@ def test_make_archive_filter_flags_with_data_check_and_prefix_includes_match_arc
assert flags == ('--match-archives', 'sh:foo-*')
def test_make_archive_filter_flags_prefers_check_arguments_match_archives_to_config_match_archives():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
'baz-*', None, '1.2.3'
).and_return(('--match-archives', 'sh:baz-*'))
flags = module.make_archive_filter_flags(
'1.2.3',
{'match_archives': 'bar-{now}', 'prefix': ''}, # noqa: FS003
('archives',),
check_arguments=flexmock(match_archives='baz-*'),
)
assert flags == ('--match-archives', 'sh:baz-*')
def test_make_archive_filter_flags_with_archives_check_and_empty_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(
@ -332,7 +316,7 @@ def test_get_repository_id_with_missing_json_keys_raises():
def test_check_archives_with_progress_passes_through_to_borg():
config = {}
config = {'progress': True}
flexmock(module).should_receive('make_check_name_flags').with_args(
{'repository'}, ()
).and_return(())
@ -353,7 +337,7 @@ def test_check_archives_with_progress_passes_through_to_borg():
config=config,
local_borg_version='1.2.3',
check_arguments=flexmock(
progress=True,
progress=None,
repair=None,
only_checks=None,
force=None,

View file

@ -27,7 +27,7 @@ def insert_execute_command_mock(
COMPACT_COMMAND = ('borg', 'compact')
def test_compact_segments_calls_borg_with_parameters():
def test_compact_segments_calls_borg_with_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('repo',), logging.INFO)
@ -40,7 +40,7 @@ def test_compact_segments_calls_borg_with_parameters():
)
def test_compact_segments_with_log_info_calls_borg_with_info_parameter():
def test_compact_segments_with_log_info_calls_borg_with_info_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--info', 'repo'), logging.INFO)
insert_logging_mock(logging.INFO)
@ -54,7 +54,7 @@ def test_compact_segments_with_log_info_calls_borg_with_info_parameter():
)
def test_compact_segments_with_log_debug_calls_borg_with_debug_parameter():
def test_compact_segments_with_log_debug_calls_borg_with_debug_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO)
insert_logging_mock(logging.DEBUG)
@ -110,7 +110,7 @@ def test_compact_segments_with_exit_codes_calls_borg_using_them():
)
def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameters():
def test_compact_segments_with_remote_path_calls_borg_with_remote_path_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
@ -124,21 +124,20 @@ def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameter
)
def test_compact_segments_with_progress_calls_borg_with_progress_parameter():
def test_compact_segments_with_progress_calls_borg_with_progress_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--progress', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False,
repository_path='repo',
config={},
config={'progress': True},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
progress=True,
)
def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_parameter():
def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--cleanup-commits', 'repo'), logging.INFO)
@ -152,21 +151,20 @@ def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_p
)
def test_compact_segments_with_threshold_calls_borg_with_threshold_parameter():
def test_compact_segments_with_threshold_calls_borg_with_threshold_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--threshold', '20', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False,
repository_path='repo',
config={},
config={'compact_threshold': 20},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
threshold=20,
)
def test_compact_segments_with_umask_calls_borg_with_umask_parameters():
def test_compact_segments_with_umask_calls_borg_with_umask_flags():
config = {'umask': '077'}
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
@ -180,7 +178,7 @@ def test_compact_segments_with_umask_calls_borg_with_umask_parameters():
)
def test_compact_segments_with_log_json_calls_borg_with_log_json_parameters():
def test_compact_segments_with_log_json_calls_borg_with_log_json_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--log-json', 'repo'), logging.INFO)
@ -193,7 +191,7 @@ def test_compact_segments_with_log_json_calls_borg_with_log_json_parameters():
)
def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_parameters():
def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_flags():
config = {'lock_wait': 5}
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)

View file

@ -631,12 +631,12 @@ def test_make_base_create_command_includes_list_flags_in_borg_command():
config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'list_details': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/run/borgmatic',
list_files=True,
)
assert create_flags == ('borg', 'create', '--list', '--filter', 'FOO')
@ -962,7 +962,7 @@ def test_make_base_create_command_with_non_existent_directory_and_source_directo
)
def test_create_archive_calls_borg_with_parameters():
def test_create_archive_calls_borg_with_flags():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
@ -1029,7 +1029,7 @@ def test_create_archive_calls_borg_with_environment():
)
def test_create_archive_with_log_info_calls_borg_with_info_parameter():
def test_create_archive_with_log_info_calls_borg_with_info_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
@ -1096,7 +1096,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
)
def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
def test_create_archive_with_log_debug_calls_borg_with_debug_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
@ -1196,7 +1196,6 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/borgmatic/run',
stats=True,
)
@ -1271,7 +1270,7 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
)
def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_output_log_level():
def test_create_archive_with_stats_calls_borg_with_stats_flag_and_answer_output_log_level():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
@ -1296,12 +1295,12 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'statistics': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/borgmatic/run',
stats=True,
)
@ -1334,16 +1333,16 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level():
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'list_details': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/borgmatic/run',
list_files=True,
)
def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_parameter_and_no_list():
def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_flag_and_no_list():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
@ -1369,16 +1368,16 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'progress': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/borgmatic/run',
progress=True,
)
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
def test_create_archive_with_progress_calls_borg_with_progress_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_base_create_command').and_return(
@ -1403,16 +1402,16 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'progress': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/borgmatic/run',
progress=True,
)
def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progress_parameter():
def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progress_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
processes = flexmock()
@ -1459,12 +1458,12 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
'progress': True,
},
patterns=[Pattern('foo'), Pattern('bar')],
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/borgmatic/run',
progress=True,
stream_processes=processes,
)
@ -1532,7 +1531,6 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
global_arguments=flexmock(log_json=False),
borgmatic_runtime_directory='/borgmatic/run',
json=True,
stats=True,
)
assert json_output == '[]'

View file

@ -21,7 +21,7 @@ def test_make_delete_command_includes_log_info():
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
@ -43,7 +43,7 @@ def test_make_delete_command_includes_log_debug():
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
@ -67,7 +67,7 @@ def test_make_delete_command_includes_dry_run():
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=True, log_json=False),
local_path='borg',
remote_path=None,
@ -91,7 +91,7 @@ def test_make_delete_command_includes_remote_path():
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path='borg1',
@ -114,7 +114,7 @@ def test_make_delete_command_includes_umask():
repository={'path': 'repo'},
config={'umask': '077'},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
@ -138,7 +138,7 @@ def test_make_delete_command_includes_log_json():
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=True),
local_path='borg',
remote_path=None,
@ -162,7 +162,7 @@ def test_make_delete_command_includes_lock_wait():
repository={'path': 'repo'},
config={'lock_wait': 5},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
@ -171,7 +171,7 @@ def test_make_delete_command_includes_lock_wait():
assert command == ('borg', 'delete', '--lock-wait', '5', 'repo')
def test_make_delete_command_includes_list():
def test_make_delete_command_with_list_config_calls_borg_with_list_flag():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'list', True
@ -184,9 +184,9 @@ def test_make_delete_command_includes_list():
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
config={'list_details': True},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=True, force=0, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=None, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
@ -207,7 +207,7 @@ def test_make_delete_command_includes_force():
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=1, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=1, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
@ -228,7 +228,7 @@ def test_make_delete_command_includes_force_twice():
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=2, match_archives=None, archive=None),
delete_arguments=flexmock(list_details=False, force=2, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
@ -252,7 +252,7 @@ def test_make_delete_command_includes_archive():
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(
list_archives=False, force=0, match_archives=None, archive='archive'
list_details=False, force=0, match_archives=None, archive='archive'
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
@ -277,7 +277,7 @@ def test_make_delete_command_includes_match_archives():
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(
list_archives=False, force=0, match_archives='sh:foo*', archive='archive'
list_details=False, force=0, match_archives='sh:foo*', archive='archive'
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
@ -287,8 +287,12 @@ def test_make_delete_command_includes_match_archives():
assert command == ('borg', 'delete', '--match-archives', 'sh:foo*', 'repo')
LOGGING_ANSWER = flexmock()
def test_delete_archives_with_archive_calls_borg_delete():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').never()
flexmock(module).should_receive('make_delete_command').and_return(flexmock())
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -308,6 +312,7 @@ def test_delete_archives_with_archive_calls_borg_delete():
def test_delete_archives_with_match_archives_calls_borg_delete():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').never()
flexmock(module).should_receive('make_delete_command').and_return(flexmock())
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -328,6 +333,7 @@ def test_delete_archives_with_match_archives_calls_borg_delete():
@pytest.mark.parametrize('argument_name', module.ARCHIVE_RELATED_ARGUMENT_NAMES[2:])
def test_delete_archives_with_archive_related_argument_calls_borg_delete(argument_name):
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').never()
flexmock(module).should_receive('make_delete_command').and_return(flexmock())
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
@ -347,6 +353,7 @@ def test_delete_archives_with_archive_related_argument_calls_borg_delete(argumen
def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').once()
flexmock(module).should_receive('make_delete_command').never()
@ -359,7 +366,7 @@ def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete
config={},
local_borg_version=flexmock(),
delete_arguments=flexmock(
list_archives=True, force=False, cache_only=False, keep_security_info=False
list_details=True, force=False, cache_only=False, keep_security_info=False
),
global_arguments=flexmock(),
)
@ -367,6 +374,7 @@ def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete
def test_delete_archives_calls_borg_delete_with_working_directory():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.borg.repo_delete).should_receive('delete_repository').never()
command = flexmock()
flexmock(module).should_receive('make_delete_command').and_return(command)

View file

@ -144,7 +144,7 @@ def test_export_tar_archive_calls_borg_with_umask_flags():
)
def test_export_tar_archive_calls_borg_with_log_json_parameter():
def test_export_tar_archive_calls_borg_with_log_json_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@ -186,7 +186,7 @@ def test_export_tar_archive_calls_borg_with_lock_wait_flags():
)
def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
def test_export_tar_archive_with_log_info_calls_borg_with_info_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@ -230,7 +230,7 @@ def test_export_tar_archive_with_log_debug_calls_borg_with_debug_flags():
)
def test_export_tar_archive_calls_borg_with_dry_run_parameter():
def test_export_tar_archive_calls_borg_with_dry_run_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@ -273,7 +273,7 @@ def test_export_tar_archive_calls_borg_with_tar_filter_flags():
)
def test_export_tar_archive_calls_borg_with_list_parameter():
def test_export_tar_archive_calls_borg_with_list_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@ -290,14 +290,13 @@ def test_export_tar_archive_calls_borg_with_list_parameter():
archive='archive',
paths=None,
destination_path='test.tar',
config={},
config={'list_details': True},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
list_files=True,
)
def test_export_tar_archive_calls_borg_with_strip_components_parameter():
def test_export_tar_archive_calls_borg_with_strip_components_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
@ -320,7 +319,7 @@ def test_export_tar_archive_calls_borg_with_strip_components_parameter():
)
def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
def test_export_tar_archive_skips_abspath_for_remote_repository_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(

View file

@ -580,7 +580,7 @@ def test_extract_archive_with_strip_components_all_and_no_paths_raises():
)
def test_extract_archive_calls_borg_with_progress_parameter():
def test_extract_archive_calls_borg_with_progress_flag():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
@ -606,10 +606,9 @@ def test_extract_archive_calls_borg_with_progress_parameter():
repository='repo',
archive='archive',
paths=None,
config={},
config={'progress': True},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
progress=True,
)
@ -622,10 +621,9 @@ def test_extract_archive_with_progress_and_extract_to_stdout_raises():
repository='repo',
archive='archive',
paths=None,
config={},
config={'progress': True},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
progress=True,
extract_to_stdout=True,
)

View file

@ -380,7 +380,7 @@ def test_make_info_command_with_match_archives_flag_passes_through_to_command():
command = module.make_info_command(
repository_path='repo',
config={'archive_name_format': 'bar-{now}'}, # noqa: FS003
config={'archive_name_format': 'bar-{now}', 'match_archives': 'sh:foo-*'}, # noqa: FS003
local_borg_version='2.3.4',
global_arguments=flexmock(log_json=False),
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives='sh:foo-*'),

View file

@ -135,32 +135,6 @@ def test_make_prune_flags_without_prefix_uses_archive_name_format_instead():
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
@ -215,7 +189,7 @@ def test_prune_archives_calls_borg_with_flags():
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -237,7 +211,7 @@ def test_prune_archives_with_log_info_calls_borg_with_info_flag():
insert_execute_command_mock(PRUNE_COMMAND + ('--info', 'repo'), logging.INFO)
insert_logging_mock(logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
repository_path='repo',
config={},
@ -259,7 +233,7 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_flag():
insert_execute_command_mock(PRUNE_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO)
insert_logging_mock(logging.DEBUG)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
repository_path='repo',
config={},
@ -280,7 +254,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_flag():
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
repository_path='repo',
config={},
@ -301,7 +275,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
).and_return(False)
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -328,7 +302,7 @@ def test_prune_archives_with_exit_codes_calls_borg_using_them():
borg_exit_codes=borg_exit_codes,
)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -349,7 +323,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_flags():
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -361,7 +335,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_flags():
)
def test_prune_archives_with_stats_calls_borg_with_stats_flag_and_answer_output_log_level():
def test_prune_archives_with_stats_config_calls_borg_with_stats_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
@ -371,18 +345,18 @@ def test_prune_archives_with_stats_calls_borg_with_stats_flag_and_answer_output_
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), module.borgmatic.logger.ANSWER)
prune_arguments = flexmock(stats=True, list_archives=False)
prune_arguments = flexmock(statistics=None, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
config={},
config={'statistics': True},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
prune_arguments=prune_arguments,
)
def test_prune_archives_with_files_calls_borg_with_list_flag_and_answer_output_log_level():
def test_prune_archives_with_list_config_calls_borg_with_list_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
@ -392,11 +366,11 @@ def test_prune_archives_with_files_calls_borg_with_list_flag_and_answer_output_l
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), module.borgmatic.logger.ANSWER)
prune_arguments = flexmock(stats=False, list_archives=True)
prune_arguments = flexmock(statistics=False, list_details=None)
module.prune_archives(
dry_run=False,
repository_path='repo',
config={},
config={'list_details': True},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
prune_arguments=prune_arguments,
@ -414,7 +388,7 @@ def test_prune_archives_with_umask_calls_borg_with_umask_flags():
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -435,7 +409,7 @@ def test_prune_archives_with_log_json_calls_borg_with_log_json_flag():
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--log-json', 'repo'), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -457,7 +431,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_flags():
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -478,7 +452,7 @@ def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options():
).and_return(False)
insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -546,7 +520,7 @@ def test_prune_archives_with_date_based_matching_calls_borg_with_date_based_flag
)
prune_arguments = flexmock(
stats=False, list_archives=False, newer='1d', newest='1y', older='1m', oldest='1w'
statistics=False, list_details=False, newer='1d', newest='1y', older='1m', oldest='1w'
)
module.prune_archives(
dry_run=False,
@ -570,7 +544,7 @@ def test_prune_archives_calls_borg_with_working_directory():
PRUNE_COMMAND + ('repo',), logging.INFO, working_directory='/working/dir'
)
prune_arguments = flexmock(stats=False, list_archives=False)
prune_arguments = flexmock(statistics=False, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
@ -581,7 +555,7 @@ def test_prune_archives_calls_borg_with_working_directory():
)
def test_prune_archives_calls_borg_with_flags_and_when_feature_available():
def test_prune_archives_calls_borg_without_stats_when_feature_is_not_available():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
@ -591,11 +565,11 @@ def test_prune_archives_calls_borg_with_flags_and_when_feature_available():
).and_return(True)
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.ANSWER)
prune_arguments = flexmock(stats=True, list_archives=False)
prune_arguments = flexmock(statistics=True, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
config={},
config={'statistics': True},
local_borg_version='2.0.0b10',
global_arguments=flexmock(log_json=False),
prune_arguments=prune_arguments,

View file

@ -0,0 +1,812 @@
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 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.feature).should_receive('available').and_return(True)