From 7e1cb691b642310f9f6ceb020566d2c3f7a95f13 Mon Sep 17 00:00:00 2001 From: Julien Cornebise Date: Sat, 3 Jan 2026 20:54:27 +0100 Subject: [PATCH 1/2] Add "unsafe_create_without_precheck" option to skip pre-backup validation (#1221). This option allows users to skip the dry-run validation that checks for special files and runtime directory exclusions. This can significantly improve performance for large filesystems with millions of files, where the validation can take hours. The option name includes "unsafe" to communicate risk, and a warning is logged on each backup showing the runtime directory path and advising users to verify their exclude patterns manually. --- borgmatic/borg/create.py | 31 +++++++++++------- borgmatic/config/schema.yaml | 16 ++++++++++ tests/unit/borg/test_create.py | 58 ++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 54a09a83..7ba24b22 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -138,7 +138,7 @@ def validate_planned_backup_paths( MAX_SPECIAL_FILE_PATHS_LENGTH = 1000 -def make_base_create_command( +def make_base_create_command( # noqa: PLR0912 dry_run, repository_path, config, @@ -249,16 +249,25 @@ def make_base_create_command( ) working_directory = borgmatic.config.paths.get_working_directory(config) - logger.debug('Checking file paths Borg plans to include') - planned_backup_paths = validate_planned_backup_paths( - dry_run, - create_flags + create_positional_arguments, - config, - patterns, - local_path, - working_directory, - borgmatic_runtime_directory=borgmatic_runtime_directory, - ) + if config.get('unsafe_create_without_precheck'): + logger.warning( + 'Skipping pre-backup safety checks due to "unsafe_create_without_precheck" option. ' + 'If using database hooks: (1) Borg may hang on special files, and (2) database dumps ' + 'may be silently excluded if your excludes match the runtime directory ' + f'({borgmatic_runtime_directory}). Verify your exclude patterns manually.' + ) + planned_backup_paths = () + else: + logger.debug('Checking file paths Borg plans to include') + planned_backup_paths = validate_planned_backup_paths( + dry_run, + create_flags + create_positional_arguments, + config, + patterns, + local_path, + working_directory, + borgmatic_runtime_directory=borgmatic_runtime_directory, + ) # If database hooks are enabled (as indicated by streaming processes), exclude files that might # cause Borg to hang. But skip this if the user has explicitly set the "read_special" to True. diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 19253fd1..c45bef95 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -144,6 +144,22 @@ properties: false. But when a database hook is used, the setting here is ignored and read_special is considered true. example: true + unsafe_create_without_precheck: + type: boolean + description: | + Skip pre-backup safety checks: (1) runtime directory exclusion + detection, and (2) special file exclusion for database streaming. + Can significantly improve performance for large filesystems. + + UNSAFE because if using database hooks: Borg may hang on special + files (named pipes, devices), and database dumps may be silently + excluded if your exclude_patterns match the runtime directory + (see "user_runtime_directory" option, defaults to $XDG_RUNTIME_DIR + or $TMPDIR/borgmatic-*). + + Only enable if not using database hooks, or after manually verifying + your excludes don't affect the runtime directory. Defaults to false. + example: true flags: type: boolean description: | diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index ba693c77..ebad6564 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -1019,6 +1019,64 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command(): assert not pattern_file +def test_make_base_create_command_with_unsafe_create_without_precheck_skips_validation(): + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( + '{hostname}', + ) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{module.flags.get_default_archive_name_format()}',), + ) + flexmock(module).should_receive('validate_planned_backup_paths').never() + flexmock(module.logger).should_receive('warning').once() + + module.make_base_create_command( + dry_run=False, + repository_path='repo', + config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + 'unsafe_create_without_precheck': True, + }, + patterns=[Pattern('foo'), Pattern('bar')], + local_borg_version='1.2.3', + global_arguments=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', + ) + + +def test_make_base_create_command_without_unsafe_create_without_precheck_calls_validation(): + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) + flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') + flexmock(module.flags).should_receive('get_default_archive_name_format').and_return( + '{hostname}', + ) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(()) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + (f'repo::{module.flags.get_default_archive_name_format()}',), + ) + flexmock(module).should_receive('validate_planned_backup_paths').once().and_return(()) + + module.make_base_create_command( + dry_run=False, + repository_path='repo', + config={ + 'source_directories': ['foo', 'bar'], + 'repositories': ['repo'], + }, + patterns=[Pattern('foo'), Pattern('bar')], + local_borg_version='1.2.3', + global_arguments=flexmock(), + borgmatic_runtime_directory='/run/borgmatic', + ) + + def test_make_base_create_command_with_non_existent_directory_and_source_directories_must_exist_raises(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) flexmock(module.borgmatic.borg.pattern).should_receive( From c08f1b8178742e88c25b7f79b0a6872c4439792a Mon Sep 17 00:00:00 2001 From: Julien Cornebise Date: Sun, 4 Jan 2026 22:02:47 +0000 Subject: [PATCH 2/2] Rename unsafe_create_without_precheck to unsafe_skip_path_validation_before_create and address PR feedback (#1222). - Use skip_ prefix consistent with other borgmatic options - Reduce runtime warning to single line (users read the config docs) - Broaden "database dumps" to "data from borgmatic data source hooks" - Simplify schema docs by referencing user_runtime_directory instead of listing defaults --- borgmatic/borg/create.py | 7 ++----- borgmatic/config/schema.yaml | 16 ++++++++-------- tests/unit/borg/test_create.py | 6 +++--- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 7ba24b22..d9248332 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -249,12 +249,9 @@ def make_base_create_command( # noqa: PLR0912 ) working_directory = borgmatic.config.paths.get_working_directory(config) - if config.get('unsafe_create_without_precheck'): + if config.get('unsafe_skip_path_validation_before_create'): logger.warning( - 'Skipping pre-backup safety checks due to "unsafe_create_without_precheck" option. ' - 'If using database hooks: (1) Borg may hang on special files, and (2) database dumps ' - 'may be silently excluded if your excludes match the runtime directory ' - f'({borgmatic_runtime_directory}). Verify your exclude patterns manually.' + 'Skipping pre-backup path validation due to "unsafe_skip_path_validation_before_create" option.' ) planned_backup_paths = () else: diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index c45bef95..48defd06 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -144,21 +144,21 @@ properties: false. But when a database hook is used, the setting here is ignored and read_special is considered true. example: true - unsafe_create_without_precheck: + unsafe_skip_path_validation_before_create: type: boolean description: | Skip pre-backup safety checks: (1) runtime directory exclusion detection, and (2) special file exclusion for database streaming. Can significantly improve performance for large filesystems. - UNSAFE because if using database hooks: Borg may hang on special - files (named pipes, devices), and database dumps may be silently - excluded if your exclude_patterns match the runtime directory - (see "user_runtime_directory" option, defaults to $XDG_RUNTIME_DIR - or $TMPDIR/borgmatic-*). + UNSAFE because if using data source hooks: Borg may hang on special + files (named pipes, devices), and data from borgmatic data source + hooks may be silently excluded if your exclude_patterns match the + runtime directory (see "user_runtime_directory" option). - Only enable if not using database hooks, or after manually verifying - your excludes don't affect the runtime directory. Defaults to false. + Only enable if not using data source hooks, or after manually + verifying your excludes don't affect the runtime directory. Defaults + to false. example: true flags: type: boolean diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index ebad6564..bf63d197 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -1019,7 +1019,7 @@ def test_make_base_create_command_includes_extra_borg_options_in_borg_command(): assert not pattern_file -def test_make_base_create_command_with_unsafe_create_without_precheck_skips_validation(): +def test_make_base_create_command_with_unsafe_skip_path_validation_before_create_skips_validation(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO') @@ -1040,7 +1040,7 @@ def test_make_base_create_command_with_unsafe_create_without_precheck_skips_vali config={ 'source_directories': ['foo', 'bar'], 'repositories': ['repo'], - 'unsafe_create_without_precheck': True, + 'unsafe_skip_path_validation_before_create': True, }, patterns=[Pattern('foo'), Pattern('bar')], local_borg_version='1.2.3', @@ -1049,7 +1049,7 @@ def test_make_base_create_command_with_unsafe_create_without_precheck_skips_vali ) -def test_make_base_create_command_without_unsafe_create_without_precheck_calls_validation(): +def test_make_base_create_command_without_unsafe_skip_path_validation_before_create_calls_validation(): flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(None) flexmock(module.borgmatic.borg.flags).should_receive('make_list_filter_flags').and_return('FOO')