diff --git a/NEWS b/NEWS index 99b6e10c..967c30f3 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,7 @@ +2.1.5.dev0 + * #1292: Fix a "source directories do not exist" regression when configuration paths are relative + symlinks and the bootstrap data source hook is enabled. + 2.1.4 * #1266: Add a stand-alone borgmatic Linux binary to the release downloads to serve as another way to install borgmatic. Consider this binary a beta feature. diff --git a/borgmatic/hooks/data_source/bootstrap.py b/borgmatic/hooks/data_source/bootstrap.py index 3b227554..2ab9dac8 100644 --- a/borgmatic/hooks/data_source/bootstrap.py +++ b/borgmatic/hooks/data_source/bootstrap.py @@ -28,17 +28,20 @@ def resolve_config_path_symlinks(path): Given a path, resolve and yield each successive symlink until the final non-symlink target. If the given path isn't a symlink, then just yield it. + The purpose of this is to ensure that configuration files that are behind a symbolic link (or + several) actually get backed up. + Raise ValueError if we have to follow too many symlinks without getting to the final target. ''' - original_path = path + original_path = os.path.normpath(path) for _ in range(MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW): - yield os.path.abspath(path) + yield path if not os.path.islink(path): return - path = os.readlink(path) + path = os.path.normpath(os.path.join(os.path.dirname(path), os.readlink(path))) raise ValueError(f'Too many symlinks to follow for configuration path: {original_path}') diff --git a/pyproject.toml b/pyproject.toml index a8625acc..8b900b8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "borgmatic" -version = "2.1.4" +version = "2.1.5.dev0" authors = [ { name="Dan Helfman", email="witten@torsion.org" }, ] diff --git a/tests/unit/hooks/data_source/test_bootstrap.py b/tests/unit/hooks/data_source/test_bootstrap.py index b768bbc6..bb4881af 100644 --- a/tests/unit/hooks/data_source/test_bootstrap.py +++ b/tests/unit/hooks/data_source/test_bootstrap.py @@ -7,14 +7,12 @@ from borgmatic.hooks.data_source import bootstrap as module def test_resolve_config_path_symlinks_passes_through_non_symlink(): - flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path) flexmock(module.os.path).should_receive('islink').and_return(False) assert tuple(module.resolve_config_path_symlinks('test.yaml')) == ('test.yaml',) def test_resolve_config_path_symlinks_follows_each_symlink(): - flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path) flexmock(module.os.path).should_receive('islink').with_args('test.yaml').and_return(True) flexmock(module.os.path).should_receive('islink').with_args('dest1.yaml').and_return(True) flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(False) @@ -29,9 +27,29 @@ def test_resolve_config_path_symlinks_follows_each_symlink(): ) +def test_resolve_config_path_symlinks_follows_each_relative_symlink(): + flexmock(module.os.path).should_receive('islink').with_args('foo/bar/test.yaml').and_return( + True + ) + flexmock(module.os.path).should_receive('islink').with_args('foo/dest1.yaml').and_return(True) + flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(False) + flexmock(module.os).should_receive('readlink').with_args('foo/bar/test.yaml').and_return( + '../dest1.yaml' + ) + flexmock(module.os).should_receive('readlink').with_args('foo/dest1.yaml').and_return( + '../dest2.yaml' + ) + flexmock(module.os).should_receive('readlink').with_args('dest2.yaml').never() + + assert tuple(module.resolve_config_path_symlinks('foo/bar/test.yaml')) == ( + 'foo/bar/test.yaml', + 'foo/dest1.yaml', + 'dest2.yaml', + ) + + def test_resolve_config_path_symlinks_with_too_many_symlinks_raises(): flexmock(module).MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW = 2 - flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path) flexmock(module.os.path).should_receive('islink').with_args('test.yaml').and_return(True) flexmock(module.os.path).should_receive('islink').with_args('dest1.yaml').and_return(True) flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(True)