Fix a "source directories do not exist" regression when configuration paths are relative symlinks and the bootstrap data source hook is enabled (#1292).
All checks were successful
build / test (push) Successful in 7m25s
build / docs (push) Successful in 1m26s

This commit is contained in:
Dan Helfman 2026-04-06 16:03:39 -07:00
commit f3ae04225d
4 changed files with 32 additions and 7 deletions

4
NEWS
View file

@ -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.

View file

@ -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}')

View file

@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "2.1.4"
version = "2.1.5.dev0"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]

View file

@ -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)