Compare commits

...
26 Commits
Author SHA1 Message Date
Dan Helfman bddc82951e Bump version for release. 2025-11-02 19:41:38 -08:00
Dan Helfman 3d15c51039 Fix the "--repository" flag not applying to command hooks (#1176). 2025-11-02 19:32:08 -08:00
Dan Helfman f9206c15e7 Fix documentation typos. 2025-11-02 09:36:08 -08:00
Dan Helfman 0da4b64838 Drop support for Python 3.9, which has been end-of-lifed. (Also hopefully fix the build.) 2025-11-01 23:16:13 -07:00
Dan Helfman 85f06a900a Revert "Attempt to fix broken build (rpds module not found error)."
This reverts commit bae8f983e0.
2025-11-01 21:48:59 -07:00
Dan Helfman bae8f983e0 Attempt to fix broken build (rpds module not found error). 2025-11-01 21:39:10 -07:00
Dan Helfman 89c0cd0af6 Add documentation on patterns and excludes. 2025-11-01 21:18:22 -07:00
Dan Helfman 292d9cb2c7 Add documentation on patterns and excludes. 2025-11-01 21:17:35 -07:00
Dan Helfman 181030c5d1 Add an "environment" option to the Sentry monitoring hook (#1172). 2025-10-30 15:39:49 -07:00
Dan Helfman 2600598359 Add environment option to Sentry monitoring hook (#1172).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1172
Reviewed-by: Dan Helfman <witten@torsion.org>
2025-10-30 22:37:53 +00:00
Tom Janssen 9e649af2a3 fix typo 2025-10-29 10:13:45 +01:00
Tom Janssen 370bc7e7f0 fix schema length 2025-10-29 09:55:03 +01:00
Tom Janssen 7cca1358fb incorporate feedback 2025-10-29 09:50:55 +01:00
Dan Helfman 67406b60b3 Cleanup "other ways to install" section and boost "Docker" there so it shows up in search. 2025-10-28 23:01:51 -07:00
Dan Helfman 72fde728a4 Fix typo (#1173).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1173
2025-10-28 18:16:36 +00:00
StefanSchoof d4433eeec1 Fix typo 2025-10-28 16:43:40 +00:00
Tom Janssen d0b5244888 add environment option 2025-10-28 16:52:50 +01:00
Dan Helfman 1ed207c6b1 Document borgmatic's limitations around parallelism—both its own and Borg's (#957). 2025-10-27 15:58:12 -07:00
Dan Helfman 5ef257ebb6 Attempt to clarify recursion root patterns in the schema comments. 2025-10-27 14:34:20 -07:00
Dan Helfman eb2b0b35c1 Fix for an inconsistent log level for Borg's last output line before exiting (#1170). 2025-10-27 14:20:27 -07:00
Dan Helfman 0f7ebcb4b7 Fix for a regression in the ZFS, LVM, and Btrfs hooks in which partial excludes of snapshot paths were ignored (#1169). 2025-10-26 14:12:52 -07:00
Dan Helfman 51d2ce7963 Add a "rename" option to "extra_borg_options" to support passing arbitrary flags to "borg rename". 2025-10-24 23:02:46 -07:00
Dan Helfman 0c4222037d Fix for the "list", "info", and "delete" options in "extra_borg_options" being ignored when "--archive" is omitted with Borg 1.x (#1168). 2025-10-24 22:57:23 -07:00
Dan Helfman 25e8aad517 Add systemd multiple service directories fix to NEWS (#1165). 2025-10-24 09:36:52 -07:00
Dan Helfman 11ffd76cc6 Support multiple configured systemd service directories (RuntimeDirectory, StateDirectory) (#1165). 2025-10-24 16:31:59 +00:00
Simon Pilkington 68864395b5 Support multiple configured systemd service directories (RuntimeDirectory, StateDirectory) 2025-10-24 08:20:59 +02:00
78 changed files with 1133 additions and 829 deletions
+20 -1
View File
@@ -1,5 +1,24 @@
2.0.11
* #957: Document borgmatic's limitations around parallelism—both its own and Borg's. See the
documentation for more information:
https://torsion.org/borgmatic/how-to/make-per-application-backups/#limitations
* #1165: Fix for when the systemd service directories (RuntimeDirectory and StateDirectory) each
contain multiple paths.
* #1168: Fix for the "list", "info", and "delete" options in "extra_borg_options" being ignored
when "--archive" is omitted with Borg 1.x.
* #1169: Fix for a regression in the ZFS, LVM, and Btrfs hooks in which partial excludes of
snapshot paths were ignored.
* #1170: Fix for an inconsistent log level for Borg's last output line before exiting.
* #1172: Add an "environment" option to the Sentry monitoring hook.
* #1176: Fix the "--repository" flag not applying to command hooks.
* Add a "rename" option to "extra_borg_options" to support passing arbitrary flags to "borg
rename".
* Add documentation on patterns and excludes:
https://torsion.org/borgmatic/reference/configuration/patterns-and-excludes/
* Drop support for Python 3.9, which has been end-of-lifed.
2.0.10
* #427: Expand the "borg_extra_options" option to support passing arbitrary Borg flags to every
* #427: Expand the "extra_borg_options" option to support passing arbitrary Borg flags to every
Borg sub-command that borgmatic uses. As part of this, deprecate the "init" option under
"borg_extra_options" in favor of "repo_create".
* #942: Factor reference material out of the documentation how-to guides. This means there's now a
+19 -24
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.borg.borg
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -19,26 +18,22 @@ def run_borg(
'''
Run the "borg" action for the given repository.
'''
if borg_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
borg_arguments.repository,
):
logger.info('Running arbitrary Borg command')
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
borg_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
borgmatic.borg.borg.run_arbitrary_borg(
repository['path'],
config,
local_borg_version,
options=borg_arguments.options,
archive=archive_name,
local_path=local_path,
remote_path=remote_path,
)
logger.info('Running arbitrary Borg command')
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
borg_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
borgmatic.borg.borg.run_arbitrary_borg(
repository['path'],
config,
local_borg_version,
options=borg_arguments.options,
archive=archive_name,
local_path=local_path,
remote_path=remote_path,
)
+9 -14
View File
@@ -1,7 +1,6 @@
import logging
import borgmatic.borg.break_lock
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -18,16 +17,12 @@ def run_break_lock(
'''
Run the "break-lock" action for the given repository.
'''
if break_lock_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
break_lock_arguments.repository,
):
logger.info('Breaking repository and cache locks')
borgmatic.borg.break_lock.break_lock(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
logger.info('Breaking repository and cache locks')
borgmatic.borg.break_lock.break_lock(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
+10 -18
View File
@@ -1,7 +1,6 @@
import logging
import borgmatic.borg.change_passphrase
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -18,20 +17,13 @@ def run_change_passphrase(
'''
Run the "key change-passphrase" action for the given repository.
'''
if (
change_passphrase_arguments.repository is None
or borgmatic.config.validate.repositories_match(
repository,
change_passphrase_arguments.repository,
)
):
logger.info('Changing repository passphrase')
borgmatic.borg.change_passphrase.change_passphrase(
repository['path'],
config,
local_borg_version,
change_passphrase_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
logger.info('Changing repository passphrase')
borgmatic.borg.change_passphrase.change_passphrase(
repository['path'],
config,
local_borg_version,
change_passphrase_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
-7
View File
@@ -22,7 +22,6 @@ import borgmatic.borg.pattern
import borgmatic.borg.repo_list
import borgmatic.borg.state
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.execute
import borgmatic.hooks.command
@@ -750,12 +749,6 @@ def run_check(
Raise ValueError if the Borg repository ID cannot be determined.
'''
if check_arguments.repository and not borgmatic.config.validate.repositories_match(
repository,
check_arguments.repository,
):
return
logger.info('Running consistency checks')
repository_id = borgmatic.borg.check.get_repository_id(
-7
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.borg.compact
import borgmatic.borg.feature
import borgmatic.config.validate
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
@@ -22,12 +21,6 @@ def run_compact(
'''
Run the "compact" action for the given repository.
'''
if compact_arguments.repository and not borgmatic.config.validate.repositories_match(
repository,
compact_arguments.repository,
):
return
if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
logger.info(f'Compacting segments{dry_run_label}')
borgmatic.borg.compact.compact_segments(
-1
View File
@@ -5,7 +5,6 @@ import os
import borgmatic.borg.extract
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
-7
View File
@@ -6,7 +6,6 @@ import borgmatic.borg.feature
import borgmatic.borg.rename
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.dispatch
from borgmatic.actions import pattern
@@ -30,12 +29,6 @@ def run_create(
If create_arguments.json is True, yield the JSON output from creating the archive.
'''
if create_arguments.repository and not borgmatic.config.validate.repositories_match(
repository,
create_arguments.repository,
):
return
if config.get('list_details') and config.get('progress'):
raise ValueError(
'With the create action, only one of --list/--files/list_details and --progress/progress can be used.',
+18 -22
View File
@@ -20,32 +20,28 @@ def run_delete(
'''
Run the "delete" action for the given repository and archive(s).
'''
if delete_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
delete_arguments.repository,
):
logger.answer('Deleting archives')
logger.answer('Deleting archives')
archive_name = (
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
delete_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
if delete_arguments.archive
else None
)
borgmatic.borg.delete.delete_archives(
repository,
archive_name = (
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
delete_arguments.archive,
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(delete_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
if delete_arguments.archive
else None
)
borgmatic.borg.delete.delete_archives(
repository,
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(delete_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
+10 -15
View File
@@ -1,7 +1,6 @@
import logging
import borgmatic.borg.export_key
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -18,17 +17,13 @@ def run_export_key(
'''
Run the "key export" action for the given repository.
'''
if export_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
export_arguments.repository,
):
logger.info('Exporting repository key')
borgmatic.borg.export_key.export_key(
repository['path'],
config,
local_borg_version,
export_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
logger.info('Exporting repository key')
borgmatic.borg.export_key.export_key(
repository['path'],
config,
local_borg_version,
export_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
+19 -24
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.borg.export_tar
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -19,30 +18,26 @@ def run_export_tar(
'''
Run the "export-tar" action for the given repository.
'''
if export_tar_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
export_tar_arguments.repository,
):
logger.info(f'Exporting archive {export_tar_arguments.archive} as tar file')
borgmatic.borg.export_tar.export_tar_archive(
global_arguments.dry_run,
logger.info(f'Exporting archive {export_tar_arguments.archive} as tar file')
borgmatic.borg.export_tar.export_tar_archive(
global_arguments.dry_run,
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
export_tar_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
export_tar_arguments.paths,
export_tar_arguments.destination,
export_tar_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
tar_filter=export_tar_arguments.tar_filter,
strip_components=export_tar_arguments.strip_components,
)
local_path,
remote_path,
),
export_tar_arguments.paths,
export_tar_arguments.destination,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
tar_filter=export_tar_arguments.tar_filter,
strip_components=export_tar_arguments.strip_components,
)
+18 -23
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.borg.extract
import borgmatic.borg.repo_list
import borgmatic.config.validate
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
@@ -21,29 +20,25 @@ def run_extract(
'''
Run the "extract" action for the given repository.
'''
if extract_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
extract_arguments.repository,
):
logger.info(f'Extracting archive {extract_arguments.archive}')
borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
logger.info(f'Extracting archive {extract_arguments.archive}')
borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
extract_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
extract_arguments.paths,
extract_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
destination_path=extract_arguments.destination,
strip_components=extract_arguments.strip_components,
)
local_path,
remote_path,
),
extract_arguments.paths,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
destination_path=extract_arguments.destination,
strip_components=extract_arguments.strip_components,
)
+10 -15
View File
@@ -1,7 +1,6 @@
import logging
import borgmatic.borg.import_key
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -18,17 +17,13 @@ def run_import_key(
'''
Run the "key import" action for the given repository.
'''
if import_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
import_arguments.repository,
):
logger.info('Importing repository key')
borgmatic.borg.import_key.import_key(
repository['path'],
config,
local_borg_version,
import_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
logger.info('Importing repository key')
borgmatic.borg.import_key.import_key(
repository['path'],
config,
local_borg_version,
import_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
+22 -27
View File
@@ -4,7 +4,6 @@ import borgmatic.actions.arguments
import borgmatic.actions.json
import borgmatic.borg.info
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -23,30 +22,26 @@ def run_info(
If info_arguments.json is True, yield the JSON output from the info for the archive.
'''
if info_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
info_arguments.repository,
):
if not info_arguments.json:
logger.answer('Displaying archive summary information')
if not info_arguments.json:
logger.answer('Displaying archive summary information')
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
info_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
json_output = borgmatic.borg.info.display_archives_info(
repository['path'],
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(info_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
info_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
json_output = borgmatic.borg.info.display_archives_info(
repository['path'],
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(info_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+25 -30
View File
@@ -3,7 +3,6 @@ import logging
import borgmatic.actions.arguments
import borgmatic.actions.json
import borgmatic.borg.list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -22,33 +21,29 @@ def run_list(
If list_arguments.json is True, yield the JSON output from listing the archive.
'''
if list_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
list_arguments.repository,
):
if not list_arguments.json:
if list_arguments.find_paths: # pragma: no cover
logger.answer('Searching archives')
elif not list_arguments.archive: # pragma: no cover
logger.answer('Listing archives')
if not list_arguments.json:
if list_arguments.find_paths: # pragma: no cover
logger.answer('Searching archives')
elif not list_arguments.archive: # pragma: no cover
logger.answer('Listing archives')
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
list_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
json_output = borgmatic.borg.list.list_archive(
repository['path'],
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(list_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
list_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
json_output = borgmatic.borg.list.list_archive(
repository['path'],
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(list_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+18 -23
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.borg.mount
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -19,30 +18,26 @@ def run_mount(
'''
Run the "mount" action for the given repository.
'''
if mount_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
mount_arguments.repository,
):
if mount_arguments.archive:
logger.info(f'Mounting archive {mount_arguments.archive}')
else: # pragma: nocover
logger.info('Mounting repository')
if mount_arguments.archive:
logger.info(f'Mounting archive {mount_arguments.archive}')
else: # pragma: nocover
logger.info('Mounting repository')
borgmatic.borg.mount.mount_archive(
borgmatic.borg.mount.mount_archive(
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
mount_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
mount_arguments,
mount_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
local_path,
remote_path,
),
mount_arguments,
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
-7
View File
@@ -1,7 +1,6 @@
import logging
import borgmatic.borg.prune
import borgmatic.config.validate
import borgmatic.hooks.command
logger = logging.getLogger(__name__)
@@ -21,12 +20,6 @@ def run_prune(
'''
Run the "prune" action for the given repository.
'''
if prune_arguments.repository and not borgmatic.config.validate.repositories_match(
repository,
prune_arguments.repository,
):
return
logger.info(f'Pruning archives{dry_run_label}')
borgmatic.borg.prune.prune_archives(
global_arguments.dry_run,
+45 -50
View File
@@ -4,7 +4,6 @@ import subprocess
import borgmatic.borg.info
import borgmatic.borg.recreate
import borgmatic.borg.repo_list
import borgmatic.config.validate
from borgmatic.actions.pattern import collect_patterns, process_patterns
logger = logging.getLogger(__name__)
@@ -25,64 +24,60 @@ def run_recreate(
'''
Run the "recreate" action for the given repository.
'''
if recreate_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
recreate_arguments.repository,
):
if recreate_arguments.archive:
logger.answer(f'Recreating archive {recreate_arguments.archive}')
else:
logger.answer('Recreating repository')
if recreate_arguments.archive:
logger.answer(f'Recreating archive {recreate_arguments.archive}')
else:
logger.answer('Recreating repository')
# Collect and process patterns.
processed_patterns = process_patterns(
collect_patterns(config),
config,
borgmatic.config.paths.get_working_directory(config),
# Collect and process patterns.
processed_patterns = process_patterns(
collect_patterns(config),
config,
borgmatic.config.paths.get_working_directory(config),
)
archive = borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
recreate_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
if archive and archive.endswith('.recreate'):
if recreate_arguments.archive == 'latest':
raise ValueError(
f'The latest archive "{archive}" is leftover from a prior recreate. Delete it first or select a different archive.',
)
raise ValueError(
f'The archive "{recreate_arguments.archive}" is leftover from a prior recreate. Select a different archive.',
)
archive = borgmatic.borg.repo_list.resolve_archive_name(
try:
borgmatic.borg.recreate.recreate_archive(
repository['path'],
recreate_arguments.archive,
archive,
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path,
remote_path,
local_path=local_path,
remote_path=remote_path,
patterns=processed_patterns,
)
if archive and archive.endswith('.recreate'):
if recreate_arguments.archive == 'latest':
except subprocess.CalledProcessError as error:
if error.returncode == BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS:
if recreate_arguments.target:
raise ValueError(
f'The latest archive "{archive}" is leftover from a prior recreate. Delete it first or select a different archive.',
f'The archive "{recreate_arguments.target}" already exists. Delete it first or set a different target archive name.',
)
raise ValueError(
f'The archive "{recreate_arguments.archive}" is leftover from a prior recreate. Select a different archive.',
)
if archive:
raise ValueError(
f'The archive "{archive}.recreate" is leftover from a prior recreate. Delete it first or select a different archive.',
)
try:
borgmatic.borg.recreate.recreate_archive(
repository['path'],
archive,
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
patterns=processed_patterns,
)
except subprocess.CalledProcessError as error:
if error.returncode == BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS:
if recreate_arguments.target:
raise ValueError(
f'The archive "{recreate_arguments.target}" already exists. Delete it first or set a different target archive name.',
)
if archive:
raise ValueError(
f'The archive "{archive}.recreate" is leftover from a prior recreate. Delete it first or select a different archive.',
)
raise
raise
-7
View File
@@ -1,7 +1,6 @@
import logging
import borgmatic.borg.repo_create
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -18,12 +17,6 @@ def run_repo_create(
'''
Run the "repo-create" action for the given repository.
'''
if repo_create_arguments.repository and not borgmatic.config.validate.repositories_match(
repository,
repo_create_arguments.repository,
):
return
logger.info('Creating repository')
encryption_mode = repo_create_arguments.encryption_mode or repository.get('encryption')
+12 -16
View File
@@ -17,20 +17,16 @@ def run_repo_delete(
'''
Run the "repo-delete" action for the given repository.
'''
if repo_delete_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
repo_delete_arguments.repository,
):
logger.answer(
'Deleting repository' + (' cache' if repo_delete_arguments.cache_only else ''),
)
logger.answer(
'Deleting repository' + (' cache' if repo_delete_arguments.cache_only else ''),
)
borgmatic.borg.repo_delete.delete_repository(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path,
remote_path,
)
borgmatic.borg.repo_delete.delete_repository(
repository,
config,
local_borg_version,
repo_delete_arguments,
global_arguments,
local_path,
remote_path,
)
+13 -18
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.actions.json
import borgmatic.borg.repo_info
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -21,21 +20,17 @@ def run_repo_info(
If repo_info_arguments.json is True, yield the JSON output from the info for the repository.
'''
if repo_info_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
repo_info_arguments.repository,
):
if not repo_info_arguments.json:
logger.answer('Displaying repository summary information')
if not repo_info_arguments.json:
logger.answer('Displaying repository summary information')
json_output = borgmatic.borg.repo_info.display_repository_info(
repository['path'],
config,
local_borg_version,
repo_info_arguments=repo_info_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
json_output = borgmatic.borg.repo_info.display_repository_info(
repository['path'],
config,
local_borg_version,
repo_info_arguments=repo_info_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+13 -18
View File
@@ -2,7 +2,6 @@ import logging
import borgmatic.actions.json
import borgmatic.borg.repo_list
import borgmatic.config.validate
logger = logging.getLogger(__name__)
@@ -21,21 +20,17 @@ def run_repo_list(
If repo_list_arguments.json is True, yield the JSON output from listing the repository.
'''
if repo_list_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository,
repo_list_arguments.repository,
):
if not repo_list_arguments.json:
logger.answer('Listing repository')
if not repo_list_arguments.json:
logger.answer('Listing repository')
json_output = borgmatic.borg.repo_list.list_repository(
repository['path'],
config,
local_borg_version,
repo_list_arguments=repo_list_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
json_output = borgmatic.borg.repo_list.list_repository(
repository['path'],
config,
local_borg_version,
repo_list_arguments=repo_list_arguments,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output:
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
+1 -9
View File
@@ -11,7 +11,6 @@ import borgmatic.borg.list
import borgmatic.borg.mount
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.data_source.dump
import borgmatic.hooks.dispatch
@@ -524,18 +523,11 @@ def run_restore(
remote_path,
):
'''
Run the "restore" action for the given repository, but only if the repository matches the
requested repository in restore arguments.
Run the "restore" action for the given repository.
Raise ValueError if a configured data source could not be found to restore or there's no
matching dump in the archive.
'''
if restore_arguments.repository and not borgmatic.config.validate.repositories_match(
repository,
restore_arguments.repository,
):
return
logger.info(f'Restoring data sources from archive {restore_arguments.archive}')
working_directory = borgmatic.config.paths.get_working_directory(config)
+8 -1
View File
@@ -27,7 +27,14 @@ def make_repo_delete_command(
arguments to the repo_delete action as an argparse.Namespace, and global arguments, return a command
as a tuple to repo_delete the entire repository.
'''
extra_borg_options = config.get('extra_borg_options', {}).get('repo_delete', '')
extra_borg_options = config.get('extra_borg_options', {}).get(
'repo_delete'
if borgmatic.borg.feature.available(
borgmatic.borg.feature.Feature.REPO_DELETE, local_borg_version
)
else 'delete',
'',
)
return (
(local_path,)
+4 -1
View File
@@ -25,7 +25,10 @@ def display_repository_info(
'''
borgmatic.logger.add_custom_log_levels()
lock_wait = config.get('lock_wait', None)
extra_borg_options = config.get('extra_borg_options', {}).get('repo_info', '')
extra_borg_options = config.get('extra_borg_options', {}).get(
'repo_info' if feature.available(feature.Feature.REPO_INFO, local_borg_version) else 'info',
'',
)
full_command = (
(local_path,)
+8 -2
View File
@@ -63,7 +63,10 @@ def get_latest_archive(
Raises ValueError if there are no archives in the repository.
'''
extra_borg_options = config.get('extra_borg_options', {}).get('repo_list', '')
extra_borg_options = config.get('extra_borg_options', {}).get(
'repo_list' if feature.available(feature.Feature.REPO_LIST, local_borg_version) else 'list',
'',
)
full_command = (
local_path,
@@ -124,7 +127,10 @@ def make_repo_list_command(
arguments to the repo_list action, global arguments as an argparse.Namespace instance, and local and
remote Borg paths, return a command as a tuple to list archives with a repository.
'''
extra_borg_options = config.get('extra_borg_options', {}).get('repo_list', '')
extra_borg_options = config.get('extra_borg_options', {}).get(
'repo_list' if feature.available(feature.Feature.REPO_LIST, local_borg_version) else 'list',
'',
)
return (
(
+17 -1
View File
@@ -340,7 +340,7 @@ def run_actions( # noqa: PLR0912, PLR0915
'''
Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration
filename, a configuration dict, a sequence of loaded configuration paths, local and remote paths
to Borg, a local Borg version string, and a repository name, run all actions from the
to Borg, a local Borg version string, and a repository dict, run all actions from the
command-line arguments on the given repository.
Yield JSON output strings from executing any actions that produce JSON.
@@ -362,6 +362,22 @@ def run_actions( # noqa: PLR0912, PLR0915
'repository': repository_path,
}
skip_actions = set(get_skip_actions(config, arguments))
requested_repository = next(
(
repository
for action_arguments in arguments.values()
for repository in (getattr(action_arguments, 'repository', None),)
if repository is not None
),
None,
)
if requested_repository and not borgmatic.config.validate.repositories_match(
repository,
requested_repository,
):
logger.debug('Skipping actions because the requested --repository does not match')
return
with borgmatic.hooks.command.Before_after_hooks(
command_hooks=config.get('commands'),
+26 -2
View File
@@ -2,10 +2,19 @@ import contextlib
import logging
import os
import tempfile
from enum import Enum
logger = logging.getLogger(__name__)
class Systemd_directories(Enum):
RUNTIME_DIRECTORY = 0
STATE_DIRECTORY = 1
CACHE_DIRECTORY = 2
LOGS_DIRECTORY = 3
CONFIGURATION_DIRECTORY = 4
def expand_user_in_path(path):
'''
Given a directory path, expand any tildes in it.
@@ -16,6 +25,17 @@ def expand_user_in_path(path):
return None
def resolve_systemd_directory(directory):
'''
Given a systemd directory environment variable enum, read the value if set and return the first
configured directory.
'''
separator = ':'
paths = os.environ.get(directory.name)
return paths.split(separator)[0] if paths else None
def get_working_directory(config): # pragma: no cover
'''
Given a configuration dict, get the working directory from it, expanding any tildes.
@@ -96,7 +116,9 @@ class Runtime_directory:
runtime_directory = (
config.get('user_runtime_directory')
or os.environ.get('XDG_RUNTIME_DIR') # Set by PAM on Linux.
or os.environ.get('RUNTIME_DIRECTORY') # Set by systemd if configured.
or resolve_systemd_directory(
Systemd_directories.RUNTIME_DIRECTORY
) # Set by systemd if configured.
)
if runtime_directory:
@@ -174,7 +196,9 @@ def get_borgmatic_state_directory(config):
os.path.join(
config.get('user_state_directory')
or os.environ.get('XDG_STATE_HOME')
or os.environ.get('STATE_DIRECTORY') # Set by systemd if configured.
or resolve_systemd_directory(
Systemd_directories.STATE_DIRECTORY
) # Set by systemd if configured.
or '~/.local/state',
'borgmatic',
),
+17 -4
View File
@@ -173,9 +173,11 @@ properties:
type: string
description: |
Any paths matching these patterns are included/excluded from
backups. Globs are expanded. (Tildes are not.) See the output of
"borg help patterns" for more details. Quote any value if it
contains leading punctuation, so it parses correctly.
backups. Recursion root patterns ("R ...") are effectively the same
as "source_directories"; they tell Borg which paths to backup
(modulo any excludes). Globs are expanded. (Tildes are not.) See
the output of "borg help patterns" for more details. Quote any value
if it contains leading punctuation, so it parses correctly.
example:
- 'R /'
- '- /home/*/.cache'
@@ -259,7 +261,7 @@ properties:
Path for storing temporary runtime data like streaming database
dumps and bootstrap metadata. borgmatic automatically creates and
uses a "borgmatic" subdirectory here. Defaults to $XDG_RUNTIME_DIR
or or $TMPDIR or $TEMP or /run/user/$UID.
or $TMPDIR or $TEMP or /run/user/$UID.
example: /run/user/1001
user_state_directory:
type: string
@@ -607,6 +609,11 @@ properties:
description: |
Extra command-line options to pass to "borg recreate".
example: "--extra-option"
rename:
type: string
description: |
Extra command-line options to pass to "borg rename".
example: "--extra-option"
repo_create:
type: string
description: |
@@ -3015,6 +3022,12 @@ properties:
project monitor. Used along with the data source name URL to
construct a cron URL.
example: mymonitor
environment:
type: string
description: |
Sentry monitor environment used in the call to Sentry. If
not set, the Sentry default is used.
example: production
states:
type: array
items:
+3 -3
View File
@@ -158,8 +158,8 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path, b
if not line or not ready_process:
break
# Keep the last few lines of output in case the process errors, and we need the output for
# the exception below.
# Keep the last few lines of output in case the process errors, and we need the
# output for the exception below.
append_last_lines(
buffer_last_lines[ready_buffer],
captured_outputs[ready_process],
@@ -199,7 +199,7 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path, b
last_lines,
captured_outputs[process],
line,
output_log_level=logging.ERROR,
output_log_level,
)
if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
+7 -1
View File
@@ -329,9 +329,15 @@ def dump_data_sources(
snapshot_subvolume(btrfs_command, subvolume.path, snapshot_path)
last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index(
patterns, subvolume.contained_patterns
)
for pattern in subvolume.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(subvolume.path, pattern)
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
borgmatic.hooks.data_source.config.replace_pattern(
patterns, pattern, snapshot_pattern, last_contained_pattern_index
)
borgmatic.hooks.data_source.config.inject_pattern(
patterns, make_snapshot_exclude_pattern(subvolume.path)
+45 -4
View File
@@ -1,3 +1,4 @@
import contextlib
import json
import logging
import shutil
@@ -131,7 +132,23 @@ def inject_pattern(patterns, data_source_pattern):
patterns.insert(0, data_source_pattern)
def replace_pattern(patterns, pattern_to_replace, data_source_pattern):
def get_last_pattern_index(patterns, patterns_subset):
'''
Given a sequence of all patterns and a subset of those patterns, find each subset pattern in the
all patterns sequence and return the highest (last) index.
'''
last_pattern_index = 0
for pattern in patterns_subset:
with contextlib.suppress(ValueError):
last_pattern_index = max(patterns.index(pattern), last_pattern_index)
return last_pattern_index
def replace_pattern(
patterns, pattern_to_replace, data_source_pattern, last_contained_pattern_index
):
'''
Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
replace the given pattern with the given data source pattern. The idea is that borgmatic is
@@ -139,8 +156,32 @@ def replace_pattern(patterns, pattern_to_replace, data_source_pattern):
that the hook's data gets included in the backup.
As part of this replacement, if the data source pattern is a root pattern, also insert an
"include" version of the given root pattern right after the replaced pattern, in an attempt to
preempt any of the user's configured exclude patterns that may follow.
"include" version of the given root pattern right after the given last contained pattern index
in an attempt to preempt any of the user's configured global exclude patterns that may follow.
But we don't want to preempt any intentional partial excludes of the data source pattern itself,
which is why the include goes after the last contained pattern index.
For instance, let's say that the patterns are effectively:
R /foo
R /bar
- /bar/.cache
R /baz
- **
... and "R /bar" is the pattern to replace, data source pattern is "R /bar/snapshot", and the
last contained pattern index is 2 (corresponding to "- /bar/.cache"). The resulting patterns
after calling this function would be:
R /foo
R /bar/snapshot
- /bar/snapshot/.cache
+ /bar/snapshot
R /baz
- **
Note that the positioning of "+ /bar/snapshot" means that it overrides the "- **" global exclude
but not the "- /bar/snapshot/.cache" contained pattern exclude.
If the pattern to replace can't be found in the given patterns, then just inject the data source
pattern at the start of the list.
@@ -156,7 +197,7 @@ def replace_pattern(patterns, pattern_to_replace, data_source_pattern):
if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT:
patterns.insert(
index + 1,
last_contained_pattern_index + 1,
borgmatic.borg.pattern.Pattern(
path=data_source_pattern.path,
type=borgmatic.borg.pattern.Pattern_type.INCLUDE,
+7 -1
View File
@@ -269,6 +269,10 @@ def dump_data_sources(
snapshot_mount_path,
)
last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index(
patterns, logical_volume.contained_patterns
)
for pattern in logical_volume.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(
pattern,
@@ -276,7 +280,9 @@ def dump_data_sources(
normalized_runtime_directory,
)
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
borgmatic.hooks.data_source.config.replace_pattern(
patterns, pattern, snapshot_pattern, last_contained_pattern_index
)
return []
+7 -1
View File
@@ -300,6 +300,10 @@ def dump_data_sources(
snapshot_mount_path,
)
last_contained_pattern_index = borgmatic.hooks.data_source.config.get_last_pattern_index(
patterns, dataset.contained_patterns
)
for pattern in dataset.contained_patterns:
snapshot_pattern = make_borg_snapshot_pattern(
pattern,
@@ -307,7 +311,9 @@ def dump_data_sources(
normalized_runtime_directory,
)
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
borgmatic.hooks.data_source.config.replace_pattern(
patterns, pattern, snapshot_pattern, last_contained_pattern_index
)
return []
+5 -1
View File
@@ -1,5 +1,6 @@
import logging
import re
import urllib
import requests
@@ -40,6 +41,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
data_source_name_url = hook_config.get('data_source_name_url')
environment = hook_config.get('environment')
monitor_slug = hook_config.get('monitor_slug')
match = DATA_SOURCE_NAME_URL_PATTERN.match(data_source_name_url)
@@ -65,10 +67,12 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
if dry_run:
return
environment_query = f'&environment={urllib.parse.quote(environment)}' if environment else ''
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(
f'{cron_url}?status={status}',
f'{cron_url}?status={status}{environment_query}',
timeout=TIMEOUT_SECONDS,
headers={'User-Agent': 'borgmatic'},
)
+4
View File
@@ -5,6 +5,8 @@ eleventyNavigation:
parent: How-to guides
order: 15
---
<span data-pagefind-weight="0.25">
To get set up to develop on borgmatic, first [`install
uv`](https://docs.astral.sh/uv/) to make managing your borgmatic environment
easier without impacting other Python applications on your system.
@@ -205,3 +207,5 @@ borgmatic's developer build for documentation optionally supports using
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.
</span>
@@ -59,6 +59,20 @@ entry using borgmatic's `--config` flag instead of relying on
`/etc/borgmatic.d`.
## Limitations
borgmatic does not currently support its own parallelism—being run multiple
times on the same machine simultaneously. In particular, many of the [data
source
hooks](https://torsion.org/borgmatic/reference/configuration/data-sources/) rely
on global borgmatic runtime files which can't be shared across processes, and
therefore multiple borgmatic instances on the same machine would interfere with
each other.
A single borgmatic instance also doesn't currently support running multiple Borg
instances in parallel on the same machine.
<a id="archive-naming"></a>
<a id="configuration-includes"></a>
<a id="configuration-overrides"></a>
+15 -6
View File
@@ -75,12 +75,16 @@ with `sudo`. If that doesn't work, you may need to update your [sudoers
Besides the approaches described above, there are several other options for
installing borgmatic:
* [container image with scheduled backups](https://hub.docker.com/r/b3vis/borgmatic/) (+ Docker Compose files)
* [container image with multi-arch and Docker CLI support](https://hub.docker.com/r/modem7/borgmatic-docker/)
#### <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 official](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
* [Fedora unofficial](https://copr.fedorainfracloud.org/coprs/heffer/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)
@@ -88,9 +92,11 @@ installing 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?show=borgmatic&sort=relevance&type=packages&query=borgmatic)
* [NixOS](https://search.nixos.org/packages?channel=unstable&show=borgmatic&query=borgmatic)
#### Etc.
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
* [Unraid](https://unraid.net/community/apps?q=borgmatic#r)
* [uv tool install](https://docs.astral.sh/uv/)
@@ -150,6 +156,9 @@ added new options since you originally created your configuration file. Also
check out how to [upgrade your
configuration](https://torsion.org/borgmatic/how-to/upgrade/#upgrading-your-configuration).
And see the documentation on [patterns and
excludes](https://torsion.org/borgmatic/reference/configuration/patterns-and-excludes/).
### Encryption
@@ -5,11 +5,14 @@ eleventyNavigation:
parent: 🔒 Credentials
---
<span class="minilink minilink-addedin">New in version 1.9.11</span> When
running inside a container, borgmatic can read [Docker
secrets](https://docs.docker.com/compose/how-tos/use-secrets/) and [Podman
running inside a container, borgmatic can read [<span
data-pagefind-weight="0.5">Docker</span>
secrets](https://docs.docker.com/compose/how-tos/use-secrets/) and [<span
data-pagefind-weight="0.5">Podman</span>
secrets](https://www.redhat.com/en/blog/new-podman-secrets-command). Creating
those secrets and passing them into your borgmatic container is outside the
scope of this documentation, but here's a simple example of that with [Docker
scope of this documentation, but here's a simple example of that with [<span
data-pagefind-weight="0.5">Docker</span>
Compose](https://docs.docker.com/compose/):
```yaml
@@ -25,9 +28,9 @@ secrets:
```
This assumes there's a file on the host at `/etc/borgmatic/passphrase.txt`
containing your passphrase. Docker or Podman mounts the contents of that file
into a secret named `borgmatic_passphrase` in the borgmatic container at
`/run/secrets/`.
containing your passphrase. <span data-pagefind-weight="0.5">Docker or
Podman</span> mounts the contents of that file into a secret named
`borgmatic_passphrase` in the borgmatic container at `/run/secrets/`.
Once your container secret is in place, you can consume it within your borgmatic
configuration file:
@@ -26,6 +26,9 @@ sentry:
The `monitor_slug` value comes from the "Monitor Slug" under "Cron Details" on
the same Sentry monitor page.
The `environment` value optionally specifies the environment that is used in
Sentry.
With this configuration, borgmatic pings Sentry whenever borgmatic starts,
finishes, or fails, but only when any of the `create`, `prune`, `compact`, or
`check` actions are run. You can optionally override the start/finish/fail
@@ -36,6 +39,7 @@ Sentry on failure:
sentry:
data_source_name_url: https://5f80ec@o294220.ingest.us.sentry.io/203069
monitor_slug: mymonitor
environment: myenvironment
states:
- fail
```
@@ -0,0 +1,159 @@
---
title: Patterns and excludes
eleventyNavigation:
key: ⛔ Patterns and excludes
parent: ⚙️ Configuration
---
borgmatic's configuration has multiple options for specifying the source files
to include in your backups. Which of these options you use depends on how
complex your file matching needs are.
## Source directories
The `source_directories` option is the simplest way to specify the files and
directories to include in your backups. Globs (`*`) allow you to match multiple
paths at once, and tildes (`~`) get expanded to the current user's home
directory. Here's an example:
```yaml
source_directories:
- /home
- /etc
- /var/log/syslog*
- /home/user/path with spaces
- ~/.config
```
## Excludes
The `exclude_patterns` options lists particular paths to exclude from your
backups, paths that would otherwise get included by `source_directories`. Globs
and tildes are also supported here. Use quotes as needed. For example:
```yaml
exclude_patterns:
- '*.pyc'
- '/home/*/.cache'
- '*/.vim*.tmp'
- /etc/ssl
- /home/user/path with spaces
```
See the [Borg patterns
documentation](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-patterns)
for more details about the specific "fnmatch"-style syntax used by excludes.
The `exclude_from` option is similar to `exclude_patterns`, but with your patterns
listed in an external file instead of directly within borgmatic's configuration.
Here's an example:
```yaml
exclude_from:
- /etc/borgmatic/excludes
```
Also see the [borgmatic configuration
reference](https://torsion.org/borgmatic/reference/configuration/) for
additional exclude-related options.
## Patterns
When you have more complex needs for including and excluding files to backup,
the `patterns` option is available. The definitive documentation on patterns is
the [Borg patterns
documentation](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-patterns),
but here are the basics.
First, unless you have `source_directories`, you need root patterns. In fact,
root patterns are just another way to specify the same paths as in
`source_directories`; they tell Borg the starting point for recursing into
directories to find files to backup. Root patterns are prefixed with "`R `". For
instance:
```yaml
patterns:
- R /home
- R /etc
```
You can also add excludes and includes to your patterns. Excludes are prefixed
with "`- `". If you're defining these directly in borgmatic's configuration
file, use quotes around the pattern. For example:
```yaml
patterns:
- R /home
- '- /home/user/.cache'
- R /etc
```
How this works is that when Borg discovers a particular file path as it's
recursing into root directories, it tries to match that path against any
excludes and includes in your patterns *in order*, one at a time. If the first
match is to an exclude, then Borg excludes the file from the backup. But if the
first match is to an include, Borg includes it—even if there's a subsequent
exclude.
The respective order of root patterns vs. exclude and include patterns doesn't
matter to Borg, so organize root patterns how you like.
Here's an example of an include, which is prefixed with "`+ `":
```yaml
patterns:
- R /home
- '+ /home/user/.cache/keep-me'
- '- /home/user/.cache'
- R /etc
```
This example excludes all of the `.cache` directory—except for the `keep-me`
subdirectory, which gets included since it's listed first.
There's also a different kind of exclude pattern—a no-recurse exclude. That's
prefixed with "`! `" and tells Borg to not only exclude any matching paths but
also to ignore any subdirectories, saving file processing time. Here's an
example:
```yaml
patterns:
- R /home
- '! /home/user/.cache'
- R /etc
```
The `patterns_from` option is similar to `patterns`, but with your patterns
listed in an external file instead of directly within borgmatic's configuration.
Here's an example:
```yaml
patterns_from:
- /etc/borgmatic/patterns
```
## Debugging
Under the hood, borgmatic actually converts `source_directories`,
`exclude_patterns`, and `exclude_from` values to Borg patterns and merges them
with any `patterns` and `patterns_from` values you've configured—passing the
resulting processed patterns to Borg.
To see the combined patterns that borgmatic passes to Borg, run borgmatic with
[`--verbosity 2`](https://torsion.org/borgmatic/reference/command-line/logging/)
(and optionally `--dry-run`) and look for "`Writing patterns to ...`" in the
output. For instance:
```
repo: Writing patterns to /tmp/borgmatic-xzwb6s07/borgmatic/tmp61shymp0:
R /tmp/borgmatic-xzwb6s07/./borgmatic/sqlite_databases
+ /tmp/borgmatic-xzwb6s07/./borgmatic/sqlite_databases
R /home
R /etc
! fm:/home/user/.cache
```
You'll notice that borgmatic prepends your patterns with its own to support
use cases like streaming database dumps to Borg, creating filesystem snapshots,
saving bootstrap metadata, and so on.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "2.0.10"
version = "2.0.11"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+1 -1
View File
@@ -25,5 +25,5 @@ export PATH="/root/.local/bin:$PATH"
uv tool install tox --with tox-uv
export COVERAGE_FILE=/tmp/.coverage
tox --workdir /tmp/.tox -e py39,py313 --sitepackages
tox --workdir /tmp/.tox -e py311,py314 --sitepackages
tox --workdir /tmp/.tox --sitepackages -e end-to-end
+19 -18
View File
@@ -1,26 +1,27 @@
apprise==1.9.3
attrs==25.3.0
certifi==2025.7.14
charset-normalizer==3.4.2
apprise==1.9.5
attrs==25.4.0
certifi==2025.10.5
charset-normalizer==3.4.4
click>=8.1.8
codespell==2.4.1
coverage==7.9.2
coverage==7.11.0
flexmock==0.12.2
idna==3.10
iniconfig==2.1.0
jsonschema==4.24.1
jsonschema-specifications==2025.4.1
Markdown==3.8.2
idna==3.11
iniconfig==2.3.0
jsonschema==4.25.1
jsonschema-specifications==2025.9.1
markdown==3.9
oauthlib==3.3.1
packaging==25.0
pluggy==1.6.0
Pygments==2.19.2
pytest==8.4.1
pytest-cov==6.2.1
PyYAML>5.0.0
referencing==0.36.2
requests==2.32.4
pygments==2.19.2
pytest==8.4.2
pytest-cov==7.0.0
pyyaml>5.0.0
referencing==0.37.0
requests==2.32.5
requests-oauthlib==2.0.0
rpds-py==0.26.0
ruamel.yaml>0.15.0
rpds-py==0.28.0
ruamel-yaml>0.15.0
typing-extensions==4.15.0
urllib3==2.5.0
@@ -5,11 +5,22 @@ from borgmatic.hooks.data_source import btrfs as module
def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
patterns = [Pattern('/foo'), Pattern('/mnt/subvol1'), Pattern('/mnt/subvol2')]
patterns = [
Pattern('/foo'),
Pattern('/mnt/subvol1'),
Pattern('/mnt/subvol1/.cache', Pattern_type.EXCLUDE),
Pattern('/mnt/subvol2'),
]
config = {'btrfs': {}}
flexmock(module).should_receive('get_subvolumes').and_return(
(
module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
module.Subvolume(
'/mnt/subvol1',
contained_patterns=(
Pattern('/mnt/subvol1'),
Pattern('/mnt/subvol1/.cache', Pattern_type.EXCLUDE),
),
),
module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
),
)
@@ -50,6 +61,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
),
Pattern('/foo'),
Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1'),
Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1/.cache', Pattern_type.EXCLUDE),
Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1', Pattern_type.INCLUDE),
Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2'),
Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2', Pattern_type.INCLUDE),
@@ -6,13 +6,20 @@ from borgmatic.hooks.data_source import lvm as module
def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
config = {'lvm': {}}
patterns = [Pattern('/mnt/lvolume1/subdir'), Pattern('/mnt/lvolume2')]
patterns = [
Pattern('/mnt/lvolume1/subdir'),
Pattern('/mnt/lvolume1/subdir/.cache', Pattern_type.EXCLUDE),
Pattern('/mnt/lvolume2'),
]
logical_volumes = (
module.Logical_volume(
name='lvolume1',
device_path='/dev/lvolume1',
mount_point='/mnt/lvolume1',
contained_patterns=(Pattern('/mnt/lvolume1/subdir'),),
contained_patterns=(
Pattern('/mnt/lvolume1/subdir'),
Pattern('/mnt/lvolume1/subdir/.cache', Pattern_type.EXCLUDE),
),
),
module.Logical_volume(
name='lvolume2',
@@ -75,6 +82,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_updates_patterns():
assert patterns == [
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir'),
Pattern(
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir/.cache', Pattern_type.EXCLUDE
),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir', Pattern_type.INCLUDE),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'),
Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2', Pattern_type.INCLUDE),
-1
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import borg as module
def test_run_borg_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
flexmock(),
)
-1
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import break_lock as module
def test_run_break_lock_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.break_lock).should_receive('break_lock')
break_lock_arguments = flexmock(repository=flexmock())
@@ -5,7 +5,6 @@ from borgmatic.actions import change_passphrase as module
def test_run_change_passphrase_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.change_passphrase).should_receive('change_passphrase')
change_passphrase_arguments = flexmock(repository=flexmock())
-69
View File
@@ -1530,7 +1530,6 @@ def test_spot_check_without_any_source_paths_errors():
def test_run_check_checks_archives_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
@@ -1566,7 +1565,6 @@ def test_run_check_checks_archives_for_configured_repository():
def test_run_check_runs_configured_extract_check():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
@@ -1600,7 +1598,6 @@ def test_run_check_runs_configured_extract_check():
def test_run_check_runs_configured_spot_check():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
@@ -1637,7 +1634,6 @@ def test_run_check_runs_configured_spot_check():
def test_run_check_without_checks_runs_nothing_except_hooks():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
@@ -1667,68 +1663,3 @@ def test_run_check_without_checks_runs_nothing_except_hooks():
local_path=None,
remote_path=None,
)
def test_run_check_checks_archives_in_selected_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(True)
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
flexmock(module).should_receive('make_archives_check_id').and_return(None)
flexmock(module).should_receive('filter_checks_on_frequency').and_return(
{'repository', 'archives'},
)
flexmock(module.borgmatic.borg.check).should_receive('check_archives').once()
flexmock(module).should_receive('make_check_time_path')
flexmock(module).should_receive('write_check_time')
flexmock(module.borgmatic.borg.extract).should_receive('extract_last_archive_dry_run').never()
check_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
repair=flexmock(),
only_checks=flexmock(),
force=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_check(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'repositories': ['repo']},
local_borg_version=None,
check_arguments=check_arguments,
global_arguments=global_arguments,
local_path=None,
remote_path=None,
)
def test_run_check_bails_if_repository_does_not_match():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(False)
flexmock(module.borgmatic.borg.check).should_receive('check_archives').never()
check_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
repair=flexmock(),
only_checks=flexmock(),
force=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_check(
config_filename='test.yaml',
repository={'path': 'repo'},
config={'repositories': ['repo']},
local_borg_version=None,
check_arguments=check_arguments,
global_arguments=global_arguments,
local_path=None,
remote_path=None,
)
-57
View File
@@ -6,7 +6,6 @@ from borgmatic.actions import compact as module
def test_compact_actions_calls_hooks_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once()
compact_arguments = flexmock(
repository=None,
@@ -27,59 +26,3 @@ def test_compact_actions_calls_hooks_for_configured_repository():
local_path=None,
remote_path=None,
)
def test_compact_runs_with_selected_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(True)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').once()
compact_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
cleanup_commits=flexmock(),
compact_threshold=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_compact(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
local_borg_version=None,
compact_arguments=compact_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
def test_compact_bails_if_repository_does_not_match():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(False)
flexmock(module.borgmatic.borg.compact).should_receive('compact_segments').never()
compact_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
cleanup_commits=flexmock(),
compact_threshold=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_compact(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
local_borg_version=None,
compact_arguments=compact_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
-88
View File
@@ -9,7 +9,6 @@ from borgmatic.actions import create as module
def test_run_create_executes_and_calls_hooks_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -48,87 +47,8 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
)
def test_run_create_runs_with_selected_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured',
).and_return({})
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
flexmock(os.path).should_receive('join').and_return('/run/borgmatic/bootstrap')
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=False,
comment=None,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
list(
module.run_create(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
),
)
def test_run_create_bails_if_repository_does_not_match():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(False)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
repository=flexmock(),
progress=flexmock(),
statistics=flexmock(),
json=False,
comment=None,
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
list(
module.run_create(
config_filename='test.yaml',
repository='repo',
config={},
config_paths=['/tmp/test.yaml'],
local_borg_version=None,
create_arguments=create_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
),
)
def test_run_create_with_both_list_and_json_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
@@ -160,9 +80,6 @@ def test_run_create_with_both_list_and_json_errors():
def test_run_create_with_both_list_and_progress_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
@@ -194,9 +111,6 @@ def test_run_create_with_both_list_and_progress_errors():
def test_run_create_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -244,7 +158,6 @@ def test_run_create_with_active_dumps_roundtrips_via_checkpoint_archive():
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -325,7 +238,6 @@ def test_run_create_with_active_dumps_json_updates_archive_info():
}
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
-2
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import delete as module
def test_run_delete_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name')
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock(),
@@ -25,7 +24,6 @@ def test_run_delete_does_not_raise():
def test_run_delete_without_archive_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name')
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock(),
-1
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import export_key as module
def test_run_export_key_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.export_key).should_receive('export_key')
export_arguments = flexmock(repository=flexmock())
-3
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import export_tar as module
def test_run_export_tar_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.export_tar).should_receive('export_tar_archive')
export_tar_arguments = flexmock(
repository=flexmock(),
@@ -31,7 +30,6 @@ def test_run_export_tar_does_not_raise():
def test_run_export_tar_favors_flags_over_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.export_tar).should_receive('export_tar_archive').with_args(
object,
object,
@@ -70,7 +68,6 @@ def test_run_export_tar_favors_flags_over_config():
def test_run_export_tar_defaults_to_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.export_tar).should_receive('export_tar_archive').with_args(
object,
object,
-3
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import extract as module
def test_run_extract_calls_hooks():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive')
extract_arguments = flexmock(
paths=flexmock(),
@@ -31,7 +30,6 @@ def test_run_extract_calls_hooks():
def test_run_extract_favors_flags_over_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').with_args(
object,
object,
@@ -69,7 +67,6 @@ def test_run_extract_favors_flags_over_config():
def test_run_extract_defaults_to_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').with_args(
object,
object,
-1
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import import_key as module
def test_run_import_key_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.import_key).should_receive('import_key')
import_arguments = flexmock(repository=flexmock())
-2
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import info as module
def test_run_info_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
flexmock(),
)
@@ -30,7 +29,6 @@ def test_run_info_does_not_raise():
def test_run_info_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
flexmock(),
)
-2
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import list as module
def test_run_list_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
flexmock(),
)
@@ -35,7 +34,6 @@ def test_run_list_does_not_raise():
def test_run_list_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
flexmock(),
)
-1
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import mount as module
def test_run_mount_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.mount).should_receive('mount_archive')
mount_arguments = flexmock(
repository=flexmock(),
-53
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import prune as module
def test_run_prune_calls_hooks_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once()
prune_arguments = flexmock(repository=None, statistics=flexmock(), list_details=flexmock())
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
@@ -21,55 +20,3 @@ def test_run_prune_calls_hooks_for_configured_repository():
local_path=None,
remote_path=None,
)
def test_run_prune_runs_with_selected_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(True)
flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').once()
prune_arguments = flexmock(
repository=flexmock(),
statistics=flexmock(),
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_prune(
config_filename='test.yaml',
repository={'path': 'repo'},
config={},
local_borg_version=None,
prune_arguments=prune_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
def test_run_prune_bails_if_repository_does_not_match():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive(
'repositories_match',
).once().and_return(False)
flexmock(module.borgmatic.borg.prune).should_receive('prune_archives').never()
prune_arguments = flexmock(
repository=flexmock(),
statistics=flexmock(),
list_details=flexmock(),
)
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
module.run_prune(
config_filename='test.yaml',
repository='repo',
config={},
local_borg_version=None,
prune_arguments=prune_arguments,
global_arguments=global_arguments,
dry_run_label='',
local_path=None,
remote_path=None,
)
-7
View File
@@ -6,7 +6,6 @@ from borgmatic.actions import recreate as module
def test_run_recreate_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -28,7 +27,6 @@ def test_run_recreate_does_not_raise():
def test_run_recreate_with_archive_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -50,7 +48,6 @@ def test_run_recreate_with_archive_does_not_raise():
def test_run_recreate_with_leftover_recreate_archive_raises():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -73,7 +70,6 @@ def test_run_recreate_with_leftover_recreate_archive_raises():
def test_run_recreate_with_latest_archive_resolving_to_leftover_recreate_archive_raises():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -96,7 +92,6 @@ def test_run_recreate_with_latest_archive_resolving_to_leftover_recreate_archive
def test_run_recreate_with_archive_already_exists_error_raises():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -124,7 +119,6 @@ def test_run_recreate_with_archive_already_exists_error_raises():
def test_run_recreate_with_target_and_archive_already_exists_error_raises():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -156,7 +150,6 @@ def test_run_recreate_with_target_and_archive_already_exists_error_raises():
def test_run_recreate_with_other_called_process_error_passes_it_through():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
-32
View File
@@ -6,7 +6,6 @@ from borgmatic.actions import repo_create as module
def test_run_repo_create_with_encryption_mode_argument_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository')
arguments = flexmock(
encryption_mode=flexmock(),
@@ -31,7 +30,6 @@ def test_run_repo_create_with_encryption_mode_argument_does_not_raise():
def test_run_repo_create_with_encryption_mode_option_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository')
arguments = flexmock(
encryption_mode=None,
@@ -56,7 +54,6 @@ def test_run_repo_create_with_encryption_mode_option_does_not_raise():
def test_run_repo_create_without_encryption_mode_raises():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository')
arguments = flexmock(
encryption_mode=None,
@@ -80,36 +77,8 @@ def test_run_repo_create_without_encryption_mode_raises():
)
def test_run_repo_create_bails_if_repository_does_not_match():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(
False,
)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository').never()
arguments = flexmock(
encryption_mode=flexmock(),
source_repository=flexmock(),
repository=flexmock(),
copy_crypt_key=flexmock(),
append_only=flexmock(),
storage_quota=flexmock(),
make_parent_directories=flexmock(),
)
module.run_repo_create(
repository={'path': 'repo'},
config={},
local_borg_version=None,
repo_create_arguments=arguments,
global_arguments=flexmock(dry_run=False),
local_path=None,
remote_path=None,
)
def test_run_repo_create_favors_flags_over_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository').with_args(
object,
object,
@@ -153,7 +122,6 @@ def test_run_repo_create_favors_flags_over_config():
def test_run_repo_create_defaults_to_config():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_create).should_receive('create_repository').with_args(
object,
object,
-2
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import repo_delete as module
def test_run_repo_delete_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock(),
)
@@ -24,7 +23,6 @@ def test_run_repo_delete_does_not_raise():
def test_run_repo_delete_with_cache_only_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock(),
)
-2
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import repo_info as module
def test_run_repo_info_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_info).should_receive('display_repository_info')
repo_info_arguments = flexmock(repository=flexmock(), json=False)
@@ -24,7 +23,6 @@ def test_run_repo_info_does_not_raise():
def test_run_repo_info_parses_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_info).should_receive('display_repository_info').and_return(
flexmock(),
)
-2
View File
@@ -5,7 +5,6 @@ from borgmatic.actions import repo_list as module
def test_run_repo_list_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('list_repository')
repo_list_arguments = flexmock(repository=flexmock(), json=False)
@@ -24,7 +23,6 @@ def test_run_repo_list_does_not_raise():
def test_run_repo_list_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.repo_list).should_receive('list_repository').and_return(
flexmock(),
)
-34
View File
@@ -1168,7 +1168,6 @@ def test_run_restore_restores_each_data_source():
module.Dump(hook_name='postgresql_databases', data_source_name='bar'),
}
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
@@ -1238,41 +1237,11 @@ def test_run_restore_restores_each_data_source():
)
def test_run_restore_bails_for_non_matching_repository():
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(
False,
)
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
flexmock(module.borgmatic.config.paths).should_receive(
'make_runtime_directory_glob',
).replace_with(lambda path: path)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.actions.pattern).should_receive('collect_patterns').and_return(())
flexmock(module.borgmatic.actions.pattern).should_receive('process_patterns').and_return([])
flexmock(module.borgmatic.hooks.dispatch).should_receive(
'call_hooks_even_if_unconfigured',
).never()
flexmock(module).should_receive('restore_single_dump').never()
module.run_restore(
repository={'path': 'repo'},
config=flexmock(),
local_borg_version=flexmock(),
restore_arguments=flexmock(repository='repo', archive='archive', data_sources=flexmock()),
global_arguments=flexmock(dry_run=False),
local_path=flexmock(),
remote_path=flexmock(),
)
def test_run_restore_restores_data_source_by_falling_back_to_all_name():
dumps_to_restore = {
module.Dump(hook_name='postgresql_databases', data_source_name='foo'),
}
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
@@ -1335,7 +1304,6 @@ def test_run_restore_restores_data_source_configured_with_all_name():
module.Dump(hook_name='postgresql_databases', data_source_name='bar'),
}
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
@@ -1420,7 +1388,6 @@ def test_run_restore_skips_missing_data_source():
module.Dump(hook_name='postgresql_databases', data_source_name='bar'),
}
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
@@ -1505,7 +1472,6 @@ def test_run_restore_restores_data_sources_from_different_hooks():
module.Dump(hook_name='mysql_databases', data_source_name='foo'),
}
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
+22 -1
View File
@@ -216,7 +216,28 @@ def test_make_repo_delete_command_includes_lock_wait():
assert command == ('borg', 'repo-delete', '--lock-wait', '5', 'repo')
def test_make_repo_delete_command_includes_extra_borg_options():
def test_make_repo_delete_command_without_feature_available_includes_delete_extra_borg_options():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(False)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',),
)
command = module.make_repo_delete_command(
repository={'path': 'repo'},
config={'extra_borg_options': {'delete': '--extra "value with space"'}},
local_borg_version='1.2.3',
repo_delete_arguments=flexmock(list_details=False, force=0),
global_arguments=flexmock(dry_run=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--extra', 'value with space', 'repo')
def test_make_repo_delete_command_with_feature_available_includes_delete_extra_borg_options():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
+43 -1
View File
@@ -528,7 +528,49 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
)
def test_display_repository_info_calls_borg_with_extra_borg_options():
def test_display_repository_info_without_feature_available_calls_borg_with_info_extra_borg_options():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
config = {'extra_borg_options': {'info': '--extra "value with space"'}}
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').replace_with(
lambda name, value: (f'--{name}', str(value)) if value else (),
)
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)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('borg', 'info', '--extra', 'value with space', '--json', '--repo', 'repo'),
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
).and_return('[]')
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--extra', 'value with space', '--repo', 'repo'),
output_log_level=module.borgmatic.logger.ANSWER,
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
)
module.display_repository_info(
repository_path='repo',
config=config,
local_borg_version='2.3.4',
repo_info_arguments=flexmock(json=False),
global_arguments=flexmock(),
)
def test_display_repository_info_with_feature_available_calls_borg_with_repo_info_extra_borg_options():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
config = {'extra_borg_options': {'repo_info': '--extra "value with space"'}}
+85 -3
View File
@@ -412,7 +412,7 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
)
def test_get_latest_archive_calls_borg_with_extra_borg_options():
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)
flexmock(module.flags).should_receive('make_flags').and_return(())
@@ -437,6 +437,42 @@ def test_get_latest_archive_calls_borg_with_extra_borg_options():
borg_exit_codes=None,
).and_return(json.dumps({'archives': [expected_archive]}))
assert (
module.get_latest_archive(
'repo',
config={'extra_borg_options': {'list': '--extra "value with space"'}},
local_borg_version='1.2.3',
global_arguments=flexmock(),
)
== expected_archive
)
def test_get_latest_archive_with_feature_available_calls_borg_with_repo_list_extra_borg_options():
expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
flexmock(module.feature).should_receive('available').and_return(True)
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_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',
'repo-list',
*BORG_LIST_LATEST_ARGUMENTS[:-1],
'--extra',
'value with space',
*BORG_LIST_LATEST_ARGUMENTS[-1:],
),
environment=None,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
).and_return(json.dumps({'archives': [expected_archive]}))
assert (
module.get_latest_archive(
'repo',
@@ -546,6 +582,7 @@ def test_get_latest_archive_calls_borg_with_working_directory():
def test_make_repo_list_command_includes_log_info():
flexmock(module.feature).should_receive('available').and_return(False)
insert_logging_mock(logging.INFO)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
@@ -574,6 +611,7 @@ def test_make_repo_list_command_includes_log_info():
def test_make_repo_list_command_includes_json_but_not_info():
flexmock(module.feature).should_receive('available').and_return(False)
insert_logging_mock(logging.INFO)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
@@ -602,6 +640,7 @@ def test_make_repo_list_command_includes_json_but_not_info():
def test_make_repo_list_command_includes_log_debug():
flexmock(module.feature).should_receive('available').and_return(False)
insert_logging_mock(logging.DEBUG)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
@@ -630,6 +669,7 @@ def test_make_repo_list_command_includes_log_debug():
def test_make_repo_list_command_includes_json_but_not_debug():
flexmock(module.feature).should_receive('available').and_return(False)
insert_logging_mock(logging.DEBUG)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
@@ -658,6 +698,7 @@ def test_make_repo_list_command_includes_json_but_not_debug():
def test_make_repo_list_command_includes_json():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
@@ -685,6 +726,7 @@ def test_make_repo_list_command_includes_json():
def test_make_repo_list_command_includes_log_json():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
('--log-json',),
).and_return(())
@@ -714,6 +756,7 @@ def test_make_repo_list_command_includes_log_json():
def test_make_repo_list_command_includes_lock_wait():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
('--lock-wait', '5'),
).and_return(())
@@ -742,7 +785,36 @@ def test_make_repo_list_command_includes_lock_wait():
assert command == ('borg', 'list', '--lock-wait', '5', 'repo')
def test_make_repo_list_command_includes_extra_borg_options():
def test_make_repo_list_command_includes_list_extra_borg_options():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
None,
'1.2.3',
).and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
command = module.make_repo_list_command(
repository_path='repo',
config={'extra_borg_options': {'list': '--extra "value with space"'}},
local_borg_version='1.2.3',
repo_list_arguments=flexmock(
archive=None,
paths=None,
json=False,
prefix=None,
match_archives=None,
),
global_arguments=flexmock(),
)
assert command == ('borg', 'list', '--extra', 'value with space', 'repo')
def test_make_repo_list_command_with_feature_available_includes_repo_list_extra_borg_options():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
@@ -766,10 +838,11 @@ def test_make_repo_list_command_includes_extra_borg_options():
global_arguments=flexmock(),
)
assert command == ('borg', 'list', '--extra', 'value with space', 'repo')
assert command == ('borg', 'repo-list', '--extra', 'value with space', 'repo')
def test_make_repo_list_command_includes_local_path():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
@@ -798,6 +871,7 @@ def test_make_repo_list_command_includes_local_path():
def test_make_repo_list_command_includes_remote_path():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').replace_with(
lambda name, value: (f'--{name}', value) if value else (),
)
@@ -828,6 +902,7 @@ def test_make_repo_list_command_includes_remote_path():
def test_make_repo_list_command_includes_umask():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').replace_with(
lambda name, value: (f'--{name}', value) if value else (),
)
@@ -857,6 +932,7 @@ def test_make_repo_list_command_includes_umask():
def test_make_repo_list_command_transforms_prefix_into_match_archives():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--match-archives', 'sh:foo*'),
)
@@ -880,6 +956,7 @@ def test_make_repo_list_command_transforms_prefix_into_match_archives():
def test_make_repo_list_command_prefers_prefix_over_archive_name_format():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
('--match-archives', 'sh:foo*'),
)
@@ -899,6 +976,7 @@ def test_make_repo_list_command_prefers_prefix_over_archive_name_format():
def test_make_repo_list_command_transforms_archive_name_format_into_match_archives():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
@@ -926,6 +1004,7 @@ def test_make_repo_list_command_transforms_archive_name_format_into_match_archiv
def test_make_repo_list_command_includes_short():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
@@ -966,6 +1045,7 @@ def test_make_repo_list_command_includes_short():
),
)
def test_make_repo_list_command_includes_additional_flags(argument_name):
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
@@ -998,6 +1078,7 @@ def test_make_repo_list_command_includes_additional_flags(argument_name):
def test_make_repo_list_command_with_match_archives_calls_borg_with_match_archives_flags():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
@@ -1073,6 +1154,7 @@ def test_list_repository_with_json_calls_json_command_only():
def test_make_repo_list_command_with_date_based_matching_calls_borg_with_date_based_flags():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
+82
View File
@@ -939,6 +939,7 @@ def test_run_configuration_with_multiple_repositories_retries_with_timeout():
def test_run_actions_runs_repo_create():
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(),
)
@@ -962,9 +963,64 @@ def test_run_actions_runs_repo_create():
)
def test_run_actions_with_matching_repository_flag_runs_repo_create():
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').and_return(True)
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.repo_create).should_receive('run_repo_create').once()
tuple(
module.run_actions(
arguments={
'global': flexmock(dry_run=False),
'repo-create': flexmock(repository='repo'),
},
config_filename=flexmock(),
config={'repositories': []},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
),
)
def test_run_actions_with_non_matching_repository_flag_bails():
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').and_return(
False
)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').never()
flexmock(module.command).should_receive('Before_after_hooks').never()
flexmock(borgmatic.actions.repo_create).should_receive('run_repo_create').never()
tuple(
module.run_actions(
arguments={
'global': flexmock(dry_run=False),
'repo-create': flexmock(repository='other-repo'),
},
config_filename=flexmock(),
config={'repositories': []},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
),
)
def test_run_actions_adds_label_file_to_hook_context():
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(),
)
@@ -1001,6 +1057,7 @@ def test_run_actions_adds_label_file_to_hook_context():
def test_run_actions_adds_log_file_to_hook_context():
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(),
)
@@ -1037,6 +1094,7 @@ def test_run_actions_adds_log_file_to_hook_context():
def test_run_actions_runs_transfer():
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(),
)
@@ -1060,6 +1118,7 @@ def test_run_actions_runs_transfer():
def test_run_actions_runs_create():
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(),
)
@@ -1085,6 +1144,7 @@ def test_run_actions_runs_create():
def test_run_actions_with_skip_actions_does_not_run_action_or_action_command_hooks():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return(['create'])
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -1121,6 +1181,7 @@ def test_run_actions_with_skip_actions_does_not_run_action_or_action_command_hoo
def test_run_actions_runs_recreate():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
flexmock(),
)
@@ -1145,6 +1206,7 @@ def test_run_actions_runs_recreate():
def test_run_actions_runs_prune():
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(),
)
@@ -1168,6 +1230,7 @@ def test_run_actions_runs_prune():
def test_run_actions_runs_compact():
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(),
)
@@ -1191,6 +1254,7 @@ def test_run_actions_runs_compact():
def test_run_actions_runs_check_when_repository_enabled_for_checks():
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(),
)
@@ -1215,6 +1279,7 @@ def test_run_actions_runs_check_when_repository_enabled_for_checks():
def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
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(),
)
@@ -1239,6 +1304,7 @@ def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
def test_run_actions_runs_extract():
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(),
)
@@ -1262,6 +1328,7 @@ def test_run_actions_runs_extract():
def test_run_actions_runs_export_tar():
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(),
)
@@ -1285,6 +1352,7 @@ def test_run_actions_runs_export_tar():
def test_run_actions_runs_mount():
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(),
)
@@ -1308,6 +1376,7 @@ def test_run_actions_runs_mount():
def test_run_actions_runs_restore():
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(),
)
@@ -1331,6 +1400,7 @@ def test_run_actions_runs_restore():
def test_run_actions_runs_repo_list():
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(),
)
@@ -1356,6 +1426,7 @@ def test_run_actions_runs_repo_list():
def test_run_actions_runs_list():
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(),
)
@@ -1381,6 +1452,7 @@ def test_run_actions_runs_list():
def test_run_actions_runs_repo_info():
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(),
)
@@ -1406,6 +1478,7 @@ def test_run_actions_runs_repo_info():
def test_run_actions_runs_info():
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(),
)
@@ -1431,6 +1504,7 @@ def test_run_actions_runs_info():
def test_run_actions_runs_break_lock():
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(),
)
@@ -1454,6 +1528,7 @@ def test_run_actions_runs_break_lock():
def test_run_actions_runs_export_key():
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(),
)
@@ -1477,6 +1552,7 @@ def test_run_actions_runs_export_key():
def test_run_actions_runs_import_key():
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(),
)
@@ -1500,6 +1576,7 @@ def test_run_actions_runs_import_key():
def test_run_actions_runs_change_passphrase():
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(),
)
@@ -1526,6 +1603,7 @@ def test_run_actions_runs_change_passphrase():
def test_run_actions_runs_delete():
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(),
)
@@ -1549,6 +1627,7 @@ def test_run_actions_runs_delete():
def test_run_actions_runs_repo_delete():
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(),
)
@@ -1575,6 +1654,7 @@ def test_run_actions_runs_repo_delete():
def test_run_actions_runs_borg():
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(),
)
@@ -1598,6 +1678,7 @@ def test_run_actions_runs_borg():
def test_run_actions_runs_multiple_actions_in_argument_order():
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(),
)
@@ -1626,6 +1707,7 @@ def test_run_actions_runs_multiple_actions_in_argument_order():
def test_run_actions_runs_action_hooks_for_one_action_at_a_time():
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(),
)
+79 -10
View File
@@ -121,7 +121,9 @@ def test_runtime_directory_with_relative_xdg_runtime_dir_errors():
def test_runtime_directory_falls_back_to_runtime_directory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
'/run',
)
flexmock(module.os).should_receive('makedirs')
@@ -133,7 +135,9 @@ def test_runtime_directory_falls_back_to_runtime_directory():
def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
'/run/borgmatic',
)
flexmock(module.os).should_receive('makedirs')
@@ -144,7 +148,9 @@ def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplic
def test_runtime_directory_with_relative_runtime_directory_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
'run',
)
flexmock(module.os).should_receive('makedirs').never()
@@ -156,7 +162,9 @@ def test_runtime_directory_with_relative_runtime_directory_errors():
def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run')
@@ -174,7 +182,9 @@ def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_
def test_runtime_directory_with_relative_tmpdir_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('run')
@@ -188,7 +198,9 @@ def test_runtime_directory_with_relative_tmpdir_errors():
def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -207,7 +219,9 @@ def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_th
def test_runtime_directory_with_relative_temp_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -222,7 +236,9 @@ def test_runtime_directory_with_relative_temp_errors():
def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -242,7 +258,9 @@ def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_
def test_runtime_directory_with_erroring_cleanup_does_not_raise():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
@@ -293,7 +311,9 @@ def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home():
def test_get_borgmatic_state_directory_falls_back_to_state_directory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.STATE_DIRECTORY
).and_return(
'/tmp',
)
@@ -304,3 +324,52 @@ def test_get_borgmatic_state_directory_defaults_to_hard_coded_path():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').and_return(None)
assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic'
def test_resolve_systemd_directory_none():
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
None
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(None)
assert module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY) is None
assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) is None
def test_resolve_systemd_directory_single():
runtime_dir = '/run/borgmatic'
state_dir = '/var/lib/borgmatic'
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
runtime_dir
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
state_dir
)
assert (
module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
== runtime_dir
)
assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) == state_dir
def test_resolve_systemd_directory_multiple():
runtime_dirs = '/run/borgmatic:/run/second:/run/third'
state_dirs = '/var/lib/borgmatic:/var/lib/second:/var/lib/third'
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
runtime_dirs
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
state_dirs
)
assert (
module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
== '/run/borgmatic'
)
assert (
module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY)
== '/var/lib/borgmatic'
)
@@ -450,6 +450,9 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
'/mnt/subvol2',
object,
).and_return(Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/subvol1'),
@@ -457,6 +460,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -465,6 +469,7 @@ def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
'/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
object,
@@ -527,6 +532,9 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
'/mnt/subvol1',
object,
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/subvol1'),
@@ -534,6 +542,7 @@ def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
object,
@@ -594,6 +603,9 @@ def test_dump_data_sources_with_findmnt_command_warns():
'/mnt/subvol1',
object,
).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/subvol1'),
@@ -601,6 +613,7 @@ def test_dump_data_sources_with_findmnt_command_warns():
'/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
object,
@@ -641,6 +654,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
)
flexmock(module).should_receive('snapshot_subvolume').never()
flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
@@ -666,6 +682,9 @@ def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patter
flexmock(module).should_receive('make_snapshot_path').never()
flexmock(module).should_receive('snapshot_subvolume').never()
flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
+52 -1
View File
@@ -203,6 +203,52 @@ def test_inject_pattern_with_root_pattern_prepends_it_along_with_corresponding_i
]
def test_get_last_pattern_index_with_ordered_subset_patterns_finds_last_one():
patterns = [
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/quux'),
]
patterns_subset = [
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
]
assert module.get_last_pattern_index(patterns, patterns_subset) == 2
def test_get_last_pattern_index_with_unordered_subset_patterns_finds_last_one():
patterns = [
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/quux'),
]
patterns_subset = [
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/bar'),
]
assert module.get_last_pattern_index(patterns, patterns_subset) == 2
def test_get_last_pattern_index_with_unknown_subset_patterns_skips_it():
patterns = [
module.borgmatic.borg.pattern.Pattern('/foo'),
module.borgmatic.borg.pattern.Pattern('/bar'),
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/quux'),
]
patterns_subset = [
module.borgmatic.borg.pattern.Pattern('/baz'),
module.borgmatic.borg.pattern.Pattern('/unknown'),
module.borgmatic.borg.pattern.Pattern('/bar'),
]
assert module.get_last_pattern_index(patterns, patterns_subset) == 2
def test_replace_pattern_swaps_out_pattern_in_place():
patterns = [
module.borgmatic.borg.pattern.Pattern('/etc'),
@@ -217,6 +263,7 @@ def test_replace_pattern_swaps_out_pattern_in_place():
'/foo/bar',
type=module.borgmatic.borg.pattern.Pattern_type.EXCLUDE,
),
0,
)
assert patterns == [
@@ -243,6 +290,7 @@ def test_replace_pattern_with_unknown_pattern_falls_back_to_injecting():
patterns,
module.borgmatic.borg.pattern.Pattern('/unknown'),
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
0,
)
@@ -251,20 +299,23 @@ def test_replace_pattern_with_root_pattern_swaps_it_in_along_with_corresponding_
module.borgmatic.borg.pattern.Pattern('/etc'),
module.borgmatic.borg.pattern.Pattern('/var'),
module.borgmatic.borg.pattern.Pattern('/lib'),
module.borgmatic.borg.pattern.Pattern('/run'),
]
module.replace_pattern(
patterns,
module.borgmatic.borg.pattern.Pattern('/var'),
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
2,
)
assert patterns == [
module.borgmatic.borg.pattern.Pattern('/etc'),
module.borgmatic.borg.pattern.Pattern('/foo/bar'),
module.borgmatic.borg.pattern.Pattern('/lib'),
module.borgmatic.borg.pattern.Pattern(
'/foo/bar',
type=module.borgmatic.borg.pattern.Pattern_type.INCLUDE,
),
module.borgmatic.borg.pattern.Pattern('/lib'),
module.borgmatic.borg.pattern.Pattern('/run'),
]
+29
View File
@@ -360,6 +360,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -367,6 +370,7 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -375,6 +379,7 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -396,6 +401,9 @@ def test_dump_data_sources_with_no_logical_volumes_skips_snapshots():
flexmock(module).should_receive('get_logical_volumes').and_return(())
flexmock(module).should_receive('snapshot_logical_volume').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
assert (
@@ -477,6 +485,9 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -484,6 +495,7 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -492,6 +504,7 @@ def test_dump_data_sources_uses_snapshot_size_for_snapshot():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -580,6 +593,9 @@ def test_dump_data_sources_uses_custom_commands():
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -587,6 +603,7 @@ def test_dump_data_sources_uses_custom_commands():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -595,6 +612,7 @@ def test_dump_data_sources_uses_custom_commands():
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -633,6 +651,9 @@ def test_dump_data_sources_with_dry_run_skips_snapshots_and_does_not_touch_patte
flexmock(module).should_receive('snapshot_logical_volume').never()
flexmock(module).should_receive('get_snapshots').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
assert (
@@ -714,6 +735,9 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
logical_volumes[1],
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/lvolume1/subdir'),
@@ -721,6 +745,7 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume1/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
@@ -729,6 +754,7 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
'/run/borgmatic/lvm_snapshots/b33f/./mnt/lvolume2',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -785,6 +811,9 @@ def test_dump_data_sources_with_missing_snapshot_errors():
snapshot_name='lvolume2_borgmatic-1234',
).never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
with pytest.raises(ValueError):
+18
View File
@@ -328,6 +328,9 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
patterns = [Pattern('/mnt/dataset/subdir')]
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/dataset/subdir'),
@@ -335,6 +338,7 @@ def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
'/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
assert (
@@ -355,6 +359,9 @@ def test_dump_data_sources_with_no_datasets_skips_snapshots():
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_dataset').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
patterns = [Pattern('/mnt/dataset')]
@@ -400,6 +407,9 @@ def test_dump_data_sources_uses_custom_commands():
dataset,
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/dataset/subdir'),
@@ -407,6 +417,7 @@ def test_dump_data_sources_uses_custom_commands():
'/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
patterns = [Pattern('/mnt/dataset/subdir')]
hook_config = {
@@ -437,6 +448,9 @@ def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patter
flexmock(module.os).should_receive('getpid').and_return(1234)
flexmock(module).should_receive('snapshot_dataset').never()
flexmock(module).should_receive('mount_snapshot').never()
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
patterns = [Pattern('/mnt/dataset')]
@@ -480,6 +494,9 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
dataset,
'/run/borgmatic',
).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
flexmock(module.borgmatic.hooks.data_source.config).should_receive(
'get_last_pattern_index'
).and_return(0)
flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
object,
Pattern('/mnt/dataset/subdir'),
@@ -487,6 +504,7 @@ def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained
'/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
),
0,
).once()
patterns = [Pattern('/hmm')]
+15 -7
View File
@@ -6,20 +6,23 @@ from borgmatic.hooks.monitoring import sentry as module
@pytest.mark.parametrize(
'state,configured_states,expected_status',
'state,configured_states,configured_environment,expected_status',
(
(borgmatic.hooks.monitoring.monitor.State.START, ['start'], 'in_progress'),
(borgmatic.hooks.monitoring.monitor.State.START, ['start'], None, 'in_progress'),
(
borgmatic.hooks.monitoring.monitor.State.START,
['start', 'finish', 'fail'],
None,
'in_progress',
),
(borgmatic.hooks.monitoring.monitor.State.START, None, 'in_progress'),
(borgmatic.hooks.monitoring.monitor.State.FINISH, ['finish'], 'ok'),
(borgmatic.hooks.monitoring.monitor.State.FAIL, ['fail'], 'error'),
(borgmatic.hooks.monitoring.monitor.State.START, None, 'production', 'in_progress'),
(borgmatic.hooks.monitoring.monitor.State.FINISH, ['finish'], 'development', 'ok'),
(borgmatic.hooks.monitoring.monitor.State.FAIL, ['fail'], 'another-environment', 'error'),
),
)
def test_ping_monitor_constructs_cron_url_and_pings_it(state, configured_states, expected_status):
def test_ping_monitor_constructs_cron_url_and_pings_it(
state, configured_states, configured_environment, expected_status
):
hook_config = {
'data_source_name_url': 'https://5f80ec@o294220.ingest.us.sentry.io/203069',
'monitor_slug': 'test',
@@ -28,8 +31,13 @@ def test_ping_monitor_constructs_cron_url_and_pings_it(state, configured_states,
if configured_states:
hook_config['states'] = configured_states
environment_query = ''
if configured_environment:
hook_config['environment'] = configured_environment
environment_query = f'&environment={configured_environment}'
flexmock(module.requests).should_receive('post').with_args(
f'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status={expected_status}',
f'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status={expected_status}{environment_query}',
timeout=int,
headers={'User-Agent': 'borgmatic'},
).and_return(flexmock(ok=True)).once()
+1 -1
View File
@@ -1,5 +1,5 @@
[tox]
env_list = py39,py310,py311,py312,py313,py314
env_list = py310,py311,py312,py313,py314
skip_missing_interpreters = true
package = editable
min_version = 4.0