Compare commits
100 commits
config-com
...
929d343214
| Author | SHA1 | Date | |
|---|---|---|---|
| 929d343214 | |||
| 9ea55d9aa3 | |||
| 3eabda45f2 | |||
| 09212961a4 | |||
| 3f25f3f0ff | |||
| e8542f3613 | |||
| 9407f24674 | |||
| 1c9d25b892 | |||
| 248999c23e | |||
| d0a5aa63be | |||
| d2c3ed26a9 | |||
| bbf6f27715 | |||
| 9301ab13cc | |||
| d5d04b89dc | |||
| 364200c65a | |||
| 4e55547235 | |||
| 96ec66de79 | |||
| 7a0c56878b | |||
| 4065c5d0f7 | |||
| affe7cdc1b | |||
| 017cbae4f9 | |||
| e96db2e100 | |||
| af97b95e2b | |||
| 6a61259f1a | |||
| 5490a83d77 | |||
| 8c907bb5a3 | |||
| f166111b9b | |||
| 10fb02c40a | |||
| cf477bdc1c | |||
| 6f07402407 | |||
| ab01e97a5e | |||
| 92ebc77597 | |||
| 863c954144 | |||
| f7e4d38762 | |||
| de4d7af507 | |||
| 5cea1e1b72 | |||
| fd8c11eb0a | |||
| 92de539bf9 | |||
| 5716e61f8f | |||
| 3e05eeb4de | |||
| 65d1b9235d | |||
| cffb8e88da | |||
| a8362f2618 | |||
| 36265eea7d | |||
| 8101e5c56f | |||
| c7feb16ab5 | |||
| da324ebeb7 | |||
| 59f9d56aae | |||
|
|
dbf2e78f62 | ||
| f6929f8891 | |||
|
|
2716d9d0b0 | ||
| 668f767bfc | |||
| 0182dbd914 | |||
| 1c27e0dadc | |||
|
|
8b3a682edf | ||
| 975a6e4540 | |||
|
|
7020f0530a | ||
| 5bf2f546b9 | |||
| b4c558d013 | |||
| 79bf641668 | |||
| 50beb334dc | |||
|
|
26fd41da92 | ||
| 088da19012 | |||
| 4c6674e0ad | |||
| 486bec698d | |||
| 7a766c717e | |||
| 520fb78a00 | |||
|
|
acc2814f11 | ||
| 996b037946 | |||
|
|
9356924418 | ||
| 79e4e089ee | |||
| d2714cb706 | |||
| 5a0430b9c8 | |||
| 23efbb8df3 | |||
| 9e694e4df9 | |||
| 76f7c53a1c | |||
|
|
203e84b91f | ||
|
|
ea5a2d8a46 | ||
|
|
a8726c408a | ||
|
|
3542673446 | ||
| 532a97623c | |||
| e1fdfe4c2f | |||
| 83a56a3fef | |||
|
|
b60cf2449a | ||
|
|
e7f14bca87 | ||
|
|
4bca7bb198 | ||
|
|
fa3b140590 | ||
|
|
a1d2f7f221 | ||
| 6a470be924 | |||
| d651813601 | |||
| 65b1d8e8b2 | |||
| 16a1121649 | |||
| 423627e67b | |||
| 9f7c71265e | |||
| ba75958a2f | |||
| 57721937a3 | |||
|
|
a750d58a2d | ||
| 2045706faa | |||
|
|
4e2805918d | ||
|
|
6adb0fd44c |
86 changed files with 4436 additions and 734 deletions
26
NEWS
26
NEWS
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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+)')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
)
|
||||
|
|
|
|||
53
borgmatic/actions/recreate.py
Normal file
53
borgmatic/actions/recreate.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ())
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
103
borgmatic/borg/recreate.py
Normal 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'),
|
||||
)
|
||||
|
|
@ -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 ())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
13
borgmatic/commands/completion/flag.py
Normal file
13
borgmatic/commands/completion/flag.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {})
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
55
tests/end-to-end/test_config_flag.py
Normal file
55
tests/end-to-end/test_config_flag.py
Normal 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)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
34
tests/integration/config/test_arguments.py
Normal file
34
tests/integration/config/test_arguments.py
Normal 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')
|
||||
|
|
@ -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'}),
|
||||
]
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
39
tests/unit/actions/test_recreate.py
Normal file
39
tests/unit/actions/test_recreate.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 == '[]'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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-*'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
812
tests/unit/borg/test_recreate.py
Normal file
812
tests/unit/borg/test_recreate.py
Normal 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)
|
||||