mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 18:13:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
731e8d7c37 | ||
|
|
3bcf592d53 | ||
|
|
9bb5791e9f | ||
|
|
8a60fb6398 | ||
|
|
9db2bb2b54 | ||
|
|
87bfd6e97f | ||
|
|
52f9442377 | ||
|
|
af841e0c89 | ||
|
|
ed8320c1bb | ||
|
|
26b3a03721 | ||
|
|
52234c47e6 | ||
|
|
b74b6aa18d | ||
|
|
0380ecd8fb | ||
|
|
32b7d1a0f7 | ||
|
|
da873c09f8 | ||
|
|
d612d398e7 | ||
|
|
1301bec702 | ||
|
|
c7fc68a49a | ||
|
|
c66e29906e | ||
|
|
475389a094 | ||
|
|
9f59bf2827 | ||
|
|
f4e9569297 | ||
|
|
f43c2f7130 | ||
|
|
889b599d55 | ||
|
|
53791d4dc9 | ||
|
|
ee58adb4eb | ||
|
|
d2903640a8 | ||
|
|
ec25a40ddc | ||
|
|
23e451e641 | ||
|
|
43d8c25234 | ||
|
|
e693e42b9e | ||
|
|
1abbfe8ee6 | ||
|
|
8a65b43ae3 | ||
|
|
dbbce4167f | ||
|
|
68c2a4d231 | ||
|
|
12e92acd15 | ||
|
|
1ada61a1d3 | ||
|
|
75dcd41050 | ||
|
|
a6878829a2 | ||
|
|
7a44be38b7 | ||
|
|
92add238d9 | ||
|
|
24e1e38615 | ||
|
|
cb8b298648 | ||
|
|
eaf3d959e6 | ||
|
|
c2449ad811 | ||
|
|
89703334f3 | ||
|
|
ee32cc19e9 | ||
|
|
ceb487a6b9 | ||
|
|
f90d7a99c6 | ||
|
|
012c02d962 | ||
|
|
607ff20971 | ||
|
|
bf2a1c6b25 | ||
|
|
7dbdd103d1 | ||
|
|
06e2b4f2c7 | ||
|
|
6cfa31e2c2 | ||
|
|
3473f034ae | ||
|
|
7dcb4e3c3a | ||
|
|
42c006cf1d | ||
|
|
ea2a253134 | ||
|
|
7ca42a8f4f | ||
|
|
4f25581a12 | ||
|
|
394f33f28a | ||
|
|
f5ff2f9ae3 | ||
|
|
67eb48e643 | ||
|
|
c7c2ef048c | ||
|
|
90d1857494 | ||
|
|
55375cc9e7 | ||
|
|
d532fc0f88 | ||
|
|
6501bd9823 | ||
|
|
477320e36c | ||
|
|
6e611f9b38 | ||
|
|
06ebff878b | ||
|
|
38cc5d4ee4 | ||
|
|
794e7eadab | ||
|
|
de35ed82be |
@@ -1,5 +1,5 @@
|
||||
name: build
|
||||
run-name: ${{ gitea.actor }} is building
|
||||
run-name: ${{ forgejo.actor }} is building
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
docs:
|
||||
needs: [test]
|
||||
runs-on: host
|
||||
if: gitea.event_name == 'push'
|
||||
if: forgejo.event_name == 'push'
|
||||
env:
|
||||
IMAGE_NAME: projects.torsion.org/borgmatic-collective/borgmatic:docs
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
## Hold up, GitHub users
|
||||
|
||||
Thanks for your contribution!
|
||||
|
||||
Unfortunately, we don't use GitHub pull requests to manage code contributions to this repository (and GitHub doesn't have any way to disable pull requests entirely). Instead, please see:
|
||||
|
||||
https://torsion.org/borgmatic/#contributing
|
||||
|
||||
... which provides full instructions on how to submit pull requests. You can even use your GitHub account to login.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# AGENTS.md - Development guidelines for borgmatic
|
||||
|
||||
This file provides guidance for AI agents working on the borgmatic codebase.
|
||||
|
||||
## Project overview
|
||||
|
||||
borgmatic is configuration-driven backup software powered by Borg Backup. It's a
|
||||
Python project using setuptools.
|
||||
|
||||
Please do not use AI agents to modify this codebase. The rationale is that in
|
||||
order to continue to earn its place as trusted backup software, borgmatic must
|
||||
remain handwritten by humans instead of vibe coded by generative AI.
|
||||
|
||||
Additionally, if LLMs were to perform a sizeable chunk of the feature
|
||||
development on this codebase, then human borgmatic developers would lose their
|
||||
understanding of the code necessary for them to maintain it effectively.
|
||||
|
||||
Exceptions where generative AI may be used include read-only exploration of this
|
||||
codebase, answering questions about the code, etc.
|
||||
|
||||
## Architecture notes
|
||||
|
||||
- **main entry point**: `borgmatic.commands.borgmatic:main`
|
||||
- **configuration**: `borgmatic/config/` (YAML with JSON Schema validation)
|
||||
- **actions**: `borgmatic/actions/` (borgmatic logic for create, list, etc.)
|
||||
- **Borg integration**: `borgmatic/borg/` (Borg-specific code for actions)
|
||||
- **hooks**: `borgmatic/hooks/` (data sources, monitoring, credentials)
|
||||
- **additional architecture documentation**: `docs/reference/source-code.md`
|
||||
@@ -1,3 +1,52 @@
|
||||
2.1.4
|
||||
* #1266: Add a stand-alone borgmatic Linux binary to the release downloads to serve as another way
|
||||
to install borgmatic. Consider this binary a beta feature.
|
||||
* #1286: Fix a regression in which running borgmatic with no arguments and no configuration files
|
||||
doesn't error as expected.
|
||||
* #1257: Fix for the Loki monitoring hook not respecting the monitoring verbosity.
|
||||
* #1264: Improve performance of the "info" and "repo-list" actions by eliminating a second "borg
|
||||
info" call that supports a "no matching archives" warning. The warning still occurs; it's just
|
||||
done now without the extra "borg info" call.
|
||||
* #1268: Fix the "spot" check, "extract" check, and all uses of the archive name "latest" to
|
||||
respect the "match_archives" and "archives_name_format" options. This means that borgmatic now
|
||||
uses the "latest" archive that also matches those options instead of the latest archive overall.
|
||||
* When Borg exits with a warning exit code, show a description of it, so you don't have to lookup
|
||||
the code.
|
||||
* Split out borgmatic installation documentation to its own page, so it's easier to find.
|
||||
* Switch the default borgmatic installation method from pipx to uv, as uv is faster and used for
|
||||
borgmatic development. If you'd like to switch, see the documentation for more information:
|
||||
https://torsion.org/borgmatic/how-to/upgrade/
|
||||
* Move the project tracker from Gitea to Forgejo.
|
||||
* Fix a regression in which borgmatic didn't show an error message when run with no configuration.
|
||||
* Fix a traceback in the "spot" check with Borg 2.
|
||||
|
||||
2.1.3
|
||||
* #1175: Add a "files_changed" option for customizing Borg's file modification detection.
|
||||
* #1175: Add a "msgpack_version_check" option to prevent Borg from validating msgpack's version.
|
||||
* #1218: Add a "config show" action to display computed borgmatic configuration as YAML or JSON,
|
||||
handy for fetching borgmatic configuration from external scripts. See the documentation for more
|
||||
information:
|
||||
https://torsion.org/borgmatic/reference/command-line/actions/config-show/
|
||||
* #1228: Adjust the "spot" check so error output includes more information about what failed.
|
||||
* #1236: Fix the "spot" check to skip hard links, as Borg doesn't produces hashes for them.
|
||||
* #1243: Add a "diff" action for viewing the difference between the contents of two archives.
|
||||
* #1248: Go back to treating Borg "file not found" warnings (exit code 107) as
|
||||
warnings instead of errors. Otherwise, borgmatic can error on files that a user intentionally
|
||||
deletes while a backup is running. You can still override this behavior with the
|
||||
"borg_exit_codes" option. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/how-to/customize-warnings-and-errors/
|
||||
* #1248: Un-deprecate the "source_directories_must_exist" option and default it to true, to
|
||||
compensate for Borg "file not found" warnings no longer being treated as errors.
|
||||
* #1269: Fix the ZFS hook to support datasets with a "canmount" property of "noauto".
|
||||
* #1270: Follow symlinks when backing up borgmatic configuration files to support the "bootstrap"
|
||||
action.
|
||||
* #1274: Add an optional override for the documentation development listen port and use Podman
|
||||
Compose if present.
|
||||
* #1281: Fix a unicode error when backing up a non-UTF-8 source filename with a
|
||||
corresponding system locale.
|
||||
* Add a policy about the use of generative AI in the borgmatic codebase:
|
||||
https://torsion.org/borgmatic/how-to/develop-on-borgmatic/#use-of-generative-ai
|
||||
|
||||
2.1.2
|
||||
* #1231: If a source file is deleted during a "spot" check, consider the file as non-matching
|
||||
and move on instead of immediately failing the entire check.
|
||||
|
||||
@@ -94,10 +94,11 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||
|
||||
## Getting started
|
||||
|
||||
Your first step is to [install and configure
|
||||
borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/).
|
||||
Your first steps are to
|
||||
[install](https://torsion.org/borgmatic/how-to/install-borgmatic/) and
|
||||
[configure borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/).
|
||||
|
||||
For additional documentation, check out the links above (left panel on wide screens)
|
||||
For additional documentation, check out the links on the top/left
|
||||
for <a href="https://torsion.org/borgmatic/#documentation">borgmatic how-to and
|
||||
reference guides</a>.
|
||||
|
||||
@@ -183,4 +184,4 @@ Thanks to all borgmatic contributors! There are multiple ways to contribute to
|
||||
this project, so the following includes those who have fixed bugs, contributed
|
||||
features, *or* filed tickets.
|
||||
|
||||
{% include borgmatic/contributors.html %}
|
||||
{% include borgmatic/contributors.html %}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
.
|
||||
apprise==1.9.9
|
||||
attrs==26.1.0
|
||||
certifi==2026.2.25
|
||||
charset-normalizer==3.4.6
|
||||
click==8.3.1
|
||||
idna==3.11
|
||||
jsonschema==4.26.0
|
||||
jsonschema-specifications==2025.9.1
|
||||
markdown==3.10.2
|
||||
oauthlib==3.3.1
|
||||
packaging==26.0
|
||||
pyyaml==6.0.3
|
||||
referencing==0.37.0
|
||||
requests==2.33.1
|
||||
requests-oauthlib==2.0.0
|
||||
rpds-py==0.30.0
|
||||
ruamel-yaml==0.19.1
|
||||
urllib3==2.6.3
|
||||
+25
-29
@@ -525,6 +525,7 @@ def compare_spot_check_hashes(
|
||||
source_sample_paths_subset = tuple(
|
||||
itertools.islice(source_sample_paths_iterator, SAMPLE_PATHS_SUBSET_COUNT),
|
||||
)
|
||||
|
||||
if not source_sample_paths_subset:
|
||||
break
|
||||
|
||||
@@ -597,23 +598,27 @@ def compare_spot_check_hashes(
|
||||
source_hashes[hash_path] = ''
|
||||
|
||||
# Get the hash for each file in the archive.
|
||||
archive_hashes.update(
|
||||
**{
|
||||
entry['path']: entry['xxh64']
|
||||
for entry in borgmatic.borg.list.capture_archive_listing(
|
||||
repository['path'],
|
||||
archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
list_paths=source_sample_paths_subset,
|
||||
path_format='{xxh64}{path}',
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
if entry
|
||||
},
|
||||
)
|
||||
for entry in borgmatic.borg.list.capture_archive_listing(
|
||||
repository['path'],
|
||||
archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
list_paths=source_sample_paths_subset,
|
||||
path_format='{xxh64}{path}{linktarget}{target}',
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
):
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
# Borg can't get hashes of stored hard links. So if this is a hard link path (and not
|
||||
# deemed as the "original" by Borg), then skip hashing of it.
|
||||
if entry.get('linktarget') or entry.get('target'):
|
||||
source_hashes.pop(os.path.join('/', entry['path']), None)
|
||||
continue
|
||||
|
||||
archive_hashes[entry['path']] = entry['xxh64']
|
||||
|
||||
# Compare the source hashes with the archive hashes to see how many match.
|
||||
failing_paths = []
|
||||
@@ -712,7 +717,7 @@ def spot_check(
|
||||
)
|
||||
logger.debug(f'Paths in latest archive but not source paths: {truncated_archive_paths}')
|
||||
raise ValueError(
|
||||
'Spot check failed: There are no source paths to compare against the archive',
|
||||
'Spot check failed; there are no source paths to compare against the archive',
|
||||
)
|
||||
|
||||
# Calculate the percentage delta between the source paths count and the archive paths count, and
|
||||
@@ -726,19 +731,13 @@ def spot_check(
|
||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||
placeholder=' ...',
|
||||
)
|
||||
logger.debug(
|
||||
f'Paths in source paths but not latest archive: {truncated_exclusive_source_paths}',
|
||||
)
|
||||
truncated_exclusive_archive_paths = textwrap.shorten(
|
||||
', '.join(set(archive_paths) - rootless_source_paths) or 'none',
|
||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||
placeholder=' ...',
|
||||
)
|
||||
logger.debug(
|
||||
f'Paths in latest archive but not source paths: {truncated_exclusive_archive_paths}',
|
||||
)
|
||||
raise ValueError(
|
||||
f'Spot check failed: {count_delta_percentage:.2f}% file count delta between source paths and latest archive (tolerance is {spot_check_config["count_tolerance_percentage"]}%)',
|
||||
f'Spot check failed\n{count_delta_percentage:.2f}% file count delta between source paths ({len(source_paths)} total) and latest archive ({len(archive_paths)} total); tolerance is {spot_check_config["count_tolerance_percentage"]}%\nOnly in source paths: {truncated_exclusive_source_paths}\nOnly in latest archive: {truncated_exclusive_archive_paths}',
|
||||
)
|
||||
|
||||
failing_paths = compare_spot_check_hashes(
|
||||
@@ -763,11 +762,8 @@ def spot_check(
|
||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||
placeholder=' ...',
|
||||
)
|
||||
logger.debug(
|
||||
f'Source paths with data not matching the latest archive: {truncated_failing_paths}',
|
||||
)
|
||||
raise ValueError(
|
||||
f'Spot check failed: {failing_percentage:.2f}% of source paths with data not matching the latest archive (tolerance is {data_tolerance_percentage}%)',
|
||||
f'Spot check failed\n{failing_percentage:.2f}% of source paths ({len(failing_paths)} out of {len(source_paths)} checked) with data not matching the latest archive; tolerance is {data_tolerance_percentage}%\nSource paths with non-matching data: {truncated_failing_paths}',
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -120,7 +120,7 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
|
||||
logger.info(f"Bootstrapping configuration paths: {', '.join(manifest_config_paths)}")
|
||||
|
||||
borgmatic.borg.extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import borgmatic.config.generate
|
||||
import borgmatic.logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_show(show_arguments, configs):
|
||||
'''
|
||||
Given the show arguments as an argparse.Namespace instance and a dict of configuration filename
|
||||
to corresponding parsed configuration, run the "show" action. That consists of rendering and
|
||||
logging the computed configuration as YAML, separating the configuration for each file with
|
||||
"---".
|
||||
|
||||
If show_arguments.option is set, limit the results to the value of that single option. If
|
||||
show_arguments.json is True, render the results as JSON with one array element per configuration
|
||||
file.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
if show_arguments.json:
|
||||
sys.stdout.write(
|
||||
json.dumps(
|
||||
[
|
||||
config.get(show_arguments.option) if show_arguments.option else config
|
||||
for config in configs.values()
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
for config in configs.values():
|
||||
if len(configs) > 1:
|
||||
logger.answer('---')
|
||||
|
||||
logger.answer(
|
||||
borgmatic.config.generate.render_configuration(
|
||||
config.get(show_arguments.option) if show_arguments.option else config
|
||||
).rstrip()
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
import logging
|
||||
|
||||
import borgmatic.actions.pattern
|
||||
import borgmatic.borg.diff
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_diff(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
diff_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Run the "diff" action for the given repository.
|
||||
'''
|
||||
|
||||
# Only process patterns if only_patterns flag is set
|
||||
if diff_arguments.only_patterns:
|
||||
processed_patterns = borgmatic.actions.pattern.process_patterns(
|
||||
(*borgmatic.actions.pattern.collect_patterns(config),),
|
||||
config,
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
)
|
||||
else:
|
||||
processed_patterns = None
|
||||
|
||||
archive = borgmatic.borg.repo_list.resolve_archive_name(
|
||||
repository['path'],
|
||||
diff_arguments.archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
second_archive = borgmatic.borg.repo_list.resolve_archive_name(
|
||||
repository['path'],
|
||||
diff_arguments.second_archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
borgmatic.borg.diff.diff(
|
||||
repository['path'],
|
||||
archive,
|
||||
second_archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
diff_arguments,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
patterns=processed_patterns,
|
||||
)
|
||||
@@ -68,7 +68,7 @@ def collect_patterns(config):
|
||||
+ tuple(
|
||||
parse_pattern(pattern_line.strip())
|
||||
for filename in config.get('patterns_from', ())
|
||||
for pattern_line in open(filename, encoding='utf-8').readlines()
|
||||
for pattern_line in open(filename, encoding='utf-8')
|
||||
if not pattern_line.lstrip().startswith('#')
|
||||
if pattern_line.strip()
|
||||
)
|
||||
@@ -78,7 +78,7 @@ def collect_patterns(config):
|
||||
borgmatic.borg.pattern.Pattern_style.FNMATCH,
|
||||
)
|
||||
for filename in config.get('exclude_from', ())
|
||||
for exclude_line in open(filename, encoding='utf-8').readlines()
|
||||
for exclude_line in open(filename, encoding='utf-8')
|
||||
if not exclude_line.lstrip().startswith('#')
|
||||
if exclude_line.strip()
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import collections
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
@@ -317,7 +318,7 @@ def collect_dumps_from_archive(
|
||||
extract_to_stdout=True,
|
||||
)
|
||||
.stdout.read()
|
||||
.decode(),
|
||||
.decode(locale.getpreferredencoding()),
|
||||
dumps_metadata_entry['path'],
|
||||
):
|
||||
dumps_from_archive[dump] = None
|
||||
|
||||
@@ -179,10 +179,7 @@ def make_base_create_command( # noqa: PLR0912
|
||||
return a tuple of (base Borg create command flags, Borg create command positional arguments,
|
||||
open pattern file handle).
|
||||
'''
|
||||
if config.get('source_directories_must_exist', False):
|
||||
logger.warning(
|
||||
'The "source_directories_must_exist" option is deprecated and will be removed from a future release; borgmatic now errors on missing files as Borg runs'
|
||||
)
|
||||
if config.get('source_directories_must_exist', True):
|
||||
borgmatic.borg.pattern.check_all_root_patterns_exist(patterns)
|
||||
|
||||
patterns_file = borgmatic.borg.pattern.write_patterns_file(
|
||||
@@ -198,6 +195,7 @@ def make_base_create_command( # noqa: PLR0912
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run)
|
||||
files_changed = config.get('files_changed')
|
||||
files_cache = config.get('files_cache')
|
||||
archive_name_format = (
|
||||
config.get('archive_name_format', flags.get_default_archive_name_format(local_borg_version))
|
||||
@@ -248,6 +246,7 @@ def make_base_create_command( # noqa: PLR0912
|
||||
+ (('--nobirthtime',) if config.get('birthtime') is False else ())
|
||||
+ (('--read-special',) if config.get('read_special') or stream_processes else ())
|
||||
+ noflags_flags
|
||||
+ (('--files-changed', files_changed) if files_changed else ())
|
||||
+ (('--files-cache', files_cache) if files_cache else ())
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
@@ -392,7 +391,7 @@ def create_archive(
|
||||
borg_exit_codes = config.get('borg_exit_codes')
|
||||
|
||||
if stream_processes:
|
||||
return '\n'.join(
|
||||
output = '\n'.join(
|
||||
execute_command_with_processes(
|
||||
create_flags + create_positional_arguments,
|
||||
stream_processes,
|
||||
@@ -404,9 +403,10 @@ def create_archive(
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
return output if json else None
|
||||
|
||||
if output_log_level is None:
|
||||
return '\n'.join(
|
||||
output = '\n'.join(
|
||||
execute_command_and_capture_output(
|
||||
create_flags + create_positional_arguments,
|
||||
working_directory=working_directory,
|
||||
@@ -415,6 +415,7 @@ def create_archive(
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
return output if json else None
|
||||
|
||||
execute_command(
|
||||
create_flags + create_positional_arguments,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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.pattern import write_patterns_file
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def diff(
|
||||
repository,
|
||||
archive,
|
||||
second_archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
diff_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path=None,
|
||||
patterns=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, two archive names, a configuration dict, the local Borg
|
||||
version string, an argparse.Namespace of diff arguments, an argparse.Namespace of global
|
||||
arguments, optional local and remote Borg paths, executes the diff command with the given
|
||||
arguments.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
exclude_flags = flags.make_exclude_flags(config)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('diff', '')
|
||||
|
||||
if diff_arguments.only_patterns:
|
||||
# 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),
|
||||
)
|
||||
else:
|
||||
patterns_file = None
|
||||
|
||||
if borgmatic.borg.feature.available(
|
||||
borgmatic.borg.feature.Feature.NUMERIC_IDS, local_borg_version
|
||||
):
|
||||
numeric_ids_flags = ('--numeric-ids',) if config.get('numeric_ids') else ()
|
||||
else:
|
||||
numeric_ids_flags = ('--numeric-owner',) if config.get('numeric_ids') else ()
|
||||
|
||||
diff_command = (
|
||||
(local_path, 'diff')
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ ('--log-json',)
|
||||
+ (('--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 and diff_arguments.only_patterns
|
||||
else ()
|
||||
)
|
||||
+ exclude_flags
|
||||
+ numeric_ids_flags
|
||||
+ (('--same-chunker-params',) if diff_arguments.same_chunker_params else ())
|
||||
+ (('--sort-by', ','.join(diff_arguments.sort_keys)) if diff_arguments.sort_keys else ())
|
||||
+ (('--content-only',) if diff_arguments.content_only else ())
|
||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||
+ (
|
||||
(*flags.make_repository_flags(repository, local_borg_version), archive)
|
||||
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)
|
||||
)
|
||||
+ (second_archive,)
|
||||
)
|
||||
|
||||
borgmatic.execute.execute_command(
|
||||
full_command=diff_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
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'),
|
||||
)
|
||||
@@ -25,6 +25,7 @@ DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
|
||||
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
|
||||
'use_chunks_archive': 'BORG_USE_CHUNKS_ARCHIVE',
|
||||
'msgpack_version_check': 'BORG_MSGPACK_VERSION_CHECK',
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +91,8 @@ def make_environment(config):
|
||||
) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items():
|
||||
if os.environ.get(environment_variable_name) is None:
|
||||
value = config.get(option_name)
|
||||
environment[environment_variable_name] = 'YES' if value else 'NO'
|
||||
if value is not None:
|
||||
environment[environment_variable_name] = 'YES' if value else 'NO'
|
||||
|
||||
for (
|
||||
option_name,
|
||||
|
||||
+10
-20
@@ -1,5 +1,4 @@
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -135,27 +134,18 @@ def make_match_archives_flags( # noqa: PLR0911
|
||||
return ('--glob-archives', f'{derived_match_archives}')
|
||||
|
||||
|
||||
def warn_for_aggressive_archive_flags(json_command, json_output):
|
||||
def warn_for_aggressive_archive_flags(command, output_lines):
|
||||
'''
|
||||
Given a JSON archives command and the resulting JSON string output from running it, parse the
|
||||
JSON and warn if the command used an archive flag but the output indicates zero archives were
|
||||
found.
|
||||
Given an archives command and the resulting output lines from running it, warn if the command
|
||||
used an archive flag but the output indicates zero archives were found.
|
||||
'''
|
||||
archive_flags_used = {'--glob-archives', '--match-archives'}.intersection(set(json_command))
|
||||
|
||||
if not archive_flags_used:
|
||||
return
|
||||
|
||||
try:
|
||||
if len(json.loads(json_output)['archives']) == 0:
|
||||
logger.warning('An archive filter was applied, but no matching archives were found.')
|
||||
logger.warning(
|
||||
'Try adding --match-archives "*" or adjusting archive_name_format/match_archives in configuration.',
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
logger.debug(f'Cannot parse JSON output from archive command: {error}')
|
||||
except (TypeError, KeyError):
|
||||
logger.debug('Cannot parse JSON output from archive command: No "archives" key found')
|
||||
if {'--glob-archives', '--match-archives'}.intersection(set(command)) and len(
|
||||
tuple(line for line in output_lines if not line.startswith('terminating with '))
|
||||
) == 0:
|
||||
logger.warning('An archive filter was applied, but no matching archives were found.')
|
||||
logger.warning(
|
||||
'Try adding --match-archives "*" or adjusting archive_name_format/match_archives in configuration.',
|
||||
)
|
||||
|
||||
|
||||
def omit_flag(arguments, flag):
|
||||
|
||||
+16
-16
@@ -5,7 +5,7 @@ import shlex
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||
from borgmatic.execute import execute_command_and_capture_output
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -103,9 +103,21 @@ def display_archives_info(
|
||||
borg_exit_codes = config.get('borg_exit_codes')
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
|
||||
json_info = '\n'.join(
|
||||
if info_arguments.json:
|
||||
return '\n'.join(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
|
||||
output_lines = tuple(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
@@ -113,18 +125,6 @@ def display_archives_info(
|
||||
)
|
||||
)
|
||||
|
||||
if info_arguments.json:
|
||||
return json_info
|
||||
|
||||
flags.warn_for_aggressive_archive_flags(json_command, json_info)
|
||||
|
||||
execute_command(
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
flags.warn_for_aggressive_archive_flags(main_command, output_lines)
|
||||
|
||||
return None
|
||||
|
||||
+23
-16
@@ -6,7 +6,7 @@ import shlex
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||
from borgmatic.execute import execute_command_and_capture_output
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -79,6 +79,13 @@ def get_latest_archive(
|
||||
*flags.make_flags('umask', config.get('umask')),
|
||||
*('--log-json',),
|
||||
*flags.make_flags('lock-wait', config.get('lock_wait')),
|
||||
*(
|
||||
flags.make_match_archives_flags(
|
||||
config.get('match_archives'),
|
||||
config.get('archive_name_format'),
|
||||
local_borg_version,
|
||||
)
|
||||
),
|
||||
*(
|
||||
flags.make_flags('consider-checkpoints', consider_checkpoints)
|
||||
if not feature.available(feature.Feature.REPO_LIST, local_borg_version)
|
||||
@@ -219,9 +226,21 @@ def list_repository(
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
borg_exit_codes = config.get('borg_exit_codes')
|
||||
|
||||
json_listing = '\n'.join(
|
||||
if repo_list_arguments.json:
|
||||
return '\n'.join(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
|
||||
output_lines = tuple(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
@@ -229,18 +248,6 @@ def list_repository(
|
||||
)
|
||||
)
|
||||
|
||||
if repo_list_arguments.json:
|
||||
return json_listing
|
||||
|
||||
flags.warn_for_aggressive_archive_flags(json_command, json_listing)
|
||||
|
||||
execute_command(
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
flags.warn_for_aggressive_archive_flags(main_command, output_lines)
|
||||
|
||||
return None
|
||||
|
||||
@@ -33,6 +33,7 @@ ACTION_ALIASES = {
|
||||
'key': [],
|
||||
'borg': [],
|
||||
'recreate': [],
|
||||
'diff': [],
|
||||
}
|
||||
|
||||
|
||||
@@ -1261,6 +1262,29 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
||||
help='Show this help message and exit',
|
||||
)
|
||||
|
||||
config_show_parser = config_parsers.add_parser(
|
||||
'show',
|
||||
help='Show the computed configuration for each file specified with --config (see borgmatic --help)',
|
||||
description='Show the computed configuration for each file specified with --config (see borgmatic --help)',
|
||||
add_help=False,
|
||||
)
|
||||
config_show_group = config_show_parser.add_argument_group('config show arguments')
|
||||
config_show_group.add_argument(
|
||||
'--option',
|
||||
help='Show the value of a single named configuration option instead of the entire configuration',
|
||||
)
|
||||
config_show_group.add_argument(
|
||||
'--json',
|
||||
action='store_true',
|
||||
help='Show the configuration as JSON with one array element per configuration file',
|
||||
)
|
||||
config_show_group.add_argument(
|
||||
'-h',
|
||||
'--help',
|
||||
action='help',
|
||||
help='Show this help message and exit',
|
||||
)
|
||||
|
||||
export_tar_parser = action_parsers.add_parser(
|
||||
'export-tar',
|
||||
aliases=ACTION_ALIASES['export-tar'],
|
||||
@@ -1970,6 +1994,47 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
||||
help='Show this help message and exit',
|
||||
)
|
||||
|
||||
diff_parser = action_parsers.add_parser(
|
||||
'diff',
|
||||
aliases=ACTION_ALIASES['diff'],
|
||||
help='This command finds differences (file contents, user/group/mode) between archives',
|
||||
description='This command finds differences (file contents, user/group/mode) between archives',
|
||||
)
|
||||
diff_group = diff_parser.add_argument_group('diff arguments')
|
||||
diff_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository containing archive to diff, defaults to the configured repository if there is only one, quoted globs supported',
|
||||
)
|
||||
diff_group.add_argument(
|
||||
'--archive',
|
||||
help='Archive name, hash, or series to diff',
|
||||
required=True,
|
||||
)
|
||||
diff_group.add_argument(
|
||||
'--second-archive',
|
||||
help='Second archive name, hash, or series to diff',
|
||||
required=True,
|
||||
)
|
||||
diff_group.add_argument(
|
||||
'--same-chunker-params', action='store_true', help='Override check of chunker parameters'
|
||||
)
|
||||
diff_group.add_argument(
|
||||
'--sort-by',
|
||||
metavar='KEY',
|
||||
dest='sort_keys',
|
||||
action='append',
|
||||
help='Advanced sorting: specify field(s) to sort by. Prefix with > for descending or < for ascending (default)',
|
||||
)
|
||||
diff_group.add_argument(
|
||||
'--content-only',
|
||||
action='store_true',
|
||||
help='Only compare differences in content (exclude metadata differences)',
|
||||
)
|
||||
diff_group.add_argument(
|
||||
'--only-patterns',
|
||||
action='store_true',
|
||||
help='Run the diff according to borgmatic configured patterns (ie do not diff entire archives)',
|
||||
)
|
||||
borg_parser = action_parsers.add_parser(
|
||||
'borg',
|
||||
aliases=ACTION_ALIASES['borg'],
|
||||
|
||||
@@ -17,9 +17,11 @@ import borgmatic.actions.check
|
||||
import borgmatic.actions.compact
|
||||
import borgmatic.actions.config.bootstrap
|
||||
import borgmatic.actions.config.generate
|
||||
import borgmatic.actions.config.show
|
||||
import borgmatic.actions.config.validate
|
||||
import borgmatic.actions.create
|
||||
import borgmatic.actions.delete
|
||||
import borgmatic.actions.diff
|
||||
import borgmatic.actions.export_key
|
||||
import borgmatic.actions.export_tar
|
||||
import borgmatic.actions.extract
|
||||
@@ -622,6 +624,16 @@ def run_actions( # noqa: PLR0912, PLR0915
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'diff':
|
||||
borgmatic.actions.diff.run_diff(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'borg':
|
||||
borgmatic.actions.borg.run_borg(
|
||||
repository,
|
||||
@@ -810,18 +822,18 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
||||
'''
|
||||
add_custom_log_levels()
|
||||
|
||||
if 'bootstrap' in arguments:
|
||||
try:
|
||||
# No configuration file is needed for bootstrap.
|
||||
local_borg_version = borg_version.local_borg_version(
|
||||
{},
|
||||
arguments['bootstrap'].local_path,
|
||||
)
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
yield from log_error_records('Error getting local Borg version', error)
|
||||
return
|
||||
try:
|
||||
if 'bootstrap' in arguments:
|
||||
try:
|
||||
# No configuration file is needed for bootstrap.
|
||||
local_borg_version = borg_version.local_borg_version(
|
||||
{},
|
||||
arguments['bootstrap'].local_path,
|
||||
)
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
yield from log_error_records('Error getting local Borg version', error)
|
||||
return
|
||||
|
||||
try:
|
||||
borgmatic.actions.config.bootstrap.run_bootstrap(
|
||||
arguments['bootstrap'],
|
||||
arguments['global'],
|
||||
@@ -835,17 +847,10 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
||||
name=logger.name,
|
||||
),
|
||||
)
|
||||
except (
|
||||
CalledProcessError,
|
||||
ValueError,
|
||||
OSError,
|
||||
) as error:
|
||||
yield from log_error_records(error)
|
||||
|
||||
return
|
||||
return
|
||||
|
||||
if 'generate' in arguments:
|
||||
try:
|
||||
if 'generate' in arguments:
|
||||
borgmatic.actions.config.generate.run_generate(
|
||||
arguments['generate'],
|
||||
arguments['global'],
|
||||
@@ -858,29 +863,22 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
||||
name=logger.name,
|
||||
),
|
||||
)
|
||||
except (
|
||||
CalledProcessError,
|
||||
ValueError,
|
||||
OSError,
|
||||
) as error:
|
||||
yield from log_error_records(error)
|
||||
|
||||
return
|
||||
|
||||
if 'validate' in arguments:
|
||||
if configuration_parse_errors:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.CRITICAL,
|
||||
levelname='CRITICAL',
|
||||
msg='Configuration validation failed',
|
||||
name=logger.name,
|
||||
),
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
if 'validate' in arguments:
|
||||
if configuration_parse_errors:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.CRITICAL,
|
||||
levelname='CRITICAL',
|
||||
msg='Configuration validation failed',
|
||||
name=logger.name,
|
||||
),
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
borgmatic.actions.config.validate.run_validate(arguments['validate'], configs)
|
||||
|
||||
yield logging.makeLogRecord(
|
||||
@@ -891,14 +889,20 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
||||
name=logger.name,
|
||||
),
|
||||
)
|
||||
except (
|
||||
CalledProcessError,
|
||||
ValueError,
|
||||
OSError,
|
||||
) as error:
|
||||
yield from log_error_records(error)
|
||||
|
||||
return
|
||||
return
|
||||
|
||||
if 'show' in arguments:
|
||||
borgmatic.actions.config.show.run_show(arguments['show'], configs)
|
||||
|
||||
return
|
||||
|
||||
except (
|
||||
CalledProcessError,
|
||||
ValueError,
|
||||
OSError,
|
||||
) as error:
|
||||
yield from log_error_records(error)
|
||||
|
||||
|
||||
def collect_configuration_run_summary_logs(configs, config_paths, arguments, log_file_path): # noqa: PLR0912
|
||||
@@ -1040,16 +1044,17 @@ def exit_with_help_link(): # pragma: no cover
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_and_show_help_on_no_args(configs):
|
||||
def check_and_show_help_on_no_args(configs, schema):
|
||||
'''
|
||||
Given a dict of configuration filename to corresponding parsed configuration, 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.
|
||||
Given a dict of configuration filename to corresponding parsed configuration and the
|
||||
configuration schema as a dict, check if the borgmatic command was run without any arguments. If
|
||||
the configuration option "default_actions" is set to False, then show the help message an exit.
|
||||
'''
|
||||
if len(sys.argv) == 1: # No arguments provided
|
||||
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')
|
||||
|
||||
if configs and not default_actions:
|
||||
parse_arguments(schema, '--help')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -1118,7 +1123,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
||||
exit_with_help_link()
|
||||
except SystemExit as error:
|
||||
if error.code == 0:
|
||||
raise error
|
||||
raise
|
||||
|
||||
configure_logging(logging.CRITICAL)
|
||||
logger.critical(f"Error parsing arguments: {' '.join(sys.argv)}")
|
||||
@@ -1146,8 +1151,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
||||
resolve_env=global_arguments.resolve_env and not arguments.get('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)
|
||||
check_and_show_help_on_no_args(configs, schema)
|
||||
|
||||
configuration_parse_errors = (
|
||||
(max(log.levelno for log in parse_logs) >= logging.CRITICAL) if parse_logs else False
|
||||
@@ -1198,3 +1202,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
||||
)
|
||||
|
||||
display_summary(summary_logs, log_json)
|
||||
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
main()
|
||||
|
||||
@@ -47,5 +47,6 @@ def collect_config_filenames(config_paths):
|
||||
for filename in sorted(os.listdir(path)):
|
||||
full_filename = os.path.join(path, filename)
|
||||
matching_filetype = full_filename.endswith(('.yaml', '.yml'))
|
||||
|
||||
if matching_filetype and not os.path.isdir(full_filename):
|
||||
yield os.path.abspath(full_filename)
|
||||
|
||||
@@ -153,6 +153,9 @@ def transform_optional_configuration(rendered_config, comment_out=True):
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
RUAMEL_YAML_END_OF_DOCUMENT_MARKER = '...\n'
|
||||
|
||||
|
||||
def render_configuration(config):
|
||||
'''
|
||||
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
|
||||
@@ -160,7 +163,13 @@ def render_configuration(config):
|
||||
dumper = ruamel.yaml.YAML(typ='rt')
|
||||
dumper.indent(mapping=INDENT, sequence=INDENT + SEQUENCE_INDENT, offset=INDENT)
|
||||
rendered = io.StringIO()
|
||||
dumper.dump(config, rendered)
|
||||
dumper.dump(
|
||||
config,
|
||||
rendered,
|
||||
# Dumping certain values (integers, for instance) causes ruamel.yaml to append an
|
||||
# end-of-document "..." marker. Strip it.
|
||||
transform=lambda dumped: dumped.removesuffix(RUAMEL_YAML_END_OF_DOCUMENT_MARKER),
|
||||
)
|
||||
|
||||
return rendered.getvalue()
|
||||
|
||||
|
||||
@@ -30,12 +30,10 @@ properties:
|
||||
source_directories_must_exist:
|
||||
type: boolean
|
||||
description: |
|
||||
Deprecated. Replaced by borgmatic treating Borg's "backup file not
|
||||
found" warning as an error by default. But if
|
||||
"source_directories_must_exist" is true, then source directories
|
||||
(and root pattern paths) must exist before a backup begins. If they
|
||||
don't, borgmatic errors. Defaults to false.
|
||||
example: true
|
||||
When true, source directories (and root pattern paths) must exist
|
||||
before a backup begins. If they don't, borgmatic errors. Defaults to
|
||||
true.
|
||||
example: false
|
||||
repositories:
|
||||
type: array
|
||||
items:
|
||||
@@ -166,6 +164,16 @@ properties:
|
||||
Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive.
|
||||
Defaults to true.
|
||||
example: false
|
||||
files_changed:
|
||||
type: string
|
||||
enum: ['ctime', 'mtime', 'disabled']
|
||||
description: |
|
||||
Threshold for considering a file as changed. See
|
||||
https://borgbackup.readthedocs.io/en/stable/usage/create.html for
|
||||
details. Defaults to "ctime". E.g., a file is considered changed if
|
||||
its ctime has changed since the last backup. (This option is
|
||||
supported for Borg 1.4.2+ only.)
|
||||
example: ctime
|
||||
files_cache:
|
||||
type: string
|
||||
description: |
|
||||
@@ -562,6 +570,12 @@ properties:
|
||||
Bypass Borg confirmation about check with repair option. Defaults to
|
||||
false and an interactive prompt from Borg.
|
||||
example: true
|
||||
msgpack_version_check:
|
||||
type: boolean
|
||||
description: |
|
||||
Optionally disable the msgpack version check. Default is true; use
|
||||
at your own risk. (This option is supported for Borg 1.4.2+ only.)
|
||||
example: false
|
||||
extra_borg_options:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -1084,9 +1098,9 @@ properties:
|
||||
default_actions:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to apply default actions (create, prune, compact and check)
|
||||
when no arguments are supplied to the borgmatic command. If set to
|
||||
false, borgmatic displays the help message instead.
|
||||
Whether to run default actions (create, prune, compact, and check)
|
||||
when no arguments are given on the command line. If set to false,
|
||||
borgmatic displays the help message instead.
|
||||
example: true
|
||||
skip_actions:
|
||||
type: array
|
||||
@@ -1115,6 +1129,7 @@ properties:
|
||||
- break-lock
|
||||
- key
|
||||
- borg
|
||||
- diff
|
||||
description: |
|
||||
List of one or more actions to skip running for this configuration
|
||||
file, even if specified on the command-line (explicitly or
|
||||
@@ -1331,6 +1346,7 @@ properties:
|
||||
- break-lock
|
||||
- key
|
||||
- borg
|
||||
- diff
|
||||
description: |
|
||||
List of actions for which the commands will be
|
||||
run. Defaults to running for all actions.
|
||||
@@ -1396,6 +1412,7 @@ properties:
|
||||
- break-lock
|
||||
- key
|
||||
- borg
|
||||
- diff
|
||||
description: |
|
||||
Only trigger the hook when borgmatic is run with
|
||||
particular actions listed here. Defaults to
|
||||
|
||||
+81
-58
@@ -2,6 +2,7 @@ import collections
|
||||
import contextlib
|
||||
import enum
|
||||
import json
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
@@ -19,7 +20,7 @@ BORG_ERROR_EXIT_CODE_START = 2
|
||||
BORG_ERROR_EXIT_CODE_END = 99
|
||||
|
||||
# See https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids
|
||||
BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS = {101, 102, 104, 105, 106, 107}
|
||||
BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS = {101, 102, 104, 105, 106}
|
||||
|
||||
|
||||
class Exit_status(enum.Enum):
|
||||
@@ -42,6 +43,18 @@ def command_is_borg(command, borg_local_path):
|
||||
return bool(borg_local_path and parsed_command[0] == borg_local_path)
|
||||
|
||||
|
||||
BORG_EXIT_CODE_TO_DESCRIPTION = {
|
||||
100: 'File changed while we backed it up',
|
||||
101: 'Include pattern never matched',
|
||||
102: 'General backup issue',
|
||||
103: 'File type or inode changed while we backed it up',
|
||||
104: 'Backup OS issue',
|
||||
105: 'Backup permission issue',
|
||||
106: 'Backup IO issue',
|
||||
107: 'Backup file not found',
|
||||
}
|
||||
|
||||
|
||||
def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None): # noqa: PLR0911
|
||||
'''
|
||||
Return an Exit_status value (e.g. SUCCESS, ERROR, or WARNING) based on interpreting the given
|
||||
@@ -55,46 +68,47 @@ def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_code
|
||||
if exit_code == 0:
|
||||
return Exit_status.SUCCESS
|
||||
|
||||
if command_is_borg(command, borg_local_path):
|
||||
# First try looking for the exit code in the borg_exit_codes configuration.
|
||||
for entry in borg_exit_codes or ():
|
||||
if entry.get('code') == exit_code:
|
||||
treat_as = entry.get('treat_as')
|
||||
if not command_is_borg(command, borg_local_path):
|
||||
return Exit_status.ERROR
|
||||
|
||||
if treat_as == 'error':
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code} as an error, as per configuration',
|
||||
)
|
||||
return Exit_status.ERROR
|
||||
description = BORG_EXIT_CODE_TO_DESCRIPTION.get(exit_code)
|
||||
description_parenthetical = f' ({description})' if description else ''
|
||||
|
||||
if treat_as == 'warning':
|
||||
logger.warning(
|
||||
f'Treating exit code {exit_code} as a warning, as per configuration',
|
||||
)
|
||||
return Exit_status.WARNING
|
||||
# First try looking for the exit code in the borg_exit_codes configuration.
|
||||
for entry in borg_exit_codes or ():
|
||||
if entry.get('code') == exit_code:
|
||||
treat_as = entry.get('treat_as')
|
||||
|
||||
# If the exit code doesn't have explicit configuration, then fall back to the default
|
||||
# behavior of treating Borg errors as errors and some Borg warnings as errors.
|
||||
if exit_code in BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS:
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code} as an error, as per borgmatic defaults',
|
||||
)
|
||||
|
||||
return Exit_status.ERROR
|
||||
|
||||
return (
|
||||
Exit_status.ERROR
|
||||
if (
|
||||
exit_code < 0
|
||||
or (
|
||||
exit_code >= BORG_ERROR_EXIT_CODE_START
|
||||
and exit_code <= BORG_ERROR_EXIT_CODE_END
|
||||
if treat_as == 'error':
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as an error, as per configuration',
|
||||
)
|
||||
)
|
||||
else Exit_status.WARNING
|
||||
return Exit_status.ERROR
|
||||
|
||||
if treat_as == 'warning':
|
||||
logger.warning(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as a warning, as per configuration',
|
||||
)
|
||||
return Exit_status.WARNING
|
||||
|
||||
# If the exit code doesn't have explicit configuration, then fall back to the default
|
||||
# behavior of treating Borg errors as errors and some Borg warnings as errors.
|
||||
if exit_code in BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS:
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as an error, as per borgmatic defaults',
|
||||
)
|
||||
|
||||
return Exit_status.ERROR
|
||||
return Exit_status.ERROR
|
||||
|
||||
if exit_code < 0 or (
|
||||
exit_code >= BORG_ERROR_EXIT_CODE_START and exit_code <= BORG_ERROR_EXIT_CODE_END
|
||||
):
|
||||
return Exit_status.ERROR
|
||||
|
||||
logger.warning(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as a warning, as per borgmatic defaults',
|
||||
)
|
||||
return Exit_status.WARNING
|
||||
|
||||
|
||||
def command_for_process(process):
|
||||
@@ -249,6 +263,7 @@ def read_lines(buffer, process, line_separator='\n'):
|
||||
data = b''
|
||||
encoded_separator = line_separator.encode()
|
||||
separator_size = len(encoded_separator)
|
||||
encoding = locale.getpreferredencoding()
|
||||
|
||||
while True:
|
||||
chunk = os.read(buffer.fileno(), READ_CHUNK_SIZE)
|
||||
@@ -271,14 +286,14 @@ def read_lines(buffer, process, line_separator='\n'):
|
||||
if separator_position == -1:
|
||||
break
|
||||
|
||||
lines.append(data[:separator_position].decode())
|
||||
lines.append(data[:separator_position].decode(encoding))
|
||||
data = data[separator_position + separator_size :]
|
||||
|
||||
yield tuple(lines)
|
||||
|
||||
# Yield any leftover data from the end of the buffer.
|
||||
if data:
|
||||
yield (data.decode().rstrip(),)
|
||||
yield (data.decode(encoding).rstrip(),)
|
||||
|
||||
|
||||
Buffer_reader = collections.namedtuple(
|
||||
@@ -299,8 +314,9 @@ def log_buffer_lines(
|
||||
'''
|
||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, and
|
||||
whether to capture stderr, read and log any ready output lines from the buffers. Additionally,
|
||||
if the log level is None for any log record, then yield those log messages for capture.
|
||||
whether to capture stderr, read and log any ready output lines from the buffers. Additionally,
|
||||
for any log records with a log level that meets or exceeds the output log level, yield those log
|
||||
messages for capture.
|
||||
|
||||
This function just does one "turn of the crank" of logging buffer output. It is intended to be
|
||||
called repeatedly to continue to process buffers.
|
||||
@@ -350,7 +366,11 @@ def log_buffer_lines(
|
||||
last_lines=process_metadatas[reader.process].last_lines,
|
||||
)
|
||||
|
||||
if log_record.levelno is None and process_metadatas[reader.process].capture:
|
||||
if (
|
||||
log_record.levelno is None
|
||||
or output_log_level is None
|
||||
or log_record.levelno >= output_log_level
|
||||
) and process_metadatas[reader.process].capture:
|
||||
yield log_record.getMessage()
|
||||
|
||||
|
||||
@@ -409,8 +429,8 @@ def log_remaining_buffer_lines(
|
||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, and
|
||||
whether to capture stderr, drain and log any remaining output lines from the buffers until
|
||||
they're empty. Additionally, if the log level is None for any log record, then yield those log
|
||||
messages for capture.
|
||||
they're empty. Additionally, for any log records with a log level that meets or exceeds the
|
||||
output log level, yield those log messages for capture.
|
||||
'''
|
||||
for output_buffer, reader in buffer_readers.items():
|
||||
if not reader.process:
|
||||
@@ -430,7 +450,11 @@ def log_remaining_buffer_lines(
|
||||
),
|
||||
)
|
||||
|
||||
if log_record.levelno is None and process_metadatas[reader.process].capture:
|
||||
if (
|
||||
log_record.levelno is None
|
||||
or output_log_level is None
|
||||
or log_record.levelno >= output_log_level
|
||||
) and process_metadatas[reader.process].capture:
|
||||
yield log_record.getMessage()
|
||||
|
||||
|
||||
@@ -613,6 +637,7 @@ def execute_command(
|
||||
|
||||
def execute_command_and_capture_output(
|
||||
full_command,
|
||||
output_log_level=None,
|
||||
input_file=None,
|
||||
capture_stderr=False,
|
||||
shell=False,
|
||||
@@ -627,13 +652,15 @@ def execute_command_and_capture_output(
|
||||
output (stdout) as a generator that yields one line at a time. The generator must be consumed in
|
||||
order for the called command to execute.
|
||||
|
||||
If an input file descriptor is given, then pipe it to the command's stdin. If capture stderr is
|
||||
True, then capture stderr in addition to stdout. If shell is True, execute the command within a
|
||||
shell. If an environment variables dict is given, then pass it into the command. If a working
|
||||
directory is given, use that as the present working directory when running the command. If a
|
||||
Borg local path is given, and the command matches it (regardless of arguments), treat exit code
|
||||
1 as a warning instead of an error. But if Borg exit codes are given as a sequence of exit code
|
||||
configuration dicts, then use that configuration to decide what's an error and what's a warning.
|
||||
If an output log level is given, then instead of suppressing log output, also output the
|
||||
captured lines at the given log level. If an input file descriptor is given, then pipe it to
|
||||
the command's stdin. If capture stderr is True, then capture stderr in addition to stdout. If
|
||||
shell is True, execute the command within a shell. If an environment variables dict is given,
|
||||
then pass it into the command. If a working directory is given, use that as the present working
|
||||
directory when running the command. If a Borg local path is given, and the command matches it
|
||||
(regardless of arguments), treat exit code 1 as a warning instead of an error. But if Borg exit
|
||||
codes are given as a sequence of exit code configuration dicts, then use that configuration to
|
||||
decide what's an error and what's a warning.
|
||||
|
||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||
'''
|
||||
@@ -661,22 +688,20 @@ def execute_command_and_capture_output(
|
||||
raise
|
||||
|
||||
if error.output is not None:
|
||||
yield from iter(error.output.decode().splitlines())
|
||||
yield from iter(error.output.decode(locale.getpreferredencoding()).splitlines())
|
||||
|
||||
return
|
||||
|
||||
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
||||
captured_lines = log_outputs(
|
||||
yield from log_outputs(
|
||||
(process,),
|
||||
(input_file,),
|
||||
None,
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
capture_stderr=capture_stderr,
|
||||
)
|
||||
|
||||
yield from captured_lines
|
||||
|
||||
|
||||
def execute_command_with_processes(
|
||||
full_command,
|
||||
@@ -739,12 +764,10 @@ def execute_command_with_processes(
|
||||
raise
|
||||
|
||||
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
||||
captured_lines = log_outputs(
|
||||
yield from log_outputs(
|
||||
(*processes, command_process),
|
||||
(input_file, output_file),
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
)
|
||||
|
||||
yield from captured_lines
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import contextlib
|
||||
import glob
|
||||
import importlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -19,6 +20,29 @@ def use_streaming(hook_config, config): # pragma: no cover
|
||||
return False
|
||||
|
||||
|
||||
MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW = 10
|
||||
|
||||
|
||||
def resolve_config_path_symlinks(path):
|
||||
'''
|
||||
Given a path, resolve and yield each successive symlink until the final non-symlink target. If
|
||||
the given path isn't a symlink, then just yield it.
|
||||
|
||||
Raise ValueError if we have to follow too many symlinks without getting to the final target.
|
||||
'''
|
||||
original_path = path
|
||||
|
||||
for _ in range(MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW):
|
||||
yield os.path.abspath(path)
|
||||
|
||||
if not os.path.islink(path):
|
||||
return
|
||||
|
||||
path = os.readlink(path)
|
||||
|
||||
raise ValueError(f'Too many symlinks to follow for configuration path: {original_path}')
|
||||
|
||||
|
||||
def dump_data_sources(
|
||||
hook_config,
|
||||
config,
|
||||
@@ -34,6 +58,9 @@ def dump_data_sources(
|
||||
the archive. But skip this if the bootstrap store_config_files option is False or if this is a
|
||||
dry run.
|
||||
|
||||
If any configuration paths are symlinks, then store each symlink along with any destination
|
||||
paths as well.
|
||||
|
||||
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
||||
'''
|
||||
if hook_config and hook_config.get('store_config_files') is False:
|
||||
@@ -45,6 +72,10 @@ def dump_data_sources(
|
||||
'manifest.json',
|
||||
)
|
||||
|
||||
resolved_config_paths = tuple(
|
||||
itertools.chain.from_iterable(resolve_config_path_symlinks(path) for path in config_paths)
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
return []
|
||||
|
||||
@@ -54,7 +85,7 @@ def dump_data_sources(
|
||||
json.dump(
|
||||
{
|
||||
'borgmatic_version': importlib.metadata.version('borgmatic'),
|
||||
'config_paths': config_paths,
|
||||
'config_paths': resolved_config_paths,
|
||||
},
|
||||
manifest_file,
|
||||
)
|
||||
@@ -67,7 +98,7 @@ def dump_data_sources(
|
||||
),
|
||||
)
|
||||
|
||||
for config_path in config_paths:
|
||||
for config_path in resolved_config_paths:
|
||||
borgmatic.hooks.data_source.config.inject_pattern(
|
||||
patterns,
|
||||
borgmatic.borg.pattern.Pattern(
|
||||
|
||||
@@ -71,7 +71,7 @@ def get_datasets_to_backup(zfs_command, patterns):
|
||||
)
|
||||
# Skip datasets that are marked "canmount=off", because mounting their snapshots will
|
||||
# result in completely empty mount points—thereby preventing us from backing them up.
|
||||
if can_mount == 'on'
|
||||
if can_mount != 'off'
|
||||
),
|
||||
key=lambda dataset: dataset.mount_point,
|
||||
reverse=True,
|
||||
|
||||
@@ -88,16 +88,17 @@ class Loki_log_handler(logging.Handler):
|
||||
A log handler that sends logs to Loki.
|
||||
'''
|
||||
|
||||
def __init__(self, url, send_logs, dry_run):
|
||||
def __init__(self, url, send_logs, log_level, dry_run):
|
||||
'''
|
||||
Given a URL to send logs to, whether all borgmatic logs should be sent (or just explicitly
|
||||
added messages from this hook), and whether this is a dry run, create an instance of
|
||||
Loki_log_buffer.
|
||||
added messages from this hook), the log level to use (influencing which logs get sent), and
|
||||
whether this is a dry run, create an instance of Loki_log_buffer.
|
||||
'''
|
||||
super().__init__()
|
||||
|
||||
self.buffer = Loki_log_buffer(url, dry_run)
|
||||
self.send_logs = send_logs
|
||||
self.setLevel(log_level)
|
||||
|
||||
def emit(self, record):
|
||||
'''
|
||||
@@ -138,7 +139,7 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
||||
Add a handler to the root logger to regularly send the logs to Loki.
|
||||
'''
|
||||
url = hook_config.get('url')
|
||||
loki = Loki_log_handler(url, hook_config.get('send_logs', False), dry_run)
|
||||
loki = Loki_log_handler(url, hook_config.get('send_logs', False), monitoring_log_level, dry_run)
|
||||
|
||||
for key, value in hook_config.get('labels').items():
|
||||
if value == '__hostname':
|
||||
@@ -150,7 +151,9 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
||||
else:
|
||||
loki.add_label(key, value)
|
||||
|
||||
logging.getLogger().addHandler(loki)
|
||||
global_logger = logging.getLogger()
|
||||
global_logger.addHandler(loki)
|
||||
global_logger.setLevel(min(handler.level for handler in global_logger.handlers))
|
||||
|
||||
|
||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
||||
@@ -166,9 +169,11 @@ def destroy_monitor(hook_config, config, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Remove the monitor handler that was added to the root logger.
|
||||
'''
|
||||
logger = logging.getLogger()
|
||||
global_logger = logging.getLogger()
|
||||
|
||||
for handler in tuple(logger.handlers):
|
||||
for handler in tuple(global_logger.handlers):
|
||||
if isinstance(handler, Loki_log_handler):
|
||||
handler.flush()
|
||||
logger.removeHandler(handler)
|
||||
global_logger.removeHandler(handler)
|
||||
|
||||
global_logger.setLevel(min(handler.level for handler in global_logger.handlers))
|
||||
|
||||
+6
-2
@@ -5,13 +5,17 @@ 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 && borgmatic config generate --destination /etc/borgmatic --split && chmod +r /etc/borgmatic/*.yaml
|
||||
RUN mkdir /command-line \
|
||||
&& borgmatic --help > /command-line/global.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 import" "key change-passphrase" recreate borg; do \
|
||||
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" "config show" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate diff borg; do \
|
||||
borgmatic $action --help > /command-line/${action/ /-}.txt; done
|
||||
RUN /app/docs/fetch-contributors >> /contributors.html
|
||||
|
||||
FROM docker.io/node:22.4.0-alpine AS html
|
||||
|
||||
ARG ENVIRONMENT=production
|
||||
ARG PORT=${PORT}
|
||||
ARG FONT=${FONT}
|
||||
|
||||
RUN echo "FONT: ${FONT}"
|
||||
|
||||
WORKDIR /source
|
||||
|
||||
@@ -28,7 +32,7 @@ COPY --from=borgmatic /etc/borgmatic/options.json /source/docs/reference/configu
|
||||
COPY --from=borgmatic /command-line/* /source/docs/_includes/borgmatic/command-line/
|
||||
COPY --from=borgmatic /contributors.html /source/docs/_includes/borgmatic/contributors.html
|
||||
COPY . /source
|
||||
RUN NODE_ENV=${ENVIRONMENT} npx eleventy --input=/source/docs --output=/output
|
||||
RUN NODE_ENV=${ENVIRONMENT} PORT=${PORT} FONT=${FONT} npx eleventy --input=/source/docs --output=/output
|
||||
RUN npx -y pagefind --site /output
|
||||
|
||||
FROM docker.io/nginx:1.26.1-alpine
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module.exports = function() {
|
||||
return {
|
||||
environment: process.env.NODE_ENV || "development"
|
||||
environment: process.env.NODE_ENV || "development",
|
||||
port: process.env.PORT || 8080,
|
||||
font: process.env.FONT || "custom"
|
||||
};
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
||||
<title>borgmatic{% if subtitle or title %} - {% endif %}{{ subtitle + ' - ' if subtitle}}{{ title }}</title>
|
||||
{% endif %}
|
||||
{%- set css %}
|
||||
{% include 'index.css' %}
|
||||
{% include 'index.css.njk' %}
|
||||
{% include 'components/lists.css' %}
|
||||
{% include 'components/external-links.css' %}
|
||||
{% include 'components/minilink.css' %}
|
||||
|
||||
@@ -21,7 +21,7 @@ headerClass: elv-header-default
|
||||
{% set navPages = collections.all | eleventyNavigation %}
|
||||
{% macro renderNavListItem(entry) -%}
|
||||
<li{% if entry.url == page.url %} class="elv-toc-active"{% endif %}>
|
||||
<a {% if entry.url %}href="{% if borgmatic.environment == "production" %}https://torsion.org/borgmatic{% else %}http://localhost:8080/borgmatic{% endif %}{{ entry.url | url }}"{% endif %}>{{ entry.title }}</a>
|
||||
<a {% if entry.url %}href="{% if borgmatic.environment == "production" %}https://torsion.org/borgmatic{% else %}http://localhost:{{ borgmatic.port }}/borgmatic{% endif %}{{ entry.url | url }}"{% endif %}>{{ entry.title }}</a>
|
||||
{%- if entry.children.length -%}
|
||||
<ul>
|
||||
{%- for child in entry.children %}{{ renderNavListItem(child) }}{% endfor -%}
|
||||
|
||||
@@ -2,10 +2,12 @@ services:
|
||||
traefik:
|
||||
image: public.ecr.aws/docker/library/traefik:3.5.3
|
||||
container_name: borgmatic-docs-traefik
|
||||
environment:
|
||||
- PORT=${PORT:-8080}
|
||||
command:
|
||||
- "--global.checkNewVersion=false"
|
||||
- "--global.sendAnonymousUsage=false"
|
||||
- "--entrypoints.web.address=:8080"
|
||||
- "--entrypoints.web.address=:${PORT}"
|
||||
- "--accesslog"
|
||||
- "--accesslog.fields.headers.defaultmode=keep"
|
||||
- "--providers.docker"
|
||||
@@ -14,7 +16,7 @@ services:
|
||||
- "--api.dashboard=false"
|
||||
- "--log.level=WARN"
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
- "127.0.0.1:${PORT}:${PORT}"
|
||||
volumes:
|
||||
- ${CONTAINER_SOCKET_PATH:-/run/user/docker.sock}:/var/run/docker.sock:ro
|
||||
docs:
|
||||
@@ -37,6 +39,10 @@ services:
|
||||
context: ..
|
||||
args:
|
||||
ENVIRONMENT: development
|
||||
PORT: ${PORT:-8080}
|
||||
FONT: ${FONT}
|
||||
environment:
|
||||
- PORT=${PORT:-8080}
|
||||
message:
|
||||
image: alpine
|
||||
container_name: borgmatic-docs-message
|
||||
@@ -44,6 +50,6 @@ services:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
echo; echo "You can view dev docs at http://localhost:8080/borgmatic/"; echo
|
||||
echo; echo "You can view dev docs at http://localhost:${PORT}/borgmatic/"; echo
|
||||
depends_on:
|
||||
- docs
|
||||
|
||||
@@ -14,7 +14,7 @@ import requests
|
||||
|
||||
def list_merged_pulls(url):
|
||||
'''
|
||||
Given a Gitea or GitHub API endpoint URL for pull requests, fetch and return the corresponding
|
||||
Given a Forgejo or GitHub API endpoint URL for pull requests, fetch and return the corresponding
|
||||
JSON for all such merged pull requests.
|
||||
'''
|
||||
response = requests.get(f'{url}?state=closed', headers={'Accept': 'application/json', 'Content-Type': 'application/json'})
|
||||
@@ -36,7 +36,6 @@ def list_contributing_issues(url):
|
||||
|
||||
PULLS_API_ENDPOINT_URLS = (
|
||||
'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/pulls',
|
||||
'https://api.github.com/repos/borgmatic-collective/borgmatic/pulls',
|
||||
)
|
||||
ISSUES_API_ENDPOINT_URL = 'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/issues?state=all'
|
||||
RECENT_CONTRIBUTORS_CUTOFF_DAYS = 365
|
||||
|
||||
@@ -12,10 +12,11 @@ decide how to respond. By default, Borg errors (and some warnings) result
|
||||
in a borgmatic error, while Borg successes don't.
|
||||
|
||||
<span class="minilink minilink-addedin">New in borgmatic version 2.1.0</span>
|
||||
borgmatic elevates most Borg warnings to errors by default. For instance, if a
|
||||
source directory is missing during backup, Borg indicates that with a warning
|
||||
exit code (`107`). And starting in borgmatic 2.1.0, that exit code is considered
|
||||
an error, so you'll actually find out about missing files.
|
||||
borgmatic elevates several Borg warnings to errors by default. For instance, if
|
||||
borgmatic doesn't have permission to read a configured source directory during
|
||||
backup, Borg indicates that with a warning exit code (`105`). And starting in
|
||||
borgmatic 2.1.0, that exit code is considered an error, so you'll actually find
|
||||
out about files that borgmatic can't read.
|
||||
|
||||
<span class="minilink minilink-addedin">With Borg version 1.4+</span> If the
|
||||
default behavior isn't sufficient for your needs, you can customize how
|
||||
@@ -23,7 +24,7 @@ borgmatic interprets [Borg's exit
|
||||
codes](https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids).
|
||||
|
||||
For instance, this borgmatic configuration elevates a Borg warning about source files
|
||||
changes during backup (exit code `100`)—and only those warnings—to
|
||||
changing during backup (exit code `100`)—and only those warnings—to
|
||||
errors:
|
||||
|
||||
```yaml
|
||||
@@ -32,14 +33,15 @@ borg_exit_codes:
|
||||
treat_as: error
|
||||
```
|
||||
|
||||
The following configuration does that *and* treats Borg's backup file not found
|
||||
(exit code `107`) as a warning:
|
||||
The following configuration does that *and* squashes errors about Borg
|
||||
encountering file permissions issues during backup (exit code `105`) to
|
||||
warnings.
|
||||
|
||||
```yaml
|
||||
borg_exit_codes:
|
||||
- code: 100
|
||||
treat_as: error
|
||||
- code: 107
|
||||
- code: 105
|
||||
treat_as: warning
|
||||
```
|
||||
|
||||
@@ -54,8 +56,11 @@ is not found:
|
||||
terminating with warning status, rc 107
|
||||
```
|
||||
|
||||
So if you want to configure borgmatic to treat this as an warning instead of an
|
||||
error, the exit status to use is `107`.
|
||||
So if you want to configure borgmatic's interpretation of this warning, the exit
|
||||
status to use is `107`. Note however that in the particular case of missing
|
||||
files, there's a separate [`source_directories_must_exist`
|
||||
option](https://torsion.org/borgmatic/reference/configuration/#source_directories_must_exist-option)
|
||||
that can catch such problems before Borg even runs.
|
||||
|
||||
<span class="minilink minilink-addedin">With Borg version 1.2 and earlier</span>
|
||||
Older versions of Borg didn't support granular exit codes, but still
|
||||
|
||||
@@ -190,8 +190,28 @@ This requires Docker (or Podman; see below) to be installed on your system.
|
||||
This script assumes you have permission to run `docker`. If you don't, then
|
||||
you may need to run with `sudo`.
|
||||
|
||||
### How to choose a different port
|
||||
|
||||
You can choose a different listening port in two ways:
|
||||
|
||||
#### 1. Modify the `.env` file
|
||||
|
||||
1. Open `docs/.env`.
|
||||
2. Change `PORT=8080` to your desired port number (e.g., `PORT=3000`).
|
||||
3. Run the development script: `scripts/dev-docs`.
|
||||
|
||||
#### 2. Use an environment variable
|
||||
|
||||
Alternatively, you can override the port directly from your terminal without
|
||||
modifying any files:
|
||||
|
||||
```bash
|
||||
PORT=3000 ./scripts/dev-docs
|
||||
```
|
||||
|
||||
After you run the script, you can point your web browser at
|
||||
http://localhost:8080/borgmatic/ to view the documentation with your changes.
|
||||
http://localhost:8080/borgmatic/ (or your chosen port) to view the documentation
|
||||
with your changes.
|
||||
|
||||
To close the documentation server, ctrl-C the script. Note that it does not
|
||||
currently auto-reload, so you'll need to stop it and re-run it for any
|
||||
@@ -205,7 +225,24 @@ borgmatic's developer build for documentation optionally supports using
|
||||
[Podman](https://podman.io/) instead of Docker.
|
||||
|
||||
Setting up Podman is outside the scope of this documentation. But once you
|
||||
install and configure Podman, then `scripts/dev-docs` should automatically use
|
||||
Podman instead of Docker.
|
||||
install and configure Podman, then `scripts/dev-docs` automatically uses Podman
|
||||
instead of Docker and [Podman
|
||||
Compose](https://github.com/containers/podman-compose) (if present) instead of
|
||||
Docker Compose. However Podman works fine with either Podman Compose or Docker
|
||||
Compose.
|
||||
|
||||
|
||||
## Use of generative AI
|
||||
|
||||
Please do not use AI agents to modify this codebase. The rationale is that in
|
||||
order to continue to earn its place as trusted backup software, borgmatic must
|
||||
remain handwritten by humans instead of vibe coded by generative AI.
|
||||
|
||||
Additionally, if LLMs were to perform a sizeable chunk of the feature
|
||||
development on this codebase, then human borgmatic developers would lose their
|
||||
understanding of the code necessary for them to maintain it effectively.
|
||||
|
||||
Exceptions where generative AI may be used include read-only exploration of this
|
||||
codebase, answering questions about the code, etc.
|
||||
|
||||
</span>
|
||||
|
||||
@@ -116,3 +116,22 @@ By default, borgmatic only logs to the console. But to enable simultaneous
|
||||
syslog or file logging, see the [logging
|
||||
documentation](https://torsion.org/borgmatic/reference/command-line/logging/)
|
||||
for details.
|
||||
|
||||
## Finding differences between two archives
|
||||
|
||||
<span class="minilink minilink-addedin">New in borgmatic version
|
||||
2.1.3</span>You can compare differences between two archives. For example:
|
||||
|
||||
```bash
|
||||
borgmatic diff --archive latest --second-archive host-2023-01-02T04:06:07.080910
|
||||
```
|
||||
|
||||
This shows the differences (file contents, user/group/mode) between the latest
|
||||
archive and the second one supplied.
|
||||
|
||||
Note that, by default, `borgmatic diff` compares everything in the archives; that
|
||||
is, patterns are _not_ taken into consideration. If you require this, supply the
|
||||
`--only-patterns` flag.
|
||||
|
||||
See the [Borg](https://borgbackup.readthedocs.io/en/stable/usage/diff.html)
|
||||
documentation for information on output format, what is compared, and more.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: 📥 How to install borgmatic
|
||||
eleventyNavigation:
|
||||
key: 📥 Install borgmatic
|
||||
parent: How-to guides
|
||||
order: -1
|
||||
---
|
||||
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before installing borgmatic, first [install
|
||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
|
||||
version 1.1. (borgmatic does not install Borg automatically so as to avoid
|
||||
conflicts with existing Borg installations.)
|
||||
|
||||
Then, [install uv](https://docs.astral.sh/uv/getting-started/installation/) as
|
||||
the root user (with `sudo`) to make installing borgmatic easier without
|
||||
impacting other Python applications on your system. For Debian, there is a
|
||||
[third-party package for
|
||||
uv](https://dario.griffo.io/posts/how-to-install-uv-debian/). On Ubuntu, there
|
||||
is a [snap package](https://snapcraft.io/install/astral-uv/ubuntu). On Arch, you
|
||||
can just install the `python-uv` package.
|
||||
|
||||
|
||||
### Root install
|
||||
|
||||
If you want borgmatic to run with privileged access so it can backup your system
|
||||
files, then install borgmatic as the root user by running the following
|
||||
commands:
|
||||
|
||||
```bash
|
||||
sudo uv tool update-shell
|
||||
sudo uv tool install borgmatic
|
||||
```
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
sudo su -
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
And if you'd also like `sudo borgmatic` to work as well, keep reading!
|
||||
|
||||
|
||||
### Non-root install
|
||||
|
||||
If you only want to run borgmatic as a non-root user (without privileged file
|
||||
access) *or* you want to make `sudo borgmatic` work so borgmatic runs as root,
|
||||
then install borgmatic as a non-root user by running the following commands as
|
||||
that user:
|
||||
|
||||
```bash
|
||||
uv tool update-shell
|
||||
uv tool install borgmatic
|
||||
```
|
||||
|
||||
This should work even if you've also installed borgmatic as the root user.
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
You can also try `sudo borgmatic --version` if you intend to run borgmatic
|
||||
with `sudo`. If that doesn't work, you may need to update your [sudoers
|
||||
`secure_path` option](https://wiki.archlinux.org/title/Sudo).
|
||||
|
||||
|
||||
### Other ways to install
|
||||
|
||||
Besides the approaches described above, there are several other options for
|
||||
installing borgmatic:
|
||||
|
||||
#### <span data-pagefind-weight="7.0">Docker / Podman</span>
|
||||
|
||||
* [container image with scheduled backups](https://github.com/borgmatic-collective/docker-borgmatic) (+ Docker Compose files)
|
||||
* [container image with multi-arch and Docker CLI support](https://github.com/modem7/docker-borgmatic)
|
||||
|
||||
#### Operating system packages
|
||||
|
||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Gentoo](https://packages.gentoo.org/packages/app-backup/borgmatic)
|
||||
* [Arch Linux](https://archlinux.org/packages/extra/any/borgmatic/)
|
||||
* [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=borgmatic)
|
||||
* [OpenBSD](https://openports.pl/path/sysutils/borgmatic)
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
|
||||
* [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)
|
||||
* [NixOS](https://search.nixos.org/packages?channel=unstable&show=borgmatic&query=borgmatic)
|
||||
|
||||
#### Etc.
|
||||
|
||||
* [stand-alone Linux binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases) (This is a beta feature!)
|
||||
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
||||
* [pipx](https://pipx.pypa.io/stable/)
|
||||
|
||||
|
||||
## Next steps
|
||||
|
||||
* [Set up backups](https://torsion.org/borgmatic/how-to/set-up-backups/)
|
||||
@@ -64,6 +64,27 @@ suppressed so as not to interfere with the captured JSON. Also note that JSON
|
||||
output only shows up at the console and not in syslog.
|
||||
|
||||
|
||||
### Getting configuration
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 2.1.3</span> If you want
|
||||
to consume borgmatic's computed configuration in your scripts, use the [`config
|
||||
show`
|
||||
action](https://torsion.org/borgmatic/reference/command-line/actions/config-show/).
|
||||
Here's an example:
|
||||
|
||||
```bash
|
||||
borgmatic config show --json
|
||||
```
|
||||
|
||||
That outputs borgmatic's entire configuration as JSON with one array element per
|
||||
configuration file.
|
||||
|
||||
Or you can ask for the value of a particular option:
|
||||
|
||||
```bash
|
||||
borgmatic config show --option repositories --json
|
||||
```
|
||||
|
||||
### Latest backups
|
||||
|
||||
All borgmatic actions that accept an `--archive` flag allow you to specify an
|
||||
|
||||
@@ -1,104 +1,14 @@
|
||||
---
|
||||
title: 📥 How to set up backups
|
||||
title: 📋 How to set up backups
|
||||
eleventyNavigation:
|
||||
key: 📥 Set up backups
|
||||
key: 📋 Set up backups
|
||||
parent: How-to guides
|
||||
order: 0
|
||||
---
|
||||
|
||||
To install borgmatic, first [install
|
||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
|
||||
version 1.1. (borgmatic does not install Borg automatically so as to avoid
|
||||
conflicts with existing Borg installations.)
|
||||
|
||||
Then, [install pipx](https://pypa.github.io/pipx/installation/) as the root
|
||||
user (with `sudo`) to make installing borgmatic easier without impacting other
|
||||
Python applications on your system. If you have trouble installing pipx with
|
||||
pip, then you can install a system package instead. E.g. on Ubuntu or Debian,
|
||||
run:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install pipx
|
||||
```
|
||||
|
||||
### Root install
|
||||
|
||||
If you want to run borgmatic on a schedule with privileged access to your
|
||||
files, then you should install borgmatic as the root user by running the
|
||||
following commands:
|
||||
|
||||
```bash
|
||||
sudo pipx ensurepath
|
||||
sudo pipx install borgmatic
|
||||
```
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
sudo su -
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
And if you'd also like `sudo borgmatic` to work, keep reading!
|
||||
|
||||
|
||||
### Non-root install
|
||||
|
||||
If you only want to run borgmatic as a non-root user (without privileged file
|
||||
access) *or* you want to make `sudo borgmatic` work so borgmatic runs as root,
|
||||
then install borgmatic as a non-root user by running the following commands as
|
||||
that user:
|
||||
|
||||
```bash
|
||||
pipx ensurepath
|
||||
pipx install borgmatic
|
||||
```
|
||||
|
||||
This should work even if you've also installed borgmatic as the root user.
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
You can also try `sudo borgmatic --version` if you intend to run borgmatic
|
||||
with `sudo`. If that doesn't work, you may need to update your [sudoers
|
||||
`secure_path` option](https://wiki.archlinux.org/title/Sudo).
|
||||
|
||||
|
||||
### Other ways to install
|
||||
|
||||
Besides the approaches described above, there are several other options for
|
||||
installing borgmatic:
|
||||
|
||||
#### <span data-pagefind-weight="7.0">Docker / Podman</span>
|
||||
|
||||
* [container image with scheduled backups](https://github.com/borgmatic-collective/docker-borgmatic) (+ Docker Compose files)
|
||||
* [container image with multi-arch and Docker CLI support](https://github.com/modem7/docker-borgmatic)
|
||||
|
||||
#### Operating system packages
|
||||
|
||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Gentoo](https://packages.gentoo.org/packages/app-backup/borgmatic)
|
||||
* [Arch Linux](https://archlinux.org/packages/extra/any/borgmatic/)
|
||||
* [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=borgmatic)
|
||||
* [OpenBSD](https://openports.pl/path/sysutils/borgmatic)
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
|
||||
* [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)
|
||||
* [NixOS](https://search.nixos.org/packages?channel=unstable&show=borgmatic&query=borgmatic)
|
||||
|
||||
#### Etc.
|
||||
|
||||
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
||||
* [uv tool install](https://docs.astral.sh/uv/)
|
||||
|
||||
Start by [installing
|
||||
borgmatic](https://torsion.org/borgmatic/how-to/install-borgmatic/) if you haven't
|
||||
already.
|
||||
|
||||
## Hosting providers
|
||||
|
||||
@@ -322,7 +232,7 @@ If you're using systemd instead of cron to run jobs, you can still configure
|
||||
borgmatic to run automatically.
|
||||
|
||||
(If you installed borgmatic from [Other ways to
|
||||
install](https://torsion.org/borgmatic/how-to/set-up-backups/#other-ways-to-install),
|
||||
install](https://torsion.org/borgmatic/how-to/install-borgmatic/#other-ways-to-install),
|
||||
you may already have borgmatic systemd service and timer files. If so, you may
|
||||
be able to skip some of the steps below.)
|
||||
|
||||
|
||||
+37
-8
@@ -5,10 +5,25 @@ eleventyNavigation:
|
||||
parent: How-to guides
|
||||
order: 14
|
||||
---
|
||||
In general, all you should need to do to upgrade borgmatic if you've
|
||||
[installed it with
|
||||
pipx](https://torsion.org/borgmatic/how-to/set-up-backups/#installation)
|
||||
is to run the following:
|
||||
In general, all you should need to do to upgrade borgmatic if you've [installed
|
||||
it with uv](https://docs.astral.sh/uv/) is to run the following:
|
||||
|
||||
```bash
|
||||
sudo uv tool upgrade borgmatic
|
||||
```
|
||||
|
||||
Omit `sudo` if you installed borgmatic as a non-root user. And if you
|
||||
installed borgmatic *both* as root and as a non-root user, you'll need to
|
||||
upgrade each installation independently.
|
||||
|
||||
|
||||
### Upgrading from other installation methods
|
||||
|
||||
#### pipx
|
||||
|
||||
If you have borgmatic installed with
|
||||
[pipx](https://pipx.pypa.io/latest/installation/), and you'd like to continue
|
||||
using pipx, then you can upgrade borgmatic with:
|
||||
|
||||
```bash
|
||||
sudo pipx upgrade borgmatic
|
||||
@@ -18,16 +33,30 @@ Omit `sudo` if you installed borgmatic as a non-root user. And if you
|
||||
installed borgmatic *both* as root and as a non-root user, you'll need to
|
||||
upgrade each installation independently.
|
||||
|
||||
But if you'd like to switch your borgmatic install from pipx to
|
||||
[uv](https://docs.astral.sh/uv/), uninstall borgmatic with pipx (`sudo pipx
|
||||
uninstall borgmatic`) and then [install borgmatic with
|
||||
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/).
|
||||
|
||||
Either one should work just fine. uv is just faster than pipx and also used for
|
||||
borgmatic
|
||||
[development](https://torsion.org/borgmatic/how-to/develop-on-borgmatic/).
|
||||
|
||||
|
||||
#### pip install
|
||||
|
||||
If you originally installed borgmatic with `sudo pip3 install --user`, you can
|
||||
uninstall it first with `sudo pip3 uninstall borgmatic` and then [install it
|
||||
again with
|
||||
pipx](https://torsion.org/borgmatic/how-to/set-up-backups/#installation),
|
||||
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/),
|
||||
which should better isolate borgmatic from your other Python applications.
|
||||
|
||||
But if you [installed borgmatic without pipx or
|
||||
pip3](https://torsion.org/borgmatic/how-to/set-up-backups/#other-ways-to-install),
|
||||
then your upgrade method may be different.
|
||||
|
||||
#### Etc.
|
||||
|
||||
If you installed borgmatic [some other
|
||||
way](https://torsion.org/borgmatic/how-to/install-borgmatic/#other-ways-to-install),
|
||||
then your upgrade method may be different.
|
||||
|
||||
|
||||
### Upgrading your configuration
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: config show
|
||||
eleventyNavigation:
|
||||
key: config show
|
||||
parent: 🎬 Actions
|
||||
---
|
||||
|
||||
{% include snippet/command-line/sample.md %}
|
||||
|
||||
```
|
||||
{% include borgmatic/command-line/config-show.txt %}
|
||||
```
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Scripting borgmatic](https://torsion.org/borgmatic/how-to/monitor-your-backups/#scripting-borgmatic)
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: diff
|
||||
eleventyNavigation:
|
||||
key: diff
|
||||
parent: 🎬 Actions
|
||||
---
|
||||
|
||||
{% include snippet/command-line/sample.md %}
|
||||
|
||||
```
|
||||
{% include borgmatic/command-line/diff.txt %}
|
||||
```
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Finding differences](https://torsion.org/borgmatic/how-to/inspect-your-backups/#finding-differences-between-two-archives)
|
||||
@@ -97,4 +97,3 @@ option for limiting the archives used for the `check` action was a separate
|
||||
`prefix` in the `consistency` section. Both of these options are deprecated in
|
||||
favor of the auto-matching behavior (or `match_archives`/`--match-archives`)
|
||||
in newer versions of borgmatic.
|
||||
|
||||
|
||||
@@ -317,3 +317,6 @@ a default location.
|
||||
|
||||
This will output the merged configuration as borgmatic sees it, which can be
|
||||
helpful for understanding how your includes work in practice.
|
||||
|
||||
Also see the [`config show`
|
||||
action](https://torsion.org/borgmatic/reference/command-line/actions/config-show/).
|
||||
|
||||
@@ -12,12 +12,11 @@ us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
|
||||
|
||||
Depending on how you installed borgmatic, it may not have come with Apprise.
|
||||
For instance, if you originally [installed borgmatic with
|
||||
pipx](https://torsion.org/borgmatic/how-to/set-up-backups/),
|
||||
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/),
|
||||
run the following to install Apprise so borgmatic can use it:
|
||||
|
||||
```bash
|
||||
sudo pipx uninstall borgmatic
|
||||
sudo pipx install borgmatic[Apprise]
|
||||
sudo uv tool install borgmatic[Apprise]
|
||||
```
|
||||
|
||||
Omit `sudo` if borgmatic is installed as a non-root user.
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "2.1.2"
|
||||
version = "2.1.4"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
@@ -44,7 +44,7 @@ namespaces = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = "tests"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end --timeout=120"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
@@ -100,6 +100,7 @@ ignore = [
|
||||
"S404", # subprocess import
|
||||
"SIM115", # open() without context manager
|
||||
"SIM905", # split() on literal string
|
||||
"TRY004", # type check and raise something other than TypeError
|
||||
]
|
||||
|
||||
[tool.ruff.lint.flake8-quotes]
|
||||
|
||||
+11
-5
@@ -2,11 +2,17 @@
|
||||
|
||||
set -e
|
||||
|
||||
USER_PODMAN_SOCKET_PATH=/run/user/$UID/podman/podman.sock
|
||||
|
||||
if [ -e "$USER_PODMAN_SOCKET_PATH" ]; then
|
||||
export DOCKER_HOST="unix://$USER_PODMAN_SOCKET_PATH"
|
||||
if command -v podman-compose >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="podman-compose"
|
||||
export CONTAINER_SOCKET_PATH="${XDG_RUNTIME_DIR:-/run/user/$UID}/podman/podman.sock"
|
||||
else
|
||||
COMPOSE_CMD="docker-compose"
|
||||
USER_PODMAN_SOCKET_PATH=/run/user/$UID/podman/podman.sock
|
||||
export CONTAINER_SOCKET_PATH="$USER_PODMAN_SOCKET_PATH"
|
||||
|
||||
if [ -e "$USER_PODMAN_SOCKET_PATH" ]; then
|
||||
export DOCKER_HOST="unix://$USER_PODMAN_SOCKET_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
BUILDKIT_PROGRESS=plain docker-compose --file docs/docker-compose.yaml up --build --force-recreate
|
||||
BUILDKIT_PROGRESS=plain $COMPOSE_CMD --file docs/docker-compose.yaml up --build --force-recreate
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ -e "$USER_PODMAN_SOCKET_PATH" ]; then
|
||||
export CONTAINER_SOCKET_PATH="$USER_PODMAN_SOCKET_PATH"
|
||||
fi
|
||||
|
||||
podman build --tag borgmatic-release-docs --file docs/Dockerfile .
|
||||
podman build --tag borgmatic-release-docs --file docs/Dockerfile --env FONT=plain .
|
||||
docs_container_id=$(podman create borgmatic-release-docs)
|
||||
podman cp "$docs_container_id":/usr/share/nginx/html - > borgmatic-docs-dump.tar
|
||||
tar xf borgmatic-docs-dump.tar
|
||||
|
||||
@@ -43,6 +43,16 @@ twine upload -r pypi --username __token__ "$wheel_path"
|
||||
scripts/export-docs-from-image
|
||||
docs_path=dist/borgmatic-docs.tar.gz
|
||||
|
||||
# Build stand-alone binary.
|
||||
uv venv --clear binary
|
||||
source binary/bin/activate
|
||||
uv pip install -r binary_requirements.txt
|
||||
nuitka --mode=onefile --enable-plugin=upx --include-package-data=borgmatic --include-data-dir=borgmatic.egg-info=borgmatic.egg-info --include-package=borgmatic.hooks --include-package=apprise --no-deployment-flag=self-execution borgmatic/commands/borgmatic.py
|
||||
deactivate
|
||||
rm -fr binary borgmatic.build borgmatic.dist
|
||||
standalone_binary_path="dist/borgmatic-${version}-binary-linux-glibc-x86_64"
|
||||
mv borgmatic.bin "$standalone_binary_path"
|
||||
|
||||
# Set release changelogs on projects.torsion.org and GitHub.
|
||||
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"
|
||||
escaped_release_changelog="$(echo "$release_changelog" | sed -z 's/\n/\\n/g' | sed -z 's/\"/\\"/g')"
|
||||
@@ -68,6 +78,11 @@ curl --silent --request POST \
|
||||
--header "Authorization: token $projects_token" \
|
||||
--header "Accept: application/json" \
|
||||
--form attachment=@"$docs_path"
|
||||
curl --silent --request POST \
|
||||
"https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/releases/$release_id/assets?name=$(basename $standalone_binary_path)" \
|
||||
--header "Authorization: token $projects_token" \
|
||||
--header "Accept: application/json" \
|
||||
--form attachment=@"$standalone_binary_path"
|
||||
|
||||
github-release create --token="$github_token" --owner=witten --repo=borgmatic --tag="$version" --target_commit="main" \
|
||||
--name="borgmatic $version" --body="$release_changelog"
|
||||
|
||||
+16
-15
@@ -1,27 +1,28 @@
|
||||
apprise==1.9.5
|
||||
attrs==25.4.0
|
||||
certifi==2025.10.5
|
||||
charset-normalizer==3.4.4
|
||||
apprise==1.9.9
|
||||
attrs==26.1.0
|
||||
certifi==2026.2.25
|
||||
charset-normalizer==3.4.6
|
||||
click>=8.1.8
|
||||
codespell==2.4.1
|
||||
coverage==7.11.0
|
||||
flexmock==0.12.2
|
||||
codespell==2.4.2
|
||||
coverage==7.13.5
|
||||
flexmock==0.13.0
|
||||
idna==3.11
|
||||
iniconfig==2.3.0
|
||||
jsonschema==4.25.1
|
||||
jsonschema==4.26.0
|
||||
jsonschema-specifications==2025.9.1
|
||||
markdown==3.9
|
||||
markdown==3.10.2
|
||||
oauthlib==3.3.1
|
||||
packaging==25.0
|
||||
packaging==26.0
|
||||
pluggy==1.6.0
|
||||
pygments==2.19.2
|
||||
pytest==8.4.2
|
||||
pytest-cov==7.0.0
|
||||
pygments==2.20.0
|
||||
pytest==9.0.2
|
||||
pytest-cov==7.1.0
|
||||
pytest-timeout==2.4.0
|
||||
pyyaml>5.0.0
|
||||
referencing==0.37.0
|
||||
requests==2.32.5
|
||||
requests==2.33.1
|
||||
requests-oauthlib==2.0.0
|
||||
rpds-py==0.28.0
|
||||
rpds-py==0.30.0
|
||||
ruamel-yaml>0.15.0
|
||||
typing-extensions==4.15.0
|
||||
urllib3==2.6.3
|
||||
|
||||
@@ -35,7 +35,7 @@ BUILTIN_DATASETS = (
|
||||
'used': '256K',
|
||||
'avail': '23.7M',
|
||||
'refer': '25K',
|
||||
'canmount': 'on',
|
||||
'canmount': 'noauto',
|
||||
'mountpoint': '/e2e/pool/dataset',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
MONGO_INITDB_ROOT_PASSWORD: test2
|
||||
command: docker-entrypoint.sh --port=27018
|
||||
tests:
|
||||
image: docker.io/alpine:3.22
|
||||
image: docker.io/alpine:3.23
|
||||
environment:
|
||||
TEST_CONTAINER: true
|
||||
volumes:
|
||||
|
||||
@@ -69,6 +69,7 @@ class Background_web_server:
|
||||
self.thread.join()
|
||||
|
||||
|
||||
FINISH = 1
|
||||
START_AND_FINISH = 2
|
||||
START_LOG_AND_FINISH = 3
|
||||
|
||||
@@ -94,7 +95,7 @@ START_LOG_AND_FINISH = 3
|
||||
),
|
||||
(
|
||||
'loki:\n url: http://localhost:12345/loki/api/v1/push\n labels:\n app: borgmatic\n send_logs: true',
|
||||
START_AND_FINISH,
|
||||
FINISH,
|
||||
),
|
||||
(
|
||||
'loki:\n url: http://localhost:12345/loki/api/v1/push\n labels:\n app: borgmatic',
|
||||
|
||||
@@ -35,7 +35,7 @@ def assert_command_does_not_duplicate_flags(command, *args, **kwargs):
|
||||
if '--json' in command:
|
||||
return '{}'
|
||||
|
||||
return None
|
||||
return ''
|
||||
|
||||
|
||||
def fuzz_argument(arguments, argument_name):
|
||||
@@ -159,10 +159,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']
|
||||
flexmock(borgmatic.borg.info).should_receive('execute_command_and_capture_output').replace_with(
|
||||
assert_command_does_not_duplicate_flags,
|
||||
)
|
||||
flexmock(borgmatic.borg.info).should_receive('execute_command').replace_with(
|
||||
assert_command_does_not_duplicate_flags,
|
||||
lambda command, *args, **kwargs: iter((assert_command_does_not_duplicate_flags(command),)),
|
||||
)
|
||||
|
||||
for argument_name in dir(arguments):
|
||||
|
||||
@@ -292,6 +292,12 @@ def test_render_configuration_converts_configuration_to_yaml_string():
|
||||
assert yaml_string == 'foo: bar\n'
|
||||
|
||||
|
||||
def test_render_configuration_strips_ruamel_yaml_end_of_document_marker():
|
||||
yaml_string = module.render_configuration(33)
|
||||
|
||||
assert yaml_string == '33\n'
|
||||
|
||||
|
||||
def test_write_configuration_does_not_raise():
|
||||
flexmock(os.path).should_receive('exists').and_return(False)
|
||||
flexmock(os).should_receive('makedirs')
|
||||
|
||||
@@ -10,7 +10,7 @@ MAXIMUM_LINE_LENGTH = 80
|
||||
def test_schema_line_length_stays_under_limit():
|
||||
schema_file = open(borgmatic.config.validate.schema_filename())
|
||||
|
||||
for line in schema_file.readlines():
|
||||
for line in schema_file:
|
||||
assert len(line.rstrip('\n')) <= MAXIMUM_LINE_LENGTH
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from borgmatic.hooks.monitoring import loki as module
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_with_send_logs_posts_to_server_after_buffer_full():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, log_level=10, dry_run=False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: ''),
|
||||
).once()
|
||||
@@ -18,7 +18,7 @@ def test_loki_log_handler_raw_with_send_logs_posts_to_server_after_buffer_full()
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_without_send_logs_posts_to_server_without_buffering():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, log_level=10, dry_run=False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: ''),
|
||||
).times(3)
|
||||
@@ -28,7 +28,7 @@ def test_loki_log_handler_raw_without_send_logs_posts_to_server_without_bufferin
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_post_failure_does_not_raise():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, log_level=10, dry_run=False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: (_ for _ in ()).throw(requests.RequestException())),
|
||||
).once()
|
||||
@@ -46,7 +46,10 @@ def test_initialize_monitor_replaces_labels():
|
||||
'labels': {'hostname': '__hostname', 'config': '__config', 'config_full': '__config_path'},
|
||||
}
|
||||
config_filename = '/mock/path/test.yaml'
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run=False)
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config, flexmock(), config_filename, monitoring_log_level=10, dry_run=False
|
||||
)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
@@ -63,11 +66,12 @@ def test_initialize_monitor_adds_log_handler():
|
||||
Assert that calling initialize_monitor adds our logger to the root logger.
|
||||
'''
|
||||
hook_config = {'url': 'http://localhost:3100/loki/api/v1/push', 'labels': {'app': 'borgmatic'}}
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config,
|
||||
flexmock(),
|
||||
config_filename='test.yaml',
|
||||
monitoring_log_level=flexmock(),
|
||||
monitoring_log_level=10,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
@@ -99,7 +103,10 @@ def test_ping_monitor_sends_log_message():
|
||||
|
||||
flexmock(module.requests).should_receive('post').replace_with(post)
|
||||
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run=False)
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config, flexmock(), config_filename, monitoring_log_level=10, dry_run=False
|
||||
)
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
flexmock(),
|
||||
@@ -108,7 +115,7 @@ def test_ping_monitor_sends_log_message():
|
||||
flexmock(),
|
||||
dry_run=False,
|
||||
)
|
||||
module.destroy_monitor(hook_config, flexmock(), flexmock(), dry_run=False)
|
||||
module.destroy_monitor(hook_config, flexmock(), monitoring_log_level=10, dry_run=False)
|
||||
|
||||
assert post_called
|
||||
|
||||
@@ -121,9 +128,11 @@ def test_destroy_monitor_removes_log_handler():
|
||||
config_filename = 'test.yaml'
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run=False)
|
||||
module.destroy_monitor(hook_config, flexmock(), flexmock(), dry_run=False)
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config, flexmock(), config_filename, monitoring_log_level=10, dry_run=False
|
||||
)
|
||||
module.destroy_monitor(hook_config, flexmock(), monitoring_log_level=10, dry_run=False)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
raise AssertionError()
|
||||
assert not isinstance(handler, module.Loki_log_handler)
|
||||
|
||||
@@ -105,18 +105,15 @@ def test_log_outputs_logs_each_line_separately():
|
||||
(),
|
||||
).and_return((there_process.stdout,))
|
||||
|
||||
assert (
|
||||
tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
assert tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
== ()
|
||||
)
|
||||
) == ('there',)
|
||||
|
||||
|
||||
def test_log_outputs_logs_stderr_as_error():
|
||||
@@ -140,18 +137,15 @@ def test_log_outputs_logs_stderr_as_error():
|
||||
(),
|
||||
).and_return((echo_process.stdout, echo_process.stderr))
|
||||
|
||||
assert (
|
||||
tuple(
|
||||
module.log_outputs(
|
||||
(echo_process,),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
assert tuple(
|
||||
module.log_outputs(
|
||||
(echo_process,),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
== ()
|
||||
)
|
||||
) == ('error',)
|
||||
|
||||
|
||||
def test_log_outputs_skips_logs_for_process_with_none_stdout():
|
||||
@@ -180,18 +174,15 @@ def test_log_outputs_skips_logs_for_process_with_none_stdout():
|
||||
(),
|
||||
).and_return((there_process.stdout,))
|
||||
|
||||
assert (
|
||||
tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
assert tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
== ()
|
||||
)
|
||||
) == ('there',)
|
||||
|
||||
|
||||
def test_log_outputs_returns_output_without_logging_for_output_log_level_none():
|
||||
@@ -561,3 +552,10 @@ def test_log_outputs_with_unfinished_process_re_polls():
|
||||
)
|
||||
== ()
|
||||
)
|
||||
|
||||
|
||||
def test_read_lines_uses_system_locale_when_decoding_output():
|
||||
flexmock(module.locale).should_receive('getpreferredencoding').and_return('ISO-8859-1')
|
||||
process = subprocess.Popen(['echo', b'\xc4pple'], stdout=subprocess.PIPE)
|
||||
|
||||
assert tuple(module.read_lines(process.stdout, process)) == (('\xc4pple',),)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.logger
|
||||
from borgmatic.actions.config import show as module
|
||||
|
||||
|
||||
def test_run_show_with_single_configuration_file_does_not_separate_output():
|
||||
log_lines = []
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
def fake_logger_answer(message):
|
||||
log_lines.append(message)
|
||||
|
||||
flexmock(module.logger).should_receive('answer').replace_with(fake_logger_answer)
|
||||
show_arguments = flexmock(option=None, json=False)
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration').and_return(
|
||||
'output'
|
||||
)
|
||||
|
||||
module.run_show(show_arguments, configs={'test.yaml': {}})
|
||||
|
||||
assert log_lines == ['output']
|
||||
|
||||
|
||||
def test_run_show_with_multiple_configuration_files_separates_output():
|
||||
log_lines = []
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
def fake_logger_answer(message):
|
||||
log_lines.append(message)
|
||||
|
||||
flexmock(module.logger).should_receive('answer').replace_with(fake_logger_answer)
|
||||
show_arguments = flexmock(option=None, json=False)
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration').and_return(
|
||||
'output'
|
||||
).and_return('other')
|
||||
|
||||
module.run_show(show_arguments, configs={'test.yaml': {}, 'other.yaml': {}})
|
||||
|
||||
assert log_lines == ['---', 'output', '---', 'other']
|
||||
|
||||
|
||||
def test_run_show_with_option_limits_output():
|
||||
log_lines = []
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
def fake_logger_answer(message):
|
||||
log_lines.append(message)
|
||||
|
||||
flexmock(module.logger).should_receive('answer').replace_with(fake_logger_answer)
|
||||
show_arguments = flexmock(option='foo', json=False)
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration').with_args(
|
||||
33
|
||||
).and_return('33')
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration').with_args(
|
||||
None
|
||||
).and_return('null')
|
||||
|
||||
module.run_show(show_arguments, configs={'test.yaml': {'foo': 33, 'bar': 44}, 'other.yaml': {}})
|
||||
|
||||
assert log_lines == ['---', '33', '---', 'null']
|
||||
|
||||
|
||||
def test_run_show_with_json_outputs_json():
|
||||
flexmock(borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
show_arguments = flexmock(option=None, json=True)
|
||||
flexmock(module.sys.stdout).should_receive('write').with_args(
|
||||
'[{"foo": 33}, {"bar": 44}]'
|
||||
).once()
|
||||
|
||||
module.run_show(show_arguments, configs={'test.yaml': {'foo': 33}, 'other.yaml': {'bar': 44}})
|
||||
|
||||
|
||||
def test_run_show_with_json_and_option_limits_json():
|
||||
flexmock(borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
show_arguments = flexmock(option='foo', json=True)
|
||||
flexmock(module.sys.stdout).should_receive('write').with_args('[33, null]').once()
|
||||
|
||||
module.run_show(show_arguments, configs={'test.yaml': {'foo': 33}, 'other.yaml': {'bar': 44}})
|
||||
@@ -5,13 +5,13 @@ from borgmatic.actions.config import validate as module
|
||||
|
||||
def test_run_validate_does_not_raise():
|
||||
validate_arguments = flexmock(show=False)
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration')
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration').and_return('')
|
||||
|
||||
module.run_validate(validate_arguments, flexmock())
|
||||
|
||||
|
||||
def test_run_validate_with_show_does_not_raise():
|
||||
validate_arguments = flexmock(show=True)
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration')
|
||||
flexmock(module.borgmatic.config.generate).should_receive('render_configuration').and_return('')
|
||||
|
||||
module.run_validate(validate_arguments, {'test.yaml': flexmock(), 'other.yaml': flexmock()})
|
||||
|
||||
@@ -1061,8 +1061,9 @@ def test_compare_spot_check_hashes_returns_paths_having_failing_hashes():
|
||||
'hash2 /bar',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'nothash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{},
|
||||
{'xxh64': 'nothash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1104,8 +1105,8 @@ def test_compare_spot_check_hashes_handles_weird_backslashed_hashes_from_xxh64su
|
||||
'\\hash2 /bar',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'nothash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'nothash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1147,8 +1148,8 @@ def test_compare_spot_check_hashes_handles_incorrect_path_names_from_xxh64sum():
|
||||
'hash2 /bar/wrong/path',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'nothash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'nothash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1200,8 +1201,8 @@ def test_compare_spot_check_hashes_with_xxh64sum_failure_falls_back_to_individua
|
||||
).once()
|
||||
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'hash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'hash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1243,8 +1244,8 @@ def test_compare_spot_check_hashes_returns_relative_paths_having_failing_hashes(
|
||||
'hash2 bar',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'nothash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'nothash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1286,8 +1287,8 @@ def test_compare_spot_check_hashes_handles_data_sample_percentage_above_100():
|
||||
'hash2 /bar',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'nothash1', 'path': 'foo'},
|
||||
{'xxh64': 'nothash2', 'path': 'bar'},
|
||||
{'xxh64': 'nothash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'nothash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1329,8 +1330,8 @@ def test_compare_spot_check_hashes_uses_xxh64sum_command_option():
|
||||
working_directory=None,
|
||||
).and_yield('hash1 /foo', 'hash2 /bar')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'nothash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'nothash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1369,7 +1370,7 @@ def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_ma
|
||||
'hash2 /bar',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'}
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''}
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1391,6 +1392,50 @@ def test_compare_spot_check_hashes_considers_path_missing_from_archive_as_not_ma
|
||||
) == ('/bar',)
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_skips_hardlink_path_in_archive():
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=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').and_return(False)
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output',
|
||||
).with_args(('xxh64sum', '/foo', '/bar', '/link'), working_directory=None).and_yield(
|
||||
'hash1 /foo',
|
||||
'hash2 /bar',
|
||||
'hash1 /link',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'hash2', 'path': 'bar', 'linktarget': ''},
|
||||
{'xxh64': '', 'path': 'link', 'linktarget': 'foo'},
|
||||
)
|
||||
|
||||
assert (
|
||||
module.compare_spot_check_hashes(
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
config={
|
||||
'checks': [
|
||||
{
|
||||
'name': 'spot',
|
||||
'data_sample_percentage': 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
local_borg_version=flexmock(),
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
source_paths=('/foo', '/bar', '/link'),
|
||||
)
|
||||
== ()
|
||||
)
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_considers_symlink_path_as_not_matching():
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count]),
|
||||
@@ -1405,8 +1450,45 @@ def test_compare_spot_check_hashes_considers_symlink_path_as_not_matching():
|
||||
'execute_command_and_capture_output',
|
||||
).with_args(('xxh64sum', '/foo'), working_directory=None).and_yield('hash1 /foo')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'hash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'hash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
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_borg_2_symlink_path_as_not_matching():
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=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_yield('hash1 /foo')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'target': ''},
|
||||
{'xxh64': 'hash2', 'path': 'bar', 'target': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1442,8 +1524,8 @@ def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching()
|
||||
'execute_command_and_capture_output',
|
||||
).with_args(('xxh64sum', '/foo'), working_directory=None).and_yield('hash1 /foo')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'hash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'hash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1488,11 +1570,11 @@ def test_compare_spot_check_hashes_with_too_many_paths_feeds_them_to_commands_in
|
||||
'hash4 /quux',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'hash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'hash2', 'path': 'bar', 'linktarget': ''},
|
||||
).and_yield(
|
||||
{'xxh64': 'hash3', 'path': 'baz'},
|
||||
{'xxh64': 'nothash4', 'path': 'quux'},
|
||||
{'xxh64': 'hash3', 'path': 'baz', 'linktarget': ''},
|
||||
{'xxh64': 'nothash4', 'path': 'quux', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
@@ -1535,8 +1617,8 @@ def test_compare_spot_check_hashes_uses_working_directory_to_access_source_paths
|
||||
'hash2 bar',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo'},
|
||||
{'xxh64': 'nothash2', 'path': 'bar'},
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'linktarget': ''},
|
||||
{'xxh64': 'nothash2', 'path': 'bar', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions import diff as module
|
||||
|
||||
|
||||
def test_run_diff_calls_borg_diff():
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
|
||||
'archive'
|
||||
).and_return('archive2')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
flexmock(module.borgmatic.borg.diff).should_receive('diff').once()
|
||||
|
||||
module.borgmatic.actions.diff.run_diff(
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
archive='archive',
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive=None,
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
|
||||
def test_run_diff_with_only_patterns():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').once().and_return(
|
||||
[]
|
||||
)
|
||||
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').once().and_return(
|
||||
[]
|
||||
)
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
|
||||
'archive'
|
||||
).and_return('archive2')
|
||||
flexmock(module.borgmatic.borg.diff).should_receive('diff').once()
|
||||
|
||||
module.borgmatic.actions.diff.run_diff(
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
archive='archive',
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive=None,
|
||||
only_patterns=True,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
@@ -521,6 +521,7 @@ def test_collect_dumps_from_archive_with_dumps_metadata_parses_it():
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
|
||||
flexmock(stdout=flexmock(read=lambda: b''))
|
||||
)
|
||||
flexmock(module.locale).should_receive('getpreferredencoding').and_return('UTF-8')
|
||||
dumps_metadata = [
|
||||
module.Dump('postgresql_databases', 'foo'),
|
||||
module.Dump('postgresql_databases', 'bar', 'host', 1234),
|
||||
|
||||
@@ -357,7 +357,8 @@ DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
|
||||
REPO_ARCHIVE = (f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||
|
||||
|
||||
def test_make_base_create_produces_borg_command():
|
||||
def test_make_base_create_command_checks_root_patterns_exist_and_produces_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist').once()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -386,7 +387,39 @@ def test_make_base_create_produces_borg_command():
|
||||
assert not pattern_file
|
||||
|
||||
|
||||
def test_make_base_create_command_without_check_all_root_patterns_exist_skips_check_and_produces_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',),
|
||||
)
|
||||
flexmock(module).should_receive('validate_planned_backup_paths').and_return(())
|
||||
|
||||
(create_flags, create_positional_arguments, pattern_file) = module.make_base_create_command(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'source_directories_must_exist': False,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
)
|
||||
|
||||
assert create_flags == ('borg', 'create', '--log-json')
|
||||
assert create_positional_arguments == REPO_ARCHIVE
|
||||
assert not pattern_file
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_patterns_file_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
mock_pattern_file = flexmock(name='/tmp/patterns')
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
@@ -424,6 +457,7 @@ def test_make_base_create_command_includes_patterns_file_in_borg_command():
|
||||
|
||||
|
||||
def test_make_base_create_command_with_store_config_false_omits_config_files():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -482,6 +516,7 @@ def test_make_base_create_command_with_store_config_false_omits_config_files():
|
||||
('flags', True, False, ()),
|
||||
('flags', False, True, ('--noflags',)),
|
||||
('flags', False, False, ('--nobsdflags',)),
|
||||
('files_changed', 'mtime', True, ('--files-changed', 'mtime')),
|
||||
('files_cache', 'ctime,size', True, ('--files-cache', 'ctime,size')),
|
||||
('umask', 740, True, ('--umask', '740')),
|
||||
('lock_wait', 5, True, ('--lock-wait', '5')),
|
||||
@@ -493,6 +528,7 @@ def test_make_base_create_command_includes_configuration_option_as_command_flag(
|
||||
feature_available,
|
||||
option_flags,
|
||||
):
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -526,6 +562,7 @@ def test_make_base_create_command_includes_configuration_option_as_command_flag(
|
||||
|
||||
|
||||
def test_make_base_create_command_with_progress_omits_log_json_from_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -560,6 +597,7 @@ def test_make_base_create_command_with_progress_omits_log_json_from_borg_command
|
||||
|
||||
|
||||
def test_make_base_create_command_with_log_json_and_progress_includes_log_json_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -595,6 +633,7 @@ def test_make_base_create_command_with_log_json_and_progress_includes_log_json_i
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_dry_run_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -628,6 +667,7 @@ def test_make_base_create_command_includes_dry_run_in_borg_command():
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_comment_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -662,6 +702,7 @@ def test_make_base_create_command_includes_comment_in_borg_command():
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_local_path_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -695,6 +736,7 @@ def test_make_base_create_command_includes_local_path_in_borg_command():
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_remote_path_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -728,6 +770,7 @@ def test_make_base_create_command_includes_remote_path_in_borg_command():
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_list_flags_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -763,6 +806,7 @@ def test_make_base_create_command_includes_list_flags_in_borg_command():
|
||||
def test_make_base_create_command_with_stream_processes_ignores_read_special_false_and_excludes_special_files():
|
||||
patterns = [Pattern('foo'), Pattern('bar')]
|
||||
patterns_file = flexmock(name='patterns')
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').with_args(
|
||||
patterns,
|
||||
@@ -822,6 +866,7 @@ def test_make_base_create_command_with_stream_processes_ignores_read_special_fal
|
||||
|
||||
|
||||
def test_make_base_create_command_without_patterns_and_with_stream_processes_ignores_read_special_false_and_excludes_special_files():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').with_args(
|
||||
[],
|
||||
@@ -881,6 +926,7 @@ def test_make_base_create_command_without_patterns_and_with_stream_processes_ign
|
||||
|
||||
|
||||
def test_make_base_create_command_with_stream_processes_and_read_special_true_skips_special_files_excludes():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -916,6 +962,7 @@ def test_make_base_create_command_with_stream_processes_and_read_special_true_sk
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_archive_name_format_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -949,6 +996,7 @@ def test_make_base_create_command_includes_archive_name_format_in_borg_command()
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_default_archive_name_format_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -982,6 +1030,7 @@ def test_make_base_create_command_includes_default_archive_name_format_in_borg_c
|
||||
|
||||
def test_make_base_create_command_includes_archive_name_format_with_placeholders_in_borg_command():
|
||||
repository_archive_pattern = 'repo::Documents_{hostname}-{now}'
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
|
||||
@@ -1015,6 +1064,7 @@ def test_make_base_create_command_includes_archive_name_format_with_placeholders
|
||||
|
||||
def test_make_base_create_command_includes_repository_and_archive_name_format_with_placeholders_in_borg_command():
|
||||
repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
flexmock(module.flags).should_receive('get_default_archive_name_format').and_return(
|
||||
@@ -1047,6 +1097,7 @@ def test_make_base_create_command_includes_repository_and_archive_name_format_wi
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_archive_suffix_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -1076,6 +1127,7 @@ def test_make_base_create_command_includes_archive_suffix_in_borg_command():
|
||||
|
||||
|
||||
def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -1116,6 +1168,7 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command():
|
||||
|
||||
|
||||
def test_make_base_create_command_with_unsafe_skip_path_validation_before_create_skips_validation():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
@@ -1145,6 +1198,7 @@ def test_make_base_create_command_with_unsafe_skip_path_validation_before_create
|
||||
|
||||
|
||||
def test_make_base_create_command_without_unsafe_skip_path_validation_before_create_calls_validation():
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('check_all_root_patterns_exist')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
|
||||
@@ -0,0 +1,849 @@
|
||||
import logging
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import diff as module
|
||||
|
||||
LOGGING_ANSWER = flexmock()
|
||||
|
||||
|
||||
def test_diff_calls_borg_with_archives():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_local_path_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg6',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg6',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg6',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_remote_path_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--remote-path',
|
||||
'borg7',
|
||||
'--log-json',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path='borg7',
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_lock_wait_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--lock-wait',
|
||||
'5',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={'lock_wait': 5},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_log_level_info_calls_borg_with_info_flag():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.INFO)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--info',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_log_level_debug_calls_borg_with_debug_flags():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.logger).should_receive('isEnabledFor').and_return(logging.DEBUG)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--debug',
|
||||
'--show-rc',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_only_patterns_calls_borg_with_configured_pattern_paths():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
flexmock(module).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='/tmp/test_patterns')
|
||||
).once()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--patterns-from',
|
||||
'/tmp/test_patterns',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=True,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_exclude_config_calls_borg_with_exclude_flags():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(
|
||||
('--exclude', 'stuff')
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--exclude',
|
||||
'stuff',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_numeric_ids_calls_borg_with_numeric_ids_flag():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--numeric-ids',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={'numeric_ids': True},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_numeric_ids_and_feature_not_available_calls_borg_with_numeric_owner_flag():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').with_args(
|
||||
module.borgmatic.borg.feature.Feature.NUMERIC_IDS, object
|
||||
).and_return(False)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').with_args(
|
||||
module.borgmatic.borg.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, object
|
||||
).and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--numeric-owner',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={'numeric_ids': True},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_same_chunker_params_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--same-chunker-params',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=True,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_sort_keys_calls_borg_with_formatted_sort_by_flags():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--sort-by',
|
||||
'foo,bar,baz',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=['foo', 'bar', 'baz'],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_content_only_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--content-only',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=True,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_extra_borg_options_calls_borg_with_them():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--extra',
|
||||
'--option',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={'extra_borg_options': {'diff': '--extra --option'}},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_without_separate_repository_archive_feature_available_calls_borg_joined_repository_archive():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').with_args(
|
||||
module.borgmatic.borg.feature.Feature.NUMERIC_IDS, object
|
||||
).and_return(True)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').with_args(
|
||||
module.borgmatic.borg.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, object
|
||||
).and_return(False)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').never()
|
||||
flexmock(module.borgmatic.borg.flags).should_receive(
|
||||
'make_repository_archive_flags'
|
||||
).and_return(('repo::archive',))
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'repo::archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
@@ -90,11 +90,6 @@ def test_make_environment_without_configuration_sets_certain_environment_variabl
|
||||
assert environment == {
|
||||
'USER': 'root',
|
||||
'BORG_EXIT_CODES': 'modern',
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'NO',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'NO',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'NO',
|
||||
'BORG_DEBUG_PASSPHRASE': 'NO',
|
||||
'BORG_DISPLAY_PASSPHRASE': 'NO',
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +102,7 @@ def test_make_environment_without_configuration_passes_through_default_environme
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'yup',
|
||||
'BORG_DEBUG_PASSPHRASE': 'nah',
|
||||
'BORG_DISPLAY_PASSPHRASE': 'yup',
|
||||
'BORG_MSGPACK_VERSION_CHECK': 'yup',
|
||||
},
|
||||
)
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
@@ -122,6 +118,7 @@ def test_make_environment_without_configuration_passes_through_default_environme
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'yup',
|
||||
'BORG_DEBUG_PASSPHRASE': 'nah',
|
||||
'BORG_DISPLAY_PASSPHRASE': 'yup',
|
||||
'BORG_MSGPACK_VERSION_CHECK': 'yup',
|
||||
'BORG_EXIT_CODES': 'modern',
|
||||
}
|
||||
|
||||
|
||||
@@ -246,10 +246,10 @@ def test_make_match_archives_flags_accepts_default_archive_name_format():
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_without_archive_flags_bails():
|
||||
def test_warn_for_aggressive_archive_flags_without_archive_flags_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--do-stuff'), '{}')
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--do-stuff'), ())
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_zero_archives_warns():
|
||||
@@ -257,7 +257,7 @@ def test_warn_for_aggressive_archive_flags_with_glob_archives_and_zero_archives_
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--glob-archives', 'foo*'),
|
||||
'{"archives": []}',
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
@@ -266,7 +266,16 @@ def test_warn_for_aggressive_archive_flags_with_match_archives_and_zero_archives
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'),
|
||||
'{"archives": []}',
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_match_archives_and_just_exit_code_warns():
|
||||
flexmock(module.logger).should_receive('warning').twice()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'),
|
||||
('terminating with success status, rc 0',),
|
||||
)
|
||||
|
||||
|
||||
@@ -275,7 +284,7 @@ def test_warn_for_aggressive_archive_flags_with_glob_archives_and_one_archive_do
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--glob-archives', 'foo*'),
|
||||
'{"archives": [{"name": "foo"]}',
|
||||
('this is an archive line',),
|
||||
)
|
||||
|
||||
|
||||
@@ -284,22 +293,10 @@ def test_warn_for_aggressive_archive_flags_with_match_archives_and_one_archive_d
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'),
|
||||
'{"archives": [{"name": "foo"]}',
|
||||
('this is an archive line',),
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_invalid_json_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--glob-archives', 'foo*'), '{"archives": [}')
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_json_missing_archives_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--glob-archives', 'foo*'), '{}')
|
||||
|
||||
|
||||
def test_omit_flag_removes_flag_from_arguments():
|
||||
assert module.omit_flag(('borg', 'create', '--flag', '--other'), '--flag') == (
|
||||
'borg',
|
||||
|
||||
@@ -565,15 +565,14 @@ def test_make_info_command_with_date_based_matching_passes_through_to_command():
|
||||
)
|
||||
|
||||
|
||||
def test_display_archives_info_calls_two_commands():
|
||||
def test_display_archives_info_calls_borg_command():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_info_command')
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield().once()
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
|
||||
module.display_archives_info(
|
||||
repository_path='repo',
|
||||
@@ -592,7 +591,6 @@ def test_display_archives_info_with_json_calls_json_command_only():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('{}')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.display_archives_info(
|
||||
@@ -616,20 +614,13 @@ def test_display_archives_info_calls_borg_with_working_directory():
|
||||
)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
full_command=object,
|
||||
output_log_level=int,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).and_yield().once()
|
||||
).and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
full_command=object,
|
||||
output_log_level=object,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).once()
|
||||
|
||||
module.display_archives_info(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -115,6 +115,7 @@ def test_get_latest_archive_calls_borg_with_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -144,6 +145,7 @@ def test_get_latest_archive_with_log_info_calls_borg_without_info_flag():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -174,6 +176,7 @@ def test_get_latest_archive_with_log_debug_calls_borg_without_debug_flag():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -204,6 +207,7 @@ def test_get_latest_archive_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -234,6 +238,7 @@ def test_get_latest_archive_with_exit_codes_calls_borg_using_them():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -267,6 +272,7 @@ def test_get_latest_archive_with_remote_path_calls_borg_with_remote_path_flags()
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -300,6 +306,7 @@ def test_get_latest_archive_with_umask_calls_borg_with_umask_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -329,6 +336,7 @@ def test_get_latest_archive_without_archives_raises():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -358,6 +366,7 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -380,6 +389,38 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
)
|
||||
|
||||
|
||||
def test_get_latest_archive_with_match_archives_calls_borg_with_match_archives_flags():
|
||||
expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(
|
||||
('--match-archives', 'foo')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'list', '--log-json', '--match-archives', 'foo', *BORG_LIST_LATEST_ARGUMENTS),
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_yield(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
config={'match_archives': 'foo'},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
)
|
||||
== expected_archive
|
||||
)
|
||||
|
||||
|
||||
def test_get_latest_archive_calls_borg_with_list_extra_borg_options():
|
||||
expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
@@ -387,6 +428,7 @@ def test_get_latest_archive_calls_borg_with_list_extra_borg_options():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -424,6 +466,7 @@ def test_get_latest_archive_with_feature_available_calls_borg_with_repo_list_ext
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -464,6 +507,7 @@ def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_c
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -497,6 +541,7 @@ def test_get_latest_archive_with_consider_checkpoints_and_feature_available_call
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -527,6 +572,7 @@ def test_get_latest_archive_calls_borg_with_working_directory():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
@@ -1108,7 +1154,7 @@ def test_make_repo_list_command_with_match_archives_calls_borg_with_match_archiv
|
||||
assert command == ('borg', 'list', '--log-json', '--match-archives', 'foo-*', 'repo')
|
||||
|
||||
|
||||
def test_list_repository_calls_two_commands():
|
||||
def test_list_repository_calls_borg_command():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_repo_list_command')
|
||||
@@ -1116,7 +1162,6 @@ def test_list_repository_calls_two_commands():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
|
||||
module.list_repository(
|
||||
repository_path='repo',
|
||||
@@ -1127,14 +1172,13 @@ def test_list_repository_calls_two_commands():
|
||||
)
|
||||
|
||||
|
||||
def test_list_repository_with_json_calls_json_command_only():
|
||||
def test_list_repository_with_json_calls_borg_json_command_only():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module).should_receive('make_repo_list_command')
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('{}')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.list_repository(
|
||||
@@ -1206,20 +1250,13 @@ def test_list_repository_calls_borg_with_working_directory():
|
||||
)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
full_command=object,
|
||||
output_log_level=int,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
full_command=object,
|
||||
output_log_level=object,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).once()
|
||||
|
||||
module.list_repository(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -329,10 +329,12 @@ def test_parse_arguments_for_actions_consumes_action_arguments_after_action_name
|
||||
remaining = flexmock()
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or remaining,
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or remaining
|
||||
),
|
||||
)
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {'action': flexmock(), 'other': flexmock()}
|
||||
@@ -355,10 +357,12 @@ def test_parse_arguments_for_actions_consumes_action_arguments_with_alias():
|
||||
remaining = flexmock()
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{canonical or action: action_namespace},
|
||||
)
|
||||
or remaining,
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{canonical or action: action_namespace},
|
||||
)
|
||||
or remaining
|
||||
),
|
||||
)
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
@@ -387,10 +391,12 @@ def test_parse_arguments_for_actions_consumes_multiple_action_arguments():
|
||||
other_namespace = flexmock(bar=3)
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: action_namespace if action == 'action' else other_namespace},
|
||||
)
|
||||
or (),
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: action_namespace if action == 'action' else other_namespace},
|
||||
)
|
||||
or ()
|
||||
),
|
||||
).and_return(('other', '--bar', '3')).and_return('action', '--foo', 'true')
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
@@ -420,10 +426,12 @@ def test_parse_arguments_for_actions_respects_command_line_action_ordering():
|
||||
action_namespace = flexmock(foo=True)
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: other_namespace if action == 'other' else action_namespace},
|
||||
)
|
||||
or (),
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: other_namespace if action == 'other' else action_namespace},
|
||||
)
|
||||
or ()
|
||||
),
|
||||
).and_return(('action',)).and_return(('other', '--foo', 'true'))
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
@@ -458,10 +466,12 @@ def test_parse_arguments_for_actions_applies_default_action_parsers():
|
||||
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: namespaces.get(action)},
|
||||
)
|
||||
or (),
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: namespaces.get(action)},
|
||||
)
|
||||
or ()
|
||||
),
|
||||
).and_return(())
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
@@ -488,10 +498,12 @@ def test_parse_arguments_for_actions_consumes_global_arguments():
|
||||
action_namespace = flexmock()
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or ('--verbosity', 'lots'),
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or ('--verbosity', 'lots')
|
||||
),
|
||||
)
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
@@ -516,10 +528,12 @@ def test_parse_arguments_for_actions_passes_through_unknown_arguments_before_act
|
||||
action_namespace = flexmock()
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or ('--wtf', 'yes'),
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or ('--wtf', 'yes')
|
||||
),
|
||||
)
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
@@ -544,10 +558,12 @@ def test_parse_arguments_for_actions_passes_through_unknown_arguments_after_acti
|
||||
action_namespace = flexmock()
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or ('--wtf', 'yes'),
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or ('--wtf', 'yes')
|
||||
),
|
||||
)
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
@@ -572,10 +588,12 @@ def test_parse_arguments_for_actions_with_borg_action_skips_other_action_parsers
|
||||
action_namespace = flexmock(options=[])
|
||||
flexmock(module).should_receive('get_subaction_parsers').and_return({})
|
||||
flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
|
||||
lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or (),
|
||||
lambda unparsed, parsed, parser, action, canonical=None: (
|
||||
parsed.update(
|
||||
{action: action_namespace},
|
||||
)
|
||||
or ()
|
||||
),
|
||||
).and_return(())
|
||||
flexmock(module).should_receive('get_subactions_for_actions').and_return({})
|
||||
action_parsers = {
|
||||
|
||||
@@ -2058,6 +2058,43 @@ def test_collect_highlander_action_summary_logs_error_on_run_validate_failure():
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_collect_highlander_action_summary_logs_nothing_additional_for_success_with_show():
|
||||
flexmock(module.borgmatic.actions.config.show).should_receive('run_show')
|
||||
arguments = {
|
||||
'show': flexmock(),
|
||||
'global': flexmock(),
|
||||
}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_highlander_action_summary_logs(
|
||||
{'test.yaml': {}},
|
||||
arguments=arguments,
|
||||
configuration_parse_errors=False,
|
||||
),
|
||||
)
|
||||
assert not logs
|
||||
|
||||
|
||||
def test_collect_highlander_action_summary_logs_error_on_run_show_failure():
|
||||
flexmock(module.borgmatic.actions.config.show).should_receive('run_show').and_raise(
|
||||
ValueError,
|
||||
)
|
||||
arguments = {
|
||||
'show': flexmock(),
|
||||
'global': flexmock(),
|
||||
}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_highlander_action_summary_logs(
|
||||
{'test.yaml': {}},
|
||||
arguments=arguments,
|
||||
configuration_parse_errors=False,
|
||||
),
|
||||
)
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
@@ -2545,23 +2582,40 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
||||
|
||||
def test_check_and_show_help_on_no_args_shows_help_when_no_args_and_default_actions_false():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
|
||||
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
|
||||
flexmock(module).should_receive('parse_arguments').with_args({}, '--help').once()
|
||||
flexmock(module.sys).should_receive('exit').with_args(0).once()
|
||||
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
|
||||
|
||||
module.check_and_show_help_on_no_args(
|
||||
configs={'test.yaml': {'default_actions': False}}, schema={}
|
||||
)
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_does_not_show_help_when_no_args_and_no_configurations():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
|
||||
module.check_and_show_help_on_no_args(configs={}, schema={})
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_does_not_show_help_when_no_args_and_default_actions_true():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': True}})
|
||||
|
||||
module.check_and_show_help_on_no_args(
|
||||
configs={'test.yaml': {'default_actions': True}}, schema={}
|
||||
)
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_does_not_show_help_when_args_provided():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic', '--create'])
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
|
||||
|
||||
module.check_and_show_help_on_no_args(
|
||||
configs={'test.yaml': {'default_actions': False}}, schema={}
|
||||
)
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_with_no_default_actions_in_all_configs():
|
||||
@@ -2574,10 +2628,10 @@ def test_check_and_show_help_on_no_args_with_no_default_actions_in_all_configs()
|
||||
}
|
||||
|
||||
# Expect help to be shown
|
||||
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
|
||||
flexmock(module).should_receive('parse_arguments').with_args({}, '--help').once()
|
||||
flexmock(module.sys).should_receive('exit').with_args(0).once()
|
||||
|
||||
module.check_and_show_help_on_no_args(configs)
|
||||
module.check_and_show_help_on_no_args(configs=configs, schema={})
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_with_conflicting_configs():
|
||||
@@ -2593,7 +2647,7 @@ def test_check_and_show_help_on_no_args_with_conflicting_configs():
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
|
||||
module.check_and_show_help_on_no_args(configs)
|
||||
module.check_and_show_help_on_no_args(configs=configs, schema={})
|
||||
|
||||
|
||||
def test_get_singular_option_value_with_conflicting_values_exits():
|
||||
@@ -2652,3 +2706,27 @@ def test_get_singular_option_value_with_no_config_returns_none():
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_diff():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module).should_receive('get_skip_actions').and_return([])
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
|
||||
flexmock(borgmatic.actions.diff).should_receive('run_diff').once()
|
||||
|
||||
tuple(
|
||||
module.run_actions(
|
||||
arguments={'global': flexmock(dry_run=False), 'diff': flexmock()},
|
||||
config_filename=flexmock(),
|
||||
config={'repositories': []},
|
||||
config_paths=[],
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -47,11 +47,6 @@ from borgmatic.config import normalize as module
|
||||
{'prefix': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'location': {'prefix': 'foo'}, 'consistency': {'prefix': 'foo'}},
|
||||
{'prefix': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'location': {'prefix': 'foo'}, 'consistency': {'bar': 'baz'}},
|
||||
{'prefix': 'foo', 'bar': 'baz'},
|
||||
@@ -62,11 +57,6 @@ from borgmatic.config import normalize as module
|
||||
{'umask': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'storage': {'umask': 'foo'}, 'hooks': {'umask': 'foo'}},
|
||||
{'umask': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'storage': {'umask': 'foo'}, 'hooks': {'bar': 'baz'}},
|
||||
{'umask': 'foo', 'bar': 'baz'},
|
||||
@@ -288,11 +278,6 @@ def test_normalize_commands_moves_individual_command_hooks_to_unified_commands(
|
||||
{'checks': [{'name': 'archives'}]},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'checks': ['archives']},
|
||||
{'checks': [{'name': 'archives'}]},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'numeric_owner': False},
|
||||
{'numeric_ids': False},
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks.data_source import bootstrap as module
|
||||
|
||||
|
||||
def test_dump_data_sources_creates_manifest_file():
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
def test_resolve_config_path_symlinks_passes_through_non_symlink():
|
||||
flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('islink').and_return(False)
|
||||
|
||||
assert tuple(module.resolve_config_path_symlinks('test.yaml')) == ('test.yaml',)
|
||||
|
||||
|
||||
def test_resolve_config_path_symlinks_follows_each_symlink():
|
||||
flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('test.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest1.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(False)
|
||||
flexmock(module.os).should_receive('readlink').with_args('test.yaml').and_return('dest1.yaml')
|
||||
flexmock(module.os).should_receive('readlink').with_args('dest1.yaml').and_return('dest2.yaml')
|
||||
flexmock(module.os).should_receive('readlink').with_args('dest2.yaml').never()
|
||||
|
||||
assert tuple(module.resolve_config_path_symlinks('test.yaml')) == (
|
||||
'test.yaml',
|
||||
'dest1.yaml',
|
||||
'dest2.yaml',
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_config_path_symlinks_with_too_many_symlinks_raises():
|
||||
flexmock(module).MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW = 2
|
||||
flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('test.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest1.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest3.yaml').never()
|
||||
flexmock(module.os).should_receive('readlink').with_args('test.yaml').and_return('dest1.yaml')
|
||||
flexmock(module.os).should_receive('readlink').with_args('dest1.yaml').and_return('dest2.yaml')
|
||||
flexmock(module.os).should_receive('readlink').with_args('dest2.yaml').and_return('dest3.yaml')
|
||||
flexmock(module.os).should_receive('readlink').with_args('dest3.yaml').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert tuple(module.resolve_config_path_symlinks('test.yaml'))
|
||||
|
||||
|
||||
def test_dump_data_sources_creates_manifest_file():
|
||||
flexmock(module).should_receive('resolve_config_path_symlinks').and_yield(
|
||||
'test.yaml', 'linkdest.yaml'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
|
||||
manifest_file = flexmock(
|
||||
__enter__=lambda *args: flexmock(write=lambda *args: None, close=lambda *args: None),
|
||||
@@ -19,7 +61,7 @@ def test_dump_data_sources_creates_manifest_file():
|
||||
encoding='utf-8',
|
||||
).and_return(manifest_file)
|
||||
flexmock(module.json).should_receive('dump').with_args(
|
||||
{'borgmatic_version': '1.0.0', 'config_paths': ('test.yaml',)},
|
||||
{'borgmatic_version': '1.0.0', 'config_paths': ('test.yaml', 'linkdest.yaml')},
|
||||
manifest_file,
|
||||
).once()
|
||||
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
|
||||
@@ -34,6 +76,12 @@ def test_dump_data_sources_creates_manifest_file():
|
||||
'test.yaml', source=module.borgmatic.borg.pattern.Pattern_source.HOOK
|
||||
),
|
||||
).once()
|
||||
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
|
||||
object,
|
||||
module.borgmatic.borg.pattern.Pattern(
|
||||
'linkdest.yaml', source=module.borgmatic.borg.pattern.Pattern_source.HOOK
|
||||
),
|
||||
).once()
|
||||
|
||||
module.dump_data_sources(
|
||||
hook_config=None,
|
||||
@@ -46,6 +94,7 @@ def test_dump_data_sources_creates_manifest_file():
|
||||
|
||||
|
||||
def test_dump_data_sources_with_store_config_files_false_does_not_create_manifest_file():
|
||||
flexmock(module).should_receive('resolve_config_path_symlinks').and_yield('test.yaml')
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
flexmock(module.json).should_receive('dump').never()
|
||||
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
|
||||
@@ -62,6 +111,7 @@ def test_dump_data_sources_with_store_config_files_false_does_not_create_manifes
|
||||
|
||||
|
||||
def test_dump_data_sources_with_dry_run_does_not_create_manifest_file():
|
||||
flexmock(module).should_receive('resolve_config_path_symlinks').and_yield('test.yaml')
|
||||
flexmock(module.os).should_receive('makedirs').never()
|
||||
flexmock(module.json).should_receive('dump').never()
|
||||
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
|
||||
|
||||
@@ -50,10 +50,7 @@ def test_loki_log_buffer_json_serializes_log_lines():
|
||||
assert json.loads(buffer.to_request())['streams'][0]['values'][0][1] == 'Some test log line'
|
||||
|
||||
|
||||
def test_loki_log_handler_add_label_gets_labels():
|
||||
'''
|
||||
Assert that adding labels works.
|
||||
'''
|
||||
def test_loki_log_buffer_add_label_gets_labels():
|
||||
buffer = module.Loki_log_buffer(flexmock(), dry_run=False)
|
||||
|
||||
buffer.add_label('test', 'label')
|
||||
@@ -64,22 +61,20 @@ def test_loki_log_handler_add_label_gets_labels():
|
||||
|
||||
|
||||
def test_loki_log_handler_emit_with_send_logs_records_log_message():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, log_level=10, dry_run=False)
|
||||
flexmock(handler).should_receive('raw').once()
|
||||
|
||||
handler.emit(flexmock(getMessage=lambda: 'Some test log line'))
|
||||
|
||||
|
||||
def test_loki_log_handler_emit_without_send_logs_skips_log_message():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, log_level=10, dry_run=False)
|
||||
flexmock(handler).should_receive('raw').never()
|
||||
|
||||
handler.emit(flexmock(getMessage=lambda: 'Some test log line'))
|
||||
|
||||
|
||||
def test_loki_log_handler_flush_with_empty_buffer_does_not_raise():
|
||||
'''
|
||||
Test that flushing an empty buffer does indeed nothing.
|
||||
'''
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, log_level=10, dry_run=False)
|
||||
|
||||
handler.flush()
|
||||
|
||||
+126
-26
@@ -31,8 +31,6 @@ def test_command_is_borg_matches_local_path_to_command(command, borg_local_path,
|
||||
(False, 1, None, module.Exit_status.ERROR),
|
||||
(True, 1, None, module.Exit_status.WARNING),
|
||||
(False, 100, None, module.Exit_status.ERROR),
|
||||
(False, 2, None, module.Exit_status.ERROR),
|
||||
(True, 2, None, module.Exit_status.ERROR),
|
||||
(True, 100, None, module.Exit_status.WARNING),
|
||||
(False, 0, None, module.Exit_status.SUCCESS),
|
||||
(True, 0, None, module.Exit_status.SUCCESS),
|
||||
@@ -62,8 +60,8 @@ def test_command_is_borg_matches_local_path_to_command(command, borg_local_path,
|
||||
(True, 105, [{'code': 105, 'treat_as': 'warning'}], module.Exit_status.WARNING),
|
||||
(True, 106, [], module.Exit_status.ERROR),
|
||||
(True, 106, [{'code': 106, 'treat_as': 'warning'}], module.Exit_status.WARNING),
|
||||
(True, 107, [], module.Exit_status.ERROR),
|
||||
(True, 107, [{'code': 107, 'treat_as': 'warning'}], module.Exit_status.WARNING),
|
||||
(True, 107, [], module.Exit_status.WARNING),
|
||||
(True, 107, [{'code': 107, 'treat_as': 'error'}], module.Exit_status.ERROR),
|
||||
),
|
||||
)
|
||||
def test_interpret_exit_code_respects_exit_code_and_borg_local_path(
|
||||
@@ -405,7 +403,7 @@ def test_log_buffer_lines_with_ready_buffer_and_running_process_handles_each_log
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -437,7 +435,7 @@ def test_log_buffer_lines_with_ready_buffer_and_capture_process_yields_each_line
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_does_not_yield_each_line():
|
||||
def test_log_buffer_lines_with_ready_buffer_and_same_log_level_and_capture_process_yields_each_line():
|
||||
process = flexmock(poll=lambda: None, stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(lines=iter((('hi', 'there'),)), process=process)
|
||||
@@ -448,7 +446,31 @@ def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_do
|
||||
).and_return(list(buffer_readers.keys()), [], [])
|
||||
flexmock(module).should_receive('parse_log_line').and_return(flexmock())
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=10, getMessage=lambda: 'message')
|
||||
flexmock(levelno=module.logging.INFO, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_higher_log_level_and_capture_process_does_not_yield_each_line():
|
||||
process = flexmock(poll=lambda: None, stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(lines=iter((('hi', 'there'),)), process=process)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module.select).should_receive('select').with_args(
|
||||
buffer_readers.keys(), [], []
|
||||
).and_return(list(buffer_readers.keys()), [], [])
|
||||
flexmock(module).should_receive('parse_log_line').and_return(flexmock())
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.DEBUG, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert (
|
||||
@@ -456,7 +478,7 @@ def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_do
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -464,6 +486,30 @@ def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_do
|
||||
)
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_lower_log_level_and_capture_process_yields_each_line():
|
||||
process = flexmock(poll=lambda: None, stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(lines=iter((('hi', 'there'),)), process=process)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module.select).should_receive('select').with_args(
|
||||
buffer_readers.keys(), [], []
|
||||
).and_return(list(buffer_readers.keys()), [], [])
|
||||
flexmock(module).should_receive('parse_log_line').and_return(flexmock())
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.ERROR, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_finished_process_vents_other_processes():
|
||||
process_stdout = flexmock()
|
||||
process = flexmock(poll=lambda: 0, stdout=process_stdout, stderr=flexmock(), args=flexmock())
|
||||
@@ -487,7 +533,7 @@ def test_log_buffer_lines_with_ready_buffer_and_finished_process_vents_other_pro
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -524,7 +570,7 @@ def test_log_buffer_lines_with_ready_buffer_and_finished_process_does_not_vent_o
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -550,7 +596,7 @@ def test_log_buffer_lines_with_ready_eof_buffer_and_running_process_skips_it():
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -573,7 +619,7 @@ def test_log_buffer_lines_with_ready_buffer_with_empty_line_skips_it():
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -603,7 +649,7 @@ def test_log_buffer_lines_with_multiple_ready_buffers_and_running_processes_hand
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -631,7 +677,7 @@ def test_log_buffer_lines_with_multiple_ready_buffers_from_same_running_process_
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -659,7 +705,7 @@ def test_log_buffer_lines_with_ready_stderr_buffer_and_running_process_elevates_
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -687,7 +733,7 @@ def test_log_buffer_lines_with_ready_stdout_buffer_and_running_process_does_not_
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -715,7 +761,7 @@ def test_log_buffer_lines_with_ready_stderr_buffer_and_capture_stderr_does_not_e
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
capture_stderr=True,
|
||||
)
|
||||
@@ -995,7 +1041,7 @@ def test_log_remaining_buffer_lines_without_reader_process_bails():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1022,7 +1068,7 @@ def test_log_remaining_buffer_lines_logs_each_line():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1053,7 +1099,7 @@ def test_log_remaining_buffer_lines_with_multiple_buffers_logs_lines_from_each()
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1088,7 +1134,7 @@ def test_log_remaining_buffer_lines_with_stderr_buffer_elevates_stderr():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1120,7 +1166,7 @@ def test_log_remaining_buffer_lines_with_stderr_buffer_and_capture_stderr_does_n
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
capture_stderr=True,
|
||||
)
|
||||
@@ -1149,13 +1195,13 @@ def test_log_remaining_buffer_lines_with_capture_process_yields_each_line():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_yield_each_line():
|
||||
def test_log_remaining_buffer_lines_with_same_log_level_and_capture_process_yields_each_line():
|
||||
process = flexmock(stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(
|
||||
@@ -1168,7 +1214,33 @@ def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_
|
||||
line=str, log_level=object, elevate_stderr=False, borg_local_path=object, command=object
|
||||
).and_return(flexmock()).twice()
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=10, getMessage=lambda: 'message')
|
||||
flexmock(levelno=module.logging.INFO, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_remaining_buffer_lines_with_higher_log_level_and_capture_process_does_not_yield_each_line():
|
||||
process = flexmock(stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(
|
||||
lines=(('hi', 'there'),),
|
||||
process=process,
|
||||
)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module).should_receive('parse_log_line').with_args(
|
||||
line=str, log_level=object, elevate_stderr=False, borg_local_path=object, command=object
|
||||
).and_return(flexmock()).twice()
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.DEBUG, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert (
|
||||
@@ -1176,7 +1248,7 @@ def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1184,6 +1256,32 @@ def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_
|
||||
)
|
||||
|
||||
|
||||
def test_log_remaining_buffer_lines_with_lower_log_level_and_capture_process_does_yields_each_line():
|
||||
process = flexmock(stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(
|
||||
lines=(('hi', 'there'),),
|
||||
process=process,
|
||||
)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module).should_receive('parse_log_line').with_args(
|
||||
line=str, log_level=object, elevate_stderr=False, borg_local_path=object, command=object
|
||||
).and_return(flexmock()).twice()
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.ERROR, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_mask_command_secrets_masks_password_flag_value():
|
||||
assert module.mask_command_secrets(('cooldb', '--username', 'bob', '--password', 'pass')) == (
|
||||
'cooldb',
|
||||
@@ -1510,6 +1608,7 @@ def test_execute_command_and_capture_output_returns_output_when_process_error_is
|
||||
flexmock(module).should_receive('interpret_exit_code').and_return(
|
||||
module.Exit_status.SUCCESS,
|
||||
).once()
|
||||
flexmock(module.locale).should_receive('getpreferredencoding').and_return('UTF-8')
|
||||
|
||||
output_lines = tuple(module.execute_command_and_capture_output(full_command))
|
||||
|
||||
@@ -1533,6 +1632,7 @@ def test_execute_command_and_capture_output_raises_when_command_errors():
|
||||
flexmock(module).should_receive('interpret_exit_code').and_return(
|
||||
module.Exit_status.ERROR,
|
||||
).once()
|
||||
flexmock(module.locale).should_receive('getpreferredencoding').and_return('UTF-8')
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
tuple(module.execute_command_and_capture_output(full_command))
|
||||
|
||||
Reference in New Issue
Block a user