From 704db9c9d72746d4b9ea5471bb6ddfdf5a360751 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 22 May 2026 14:12:46 -0700 Subject: [PATCH 1/3] Add a "--quick-stats" flag and corresponding "quick_statistics" option (#1309). --- NEWS | 2 + borgmatic/borg/create.py | 9 +++- borgmatic/commands/arguments.py | 7 ++++ borgmatic/config/schema.yaml | 9 ++++ tests/unit/borg/test_create.py | 74 +++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index fe7a74a3..99a3be21 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,8 @@ * #1303: For the MariaDB hook, include only a subset of system data when dumping the "mysql" system database (or "all" databases), so the dump is actually restorable. See the documentation for more information: https://torsion.org/borgmatic/reference/configuration/data-sources/mariadb/ + * #1309: Add a "--quick-stats" flag and corresponding "quick_statistics" option for showing only + abbreviated statistics for the "create" action. Borg 1.4.5+ only. * Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the documentation for more information: https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/ diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 8e3e9820..1ed7b4dd 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -374,7 +374,9 @@ def create_archive( if json: output_log_level = None - elif config.get('list_details') or (config.get('statistics') and not dry_run): + elif config.get('list_details') or ( + (config.get('statistics') or config.get('quick_statistics')) and not dry_run + ): output_log_level = logging.ANSWER else: output_log_level = logging.INFO @@ -386,6 +388,11 @@ def create_archive( create_flags += ( (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ()) + (('--stats',) if config.get('statistics') and not json and not dry_run else ()) + + ( + ('--quick-stats',) + if config.get('quick_statistics') and not json and not dry_run + else () + ) + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ()) + (('--progress',) if config.get('progress') else ()) + (('--json',) if json else ()) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index bce78774..13740769 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -896,6 +896,13 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915 action='store_true', help='Display statistics of archive', ) + create_group.add_argument( + '--quick-stats', + dest='quick_statistics', + default=None, + action='store_true', + help='Display statistics of archive, skipping repository-wide "All archives" and chunk index statistics [Borg 1.4.5+ only]', + ) create_group.add_argument( '--list', '--files', diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 09f014b2..3d18c0a5 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1091,6 +1091,15 @@ properties: Corresponds to the "--stats" flag on those actions. Defaults to false. example: true + quick_statistics: + type: boolean + description: | + Display statistics for an archive when running supported actions, + skipping the repository-wide "All archives" and chunk index + statistics to save some time. Corresponds to the "--quick-stats" + flag on those actions. Defaults to false. (This option is supported + for Borg 1.4.5+ only.) + example: true list_details: type: boolean description: | diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index 2e21e648..199385ff 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -1477,6 +1477,44 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats(): 'source_directories': ['foo', 'bar'], 'repositories': ['repo'], 'exclude_patterns': None, + 'statistics': True, + }, + patterns=[Pattern('foo'), Pattern('bar')], + local_borg_version='1.2.3', + global_arguments=flexmock(), + borgmatic_runtime_directory='/borgmatic/run', + ) + + +def test_create_archive_with_quick_stats_and_dry_run_calls_borg_without_quick_stats(): + # --dry-run and --quick-stats are mutually exclusive, see: + # https://borgbackup.readthedocs.io/en/stable/usage/create.html#description + flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') + flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER + flexmock(module).should_receive('make_base_create_command').and_return( + (('borg', 'create', '--dry-run'), REPO_ARCHIVE, flexmock()), + ) + 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').with_args( + ('borg', 'create', '--dry-run', '--info', *REPO_ARCHIVE), + output_log_level=logging.INFO, + output_file=None, + borg_local_path='borg', + borg_exit_codes=None, + working_directory=None, + environment=None, + ) + insert_logging_mock(logging.INFO) + + module.create_archive( + dry_run=True, + repository_path='repo', + config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'exclude_patterns': None, + 'quick_statistics': True, }, patterns=[Pattern('foo'), Pattern('bar')], local_borg_version='1.2.3', @@ -1806,6 +1844,42 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag(): 'source_directories': ['foo*'], 'repositories': ['repo'], 'exclude_patterns': None, + 'statistics': True, + }, + patterns=[Pattern('foo'), Pattern('bar')], + local_borg_version='1.2.3', + global_arguments=flexmock(), + borgmatic_runtime_directory='/borgmatic/run', + json=True, + ) + + assert json_output == '[]' + + +def test_create_archive_with_quick_stats_and_json_calls_borg_without_quick_stats_flag(): + flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels') + flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER + flexmock(module).should_receive('make_base_create_command').and_return( + (('borg', 'create'), REPO_ARCHIVE, flexmock()), + ) + 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', 'create', '--json', *REPO_ARCHIVE), + working_directory=None, + environment=None, + borg_local_path='borg', + borg_exit_codes=None, + ).and_yield('[]') + + json_output = module.create_archive( + dry_run=False, + repository_path='repo', + config={ + 'source_directories': ['foo*'], + 'repositories': ['repo'], + 'exclude_patterns': None, + 'quick_statistics': True, }, patterns=[Pattern('foo'), Pattern('bar')], local_borg_version='1.2.3', From 7e7c3c273e555e304e8763ac4a0ea2053f8362e2 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 22 May 2026 15:25:42 -0700 Subject: [PATCH 2/3] Update the Apprise monitoring hook's "url" option to support loading credentials with the "{credential ...}" syntax (#1308). --- NEWS | 3 +++ borgmatic/config/schema.yaml | 4 +++- borgmatic/hooks/monitoring/apprise.py | 8 +++++++- .../configuration/monitoring/apprise.md | 6 +++--- tests/unit/hooks/monitoring/test_apprise.py | 20 ++++++++++++++----- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/NEWS b/NEWS index 99a3be21..8afb3c41 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,9 @@ * #1303: For the MariaDB hook, include only a subset of system data when dumping the "mysql" system database (or "all" databases), so the dump is actually restorable. See the documentation for more information: https://torsion.org/borgmatic/reference/configuration/data-sources/mariadb/ + * #1308: Update the Apprise monitoring hook's "url" option to support loading credentials with the + "{credential ...}" syntax. See the documentation for more information: + https://torsion.org/borgmatic/reference/configuration/credentials/ * #1309: Add a "--quick-stats" flag and corresponding "quick_statistics" option for showing only abbreviated statistics for the "create" action. Borg 1.4.5+ only. * Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 3d18c0a5..16f43d5c 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -2834,7 +2834,9 @@ properties: properties: url: type: string - description: URL of this Apprise service. + description: | + URL of this Apprise service. Supports the + "{credential ...}" syntax. example: "gotify://hostname/token" label: type: string diff --git a/borgmatic/hooks/monitoring/apprise.py b/borgmatic/hooks/monitoring/apprise.py index 0280758a..a4e649d2 100644 --- a/borgmatic/hooks/monitoring/apprise.py +++ b/borgmatic/hooks/monitoring/apprise.py @@ -1,6 +1,7 @@ import logging import operator +import borgmatic.hooks.credential.parse import borgmatic.hooks.monitoring.logs import borgmatic.hooks.monitoring.monitor @@ -76,7 +77,12 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev logger.info(f'Pinging Apprise services: {labels_string}{dry_run_string}') apprise_object = apprise.Apprise() - apprise_object.add(list(map(operator.itemgetter('url'), hook_config.get('services')))) + apprise_object.add( + [ + borgmatic.hooks.credential.parse.resolve_credential(service['url'], config) + for service in hook_config.get('services') + ] + ) if dry_run: return diff --git a/docs/reference/configuration/monitoring/apprise.md b/docs/reference/configuration/monitoring/apprise.md index 27b0a7d6..17cd4e84 100644 --- a/docs/reference/configuration/monitoring/apprise.md +++ b/docs/reference/configuration/monitoring/apprise.md @@ -5,9 +5,9 @@ eleventyNavigation: parent: 🚨 Monitoring --- New in version 1.8.4 -[Apprise](https://github.com/caronc/apprise/wiki) is a local notification library +[Apprise](https://appriseit.com/) is a local notification library that "allows you to send a notification to almost all of the most popular -[notification services](https://github.com/caronc/apprise/wiki) available to +[notification services](https://appriseit.com/services/) available to us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc." Depending on how you installed borgmatic, it may not have come with Apprise. @@ -22,7 +22,7 @@ sudo uv tool install borgmatic[Apprise] Omit `sudo` if borgmatic is installed as a non-root user. Once Apprise is installed, configure borgmatic to notify one or more [Apprise -services](https://github.com/caronc/apprise/wiki). For example: +services](https://appriseit.com/services/). For example: ```yaml apprise: diff --git a/tests/unit/hooks/monitoring/test_apprise.py b/tests/unit/hooks/monitoring/test_apprise.py index 4521427a..457d4abe 100644 --- a/tests/unit/hooks/monitoring/test_apprise.py +++ b/tests/unit/hooks/monitoring/test_apprise.py @@ -66,7 +66,9 @@ def test_initialize_monitor_without_send_logs_does_not_add_handler(): def test_ping_monitor_respects_dry_run(): - flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler') + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential' + ).replace_with(lambda url, config: url) flexmock(module.borgmatic.hooks.monitoring.logs).should_receive( 'format_buffered_logs_for_payload', ).and_return('loggy log') @@ -83,7 +85,9 @@ def test_ping_monitor_respects_dry_run(): def test_ping_monitor_with_no_states_does_not_notify(): - flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never() + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential' + ).replace_with(lambda url, config: url) flexmock(module.borgmatic.hooks.monitoring.logs).should_receive( 'format_buffered_logs_for_payload', ).never() @@ -100,7 +104,9 @@ def test_ping_monitor_with_no_states_does_not_notify(): def test_ping_monitor_notifies_fail_by_default(): - flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler') + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential' + ).replace_with(lambda url, config: url) flexmock(module.borgmatic.hooks.monitoring.logs).should_receive( 'format_buffered_logs_for_payload', ).and_return('') @@ -123,7 +129,9 @@ def test_ping_monitor_notifies_fail_by_default(): def test_ping_monitor_with_logs_appends_logs_to_body(): - flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler') + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential' + ).replace_with(lambda url, config: url) flexmock(module.borgmatic.hooks.monitoring.logs).should_receive( 'format_buffered_logs_for_payload', ).and_return('loggy log') @@ -146,7 +154,9 @@ def test_ping_monitor_with_logs_appends_logs_to_body(): def test_ping_monitor_with_finish_default_config_notifies(): - flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler') + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential' + ).replace_with(lambda url, config: url) flexmock(module.borgmatic.hooks.monitoring.logs).should_receive( 'format_buffered_logs_for_payload', ).and_return('') From b1f42eb233d13e3486af810c90213298dc0196bd Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 22 May 2026 20:00:40 -0700 Subject: [PATCH 3/3] Upgrade dependencies. --- binary_requirements.txt | 8 ++++---- test_requirements.txt | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/binary_requirements.txt b/binary_requirements.txt index ed52f8e1..78c1bfe0 100644 --- a/binary_requirements.txt +++ b/binary_requirements.txt @@ -3,10 +3,10 @@ apprise==1.10.0 # via -r binary_requirements.in attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.in . # via -r binary_requirements.in -certifi==2026.4.22 # via apprise, requests, -r binary_requirements.in +certifi==2026.5.20 # via apprise, requests, -r binary_requirements.in charset-normalizer==3.4.7 # via requests, -r binary_requirements.in -click==8.3.3 # via apprise, -r binary_requirements.in -idna==3.14 # via requests, -r binary_requirements.in +click==8.4.1 # via apprise, -r binary_requirements.in +idna==3.16 # via requests, -r binary_requirements.in jsonschema==4.26.0 # via borgmatic, -r binary_requirements.in jsonschema-specifications==2025.9.1 # via jsonschema, -r binary_requirements.in markdown==3.10.2 # via apprise, -r binary_requirements.in @@ -14,7 +14,7 @@ oauthlib==3.3.1 # via requests-oauthlib, -r binary_requirements.in packaging==26.2 # via borgmatic, -r binary_requirements.in pyyaml==6.0.3 # via apprise, -r binary_requirements.in referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r binary_requirements.in -requests==2.34.0 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in +requests==2.34.2 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in requests-oauthlib==2.0.0 # via apprise, -r binary_requirements.in rpds-py==0.30.0 # via jsonschema, referencing, -r binary_requirements.in ruamel-yaml==0.19.1 # via borgmatic, -r binary_requirements.in diff --git a/test_requirements.txt b/test_requirements.txt index 4b9aff2b..1f58a643 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,13 +2,13 @@ # uv pip compile --annotation-style line test_requirements.in -o test_requirements.txt apprise==1.10.0 # via -r test_requirements.in attrs==26.1.0 # via jsonschema, referencing, -r test_requirements.in -certifi==2026.4.22 # via apprise, requests, -r test_requirements.in +certifi==2026.5.20 # via apprise, requests, -r test_requirements.in charset-normalizer==3.4.7 # via requests, -r test_requirements.in click>=8.1.8 codespell==2.4.2 # via -r test_requirements.in coverage==7.14.0 # via pytest-cov, -r test_requirements.in flexmock==0.13.0 # via -r test_requirements.in -idna==3.14 # via requests, -r test_requirements.in +idna==3.16 # via requests, -r test_requirements.in iniconfig==2.3.0 # via pytest, -r test_requirements.in jsonschema==4.26.0 # via -r test_requirements.in jsonschema-specifications==2025.9.1 # via jsonschema, -r test_requirements.in @@ -22,7 +22,7 @@ pytest-cov==7.1.0 # via -r test_requirements.in pytest-timeout==2.4.0 # via -r test_requirements.in pyyaml>5.0.0 referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r test_requirements.in -requests==2.34.0 # via apprise, requests-oauthlib, -r test_requirements.in +requests==2.34.2 # via apprise, requests-oauthlib, -r test_requirements.in requests-oauthlib==2.0.0 # via apprise, -r test_requirements.in rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in ruamel-yaml>0.15.0