diff --git a/NEWS b/NEWS index 6bd9ece3f..102967909 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,6 @@ +1.1.9.dev0 + * #29: Support for using tilde in source directory path to reference home directory. + 1.1.8 * #39: Fix to make /etc/borgmatic/config.yaml optional rather than required when using the default config paths. diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index c2f2cf34a..47315dd10 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -14,6 +14,16 @@ def initialize(storage_config): os.environ['BORG_PASSPHRASE'] = passphrase +def _expand_directory(directory): + ''' + Given a directory path, expand any tilde (representing a user's home directory) and any globs + therein. Return a list of one or more resulting paths. + ''' + expanded_directory = os.path.expanduser(directory) + + return glob.glob(expanded_directory) or [expanded_directory] + + def _write_exclude_file(exclude_patterns=None): ''' Given a sequence of exclude patterns, write them to a named temporary file and return it. Return @@ -59,7 +69,7 @@ def create_archive( ''' sources = tuple( itertools.chain.from_iterable( - glob.glob(directory) or [directory] + _expand_directory(directory) for directory in location_config['source_directories'] ) ) @@ -92,4 +102,4 @@ def create_archive( ) + sources + exclude_flags + compression_flags + one_file_system_flags + \ remote_path_flags + umask_flags + verbosity_flags - subprocess.check_call(full_command) \ No newline at end of file + subprocess.check_call(full_command) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 74d99fe83..d559ee3f3 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -12,7 +12,8 @@ map: required: true seq: - type: scalar - desc: List of source directories to backup (required). Globs are expanded. + desc: | + List of source directories to backup (required). Globs and tildes are expanded. example: - /home - /etc diff --git a/borgmatic/tests/unit/borg/test_create.py b/borgmatic/tests/unit/borg/test_create.py index 743c0946d..ebbee972f 100644 --- a/borgmatic/tests/unit/borg/test_create.py +++ b/borgmatic/tests/unit/borg/test_create.py @@ -28,6 +28,24 @@ def test_initialize_without_passphrase_should_not_set_environment(): os.environ = orig_environ +def test_expand_directory_with_basic_path_passes_it_through(): + flexmock(module.os.path).should_receive('expanduser').and_return('foo') + flexmock(module.glob).should_receive('glob').and_return([]) + + paths = module._expand_directory('foo') + + assert paths == ['foo'] + + +def test_expand_directory_with_glob_expands(): + flexmock(module.os.path).should_receive('expanduser').and_return('foo*') + flexmock(module.glob).should_receive('glob').and_return(['foo', 'food']) + + paths = module._expand_directory('foo*') + + assert paths == ['foo', 'food'] + + def test_write_exclude_file_does_not_raise(): temporary_file = flexmock( name='filename', @@ -122,7 +140,8 @@ DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' CREATE_COMMAND = ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'bar') -def test_create_archive_should_call_borg_with_parameters(): +def test_create_archive_calls_borg_with_parameters(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(CREATE_COMMAND) @@ -139,8 +158,9 @@ def test_create_archive_should_call_borg_with_parameters(): ) -def test_create_archive_with_exclude_patterns_should_call_borg_with_excludes(): +def test_create_archive_with_exclude_patterns_calls_borg_with_excludes(): exclude_flags = ('--exclude-from', 'excludes') + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(flexmock(name='/tmp/excludes')) flexmock(module).should_receive('_make_exclude_flags').and_return(exclude_flags) insert_subprocess_mock(CREATE_COMMAND + exclude_flags) @@ -157,7 +177,8 @@ def test_create_archive_with_exclude_patterns_should_call_borg_with_excludes(): ) -def test_create_archive_with_verbosity_some_should_call_borg_with_info_parameter(): +def test_create_archive_with_verbosity_some_calls_borg_with_info_parameter(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(CREATE_COMMAND + ('--info', '--stats',)) @@ -174,7 +195,8 @@ def test_create_archive_with_verbosity_some_should_call_borg_with_info_parameter ) -def test_create_archive_with_verbosity_lots_should_call_borg_with_debug_parameter(): +def test_create_archive_with_verbosity_lots_calls_borg_with_debug_parameter(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(CREATE_COMMAND + ('--debug', '--list', '--stats')) @@ -191,7 +213,8 @@ def test_create_archive_with_verbosity_lots_should_call_borg_with_debug_paramete ) -def test_create_archive_with_compression_should_call_borg_with_compression_parameters(): +def test_create_archive_with_compression_calls_borg_with_compression_parameters(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(CREATE_COMMAND + ('--compression', 'rle')) @@ -208,7 +231,8 @@ def test_create_archive_with_compression_should_call_borg_with_compression_param ) -def test_create_archive_with_one_file_system_should_call_borg_with_one_file_system_parameters(): +def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameters(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(CREATE_COMMAND + ('--one-file-system',)) @@ -226,7 +250,8 @@ def test_create_archive_with_one_file_system_should_call_borg_with_one_file_syst ) -def test_create_archive_with_remote_path_should_call_borg_with_remote_path_parameters(): +def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(CREATE_COMMAND + ('--remote-path', 'borg1')) @@ -244,7 +269,8 @@ def test_create_archive_with_remote_path_should_call_borg_with_remote_path_param ) -def test_create_archive_with_umask_should_call_borg_with_umask_parameters(): +def test_create_archive_with_umask_calls_borg_with_umask_parameters(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(CREATE_COMMAND + ('--umask', '740')) @@ -262,6 +288,7 @@ def test_create_archive_with_umask_should_call_borg_with_umask_parameters(): def test_create_archive_with_source_directories_glob_expands(): + flexmock(module).should_receive('_expand_directory').and_return(['foo', 'food']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food')) @@ -280,6 +307,7 @@ def test_create_archive_with_source_directories_glob_expands(): def test_create_archive_with_non_matching_source_directories_glob_passes_through(): + flexmock(module).should_receive('_expand_directory').and_return(['foo*']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*')) @@ -297,11 +325,11 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through ) -def test_create_archive_with_glob_should_call_borg_with_expanded_directories(): +def test_create_archive_with_glob_calls_borg_with_expanded_directories(): + flexmock(module).should_receive('_expand_directory').and_return(['foo', 'food']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food')) - flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food']) module.create_archive( verbosity=None, @@ -315,7 +343,8 @@ def test_create_archive_with_glob_should_call_borg_with_expanded_directories(): ) -def test_create_archive_with_archive_name_format_without_placeholders(): +def test_create_archive_with_archive_name_format_calls_borg_with_archive_name(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar')) @@ -335,6 +364,7 @@ def test_create_archive_with_archive_name_format_without_placeholders(): def test_create_archive_with_archive_name_format_accepts_borg_placeholders(): + flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar']) flexmock(module).should_receive('_write_exclude_file').and_return(None) flexmock(module).should_receive('_make_exclude_flags').and_return(()) insert_subprocess_mock(('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar')) diff --git a/setup.py b/setup.py index d3b8307e6..d23552be3 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.1.8' +VERSION = '1.1.9.dev0' setup(