0
0
Fork 0
mirror of https://projects.torsion.org/witten/borgmatic.git synced 2025-04-10 15:37:33 +00:00

Fix a "no such file or directory" error in ZFS, Btrfs, and LVM hooks with nested directories that reside on separate devices/filesystems ().

This commit is contained in:
Dan Helfman 2025-03-30 14:55:54 -07:00
parent 5cea1e1b72
commit ab01e97a5e
3 changed files with 72 additions and 12 deletions
NEWS
borgmatic/hooks/data_source
tests/unit/hooks/data_source

2
NEWS
View file

@ -22,6 +22,8 @@
"working_directory" are used.
* #1044: Fix an error in the systemd credential hook when the credential name contains a "."
character.
* #1048: Fix a "no such file or directory" error in ZFS, Btrfs, and LVM hooks with nested
directories that reside on separate devices/filesystems.
1.9.14
* #409: With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the

View file

@ -1,3 +1,4 @@
import os
import pathlib
IS_A_HOOK = False
@ -11,6 +12,10 @@ def get_contained_patterns(parent_directory, candidate_patterns):
paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at /var, then
/var is what we want to snapshot.
If a parent directory and a candidate pattern are on different devices, skip the pattern. That's
because any snapshot of a parent directory won't actually include "contained" directories if
they reside on separate devices.
For this function to work, a candidate pattern path can't have any globs or other non-literal
characters in the initial portion of the path that matches the parent directory. For instance, a
parent directory of /var would match a candidate pattern path of /var/log/*/data, but not a
@ -27,6 +32,8 @@ def get_contained_patterns(parent_directory, candidate_patterns):
if not candidate_patterns:
return ()
parent_device = os.stat(parent_directory).st_dev
contained_patterns = tuple(
candidate
for candidate in candidate_patterns
@ -35,6 +42,7 @@ def get_contained_patterns(parent_directory, candidate_patterns):
pathlib.PurePath(parent_directory) == candidate_path
or pathlib.PurePath(parent_directory) in candidate_path.parents
)
if candidate.device == parent_device
)
candidate_patterns -= set(contained_patterns)

View file

@ -1,34 +1,84 @@
from flexmock import flexmock
from borgmatic.borg.pattern import Pattern
from borgmatic.hooks.data_source import snapshot as module
def test_get_contained_patterns_without_candidates_returns_empty():
flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=flexmock()))
assert module.get_contained_patterns('/mnt', {}) == ()
def test_get_contained_patterns_with_self_candidate_returns_self():
candidates = {Pattern('/foo'), Pattern('/mnt'), Pattern('/bar')}
device = flexmock()
flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
candidates = {
Pattern('/foo', device=device),
Pattern('/mnt', device=device),
Pattern('/bar', device=device),
}
assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt'),)
assert candidates == {Pattern('/foo'), Pattern('/bar')}
assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt', device=device),)
assert candidates == {Pattern('/foo', device=device), Pattern('/bar', device=device)}
def test_get_contained_patterns_with_self_candidate_and_caret_prefix_returns_self():
candidates = {Pattern('^/foo'), Pattern('^/mnt'), Pattern('^/bar')}
device = flexmock()
flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
candidates = {
Pattern('^/foo', device=device),
Pattern('^/mnt', device=device),
Pattern('^/bar', device=device),
}
assert module.get_contained_patterns('/mnt', candidates) == (Pattern('^/mnt'),)
assert candidates == {Pattern('^/foo'), Pattern('^/bar')}
assert module.get_contained_patterns('/mnt', candidates) == (Pattern('^/mnt', device=device),)
assert candidates == {Pattern('^/foo', device=device), Pattern('^/bar', device=device)}
def test_get_contained_patterns_with_child_candidate_returns_child():
candidates = {Pattern('/foo'), Pattern('/mnt/subdir'), Pattern('/bar')}
device = flexmock()
flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
candidates = {
Pattern('/foo', device=device),
Pattern('/mnt/subdir', device=device),
Pattern('/bar', device=device),
}
assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt/subdir'),)
assert candidates == {Pattern('/foo'), Pattern('/bar')}
assert module.get_contained_patterns('/mnt', candidates) == (
Pattern('/mnt/subdir', device=device),
)
assert candidates == {Pattern('/foo', device=device), Pattern('/bar', device=device)}
def test_get_contained_patterns_with_grandchild_candidate_returns_child():
candidates = {Pattern('/foo'), Pattern('/mnt/sub/dir'), Pattern('/bar')}
device = flexmock()
flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=device))
candidates = {
Pattern('/foo', device=device),
Pattern('/mnt/sub/dir', device=device),
Pattern('/bar', device=device),
}
assert module.get_contained_patterns('/mnt', candidates) == (Pattern('/mnt/sub/dir'),)
assert candidates == {Pattern('/foo'), Pattern('/bar')}
assert module.get_contained_patterns('/mnt', candidates) == (
Pattern('/mnt/sub/dir', device=device),
)
assert candidates == {Pattern('/foo', device=device), Pattern('/bar', device=device)}
def test_get_contained_patterns_ignores_child_candidate_on_another_device():
one_device = flexmock()
another_device = flexmock()
flexmock(module.os).should_receive('stat').and_return(flexmock(st_dev=one_device))
candidates = {
Pattern('/foo', device=one_device),
Pattern('/mnt/subdir', device=another_device),
Pattern('/bar', device=one_device),
}
assert module.get_contained_patterns('/mnt', candidates) == ()
assert candidates == {
Pattern('/foo', device=one_device),
Pattern('/mnt/subdir', device=another_device),
Pattern('/bar', device=one_device),
}