Skip to content
This repository was archived by the owner on Jun 15, 2025. It is now read-only.

Commit fc3bef3

Browse files
committed
Allow Repository to work on relative paths
A previous change adjusted the Repository class to work with absolute paths, even if a relative one was passed in. With the introduction of remote directories this approach is no longer feasible: in general we want to avoid executing (and thus, relying, on) anything except what is truly necessary (think: the btrfs suite of programs) on the remote end of the connection. This includes utilities such as realpath and whatnot to canonicalize paths. This change allows for relative paths to be accepted on various repository based operations.
1 parent c7f0f8c commit fc3bef3

File tree

2 files changed

+106
-10
lines changed

2 files changed

+106
-10
lines changed

btrfs-backup/src/deso/btrfs/repository.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,15 @@
5050
pipeline,
5151
)
5252
from os import (
53+
curdir,
54+
pardir,
5355
sep,
5456
uname,
5557
)
5658
from os.path import (
57-
abspath,
5859
dirname,
60+
expanduser,
61+
isabs,
5962
isdir,
6063
join,
6164
realpath,
@@ -234,7 +237,6 @@ def isDir(directory):
234237
def _findRoot(directory, repository):
235238
"""Find the root of the btrfs file system containing the given directory."""
236239
assert directory
237-
assert directory == abspath(directory)
238240

239241
# Note that we have no guard here against an empty directory as input
240242
# or later because of a dirname invocation. However, the show command
@@ -509,7 +511,6 @@ def sync(subvolumes, src, dst):
509511
def _restore(subvolume, src, dst, snapshots, snapshots_only):
510512
"""Restore a snapshot/subvolume by transferal from another repository."""
511513
snapshot = _findMostRecent(snapshots, subvolume)
512-
subvolume = realpath(subvolume)
513514

514515
# In case the given source repository does not contain any snapshots
515516
# for the given subvolume we cannot do anything but signal that to
@@ -552,7 +553,7 @@ def restore(subvolumes, src, dst, snapshots_only=False):
552553
snapshots = src.snapshots()
553554

554555
for subvolume in subvolumes:
555-
_restore(subvolume, src, dst, snapshots, snapshots_only)
556+
_restore(realpath(subvolume), src, dst, snapshots, snapshots_only)
556557

557558

558559
def _diff(snapshot, subvolume, repository):
@@ -618,18 +619,31 @@ def _untrail(path):
618619
return path
619620

620621

622+
def _relativize(path):
623+
"""Adjust a relative path to make it point to the current directory."""
624+
if expanduser(path) == path and\
625+
not path.startswith(curdir) and\
626+
not path.startswith(pardir) and\
627+
not isabs(path):
628+
return join(curdir, path)
629+
630+
return path
631+
632+
621633
class RepositoryBase:
622634
"""This class represents the base class for repositories for snapshots."""
623635
def __init__(self, directory, filters=None, read_err=True,
624636
remote_cmd=None):
625637
"""Initialize the object and bind it to the given directory."""
626-
# We always work with absolute paths here.
627-
directory = abspath(directory)
628-
629638
self._filters = filters
630639
self._read_err = read_err
631640
self._remote_cmd = remote_cmd
632-
self._directory = _trail(directory)
641+
# In order to properly handle relative paths correctly we need them
642+
# to start with the system's indicator for the current directory.
643+
# TODO: We could use a better story for path handling. The main
644+
# concern are probably character based path comparisons (for
645+
# prefixes, for instance).
646+
self._directory = _trail(_relativize(directory))
633647

634648
def snapshots(self):
635649
"""Retrieve a list of snapshots in this repository."""
@@ -690,7 +704,7 @@ class Repository(RepositoryBase):
690704
def __init__(self, directory, *args, **kwargs):
691705
"""Initialize the repository, query its root in the btrfs file system."""
692706
super().__init__(directory, *args, **kwargs)
693-
self._root = _findRoot(abspath(directory), self)
707+
self._root = _findRoot(_untrail(self._directory), self)
694708

695709

696710
def snapshots(self):
@@ -732,7 +746,7 @@ def purge(self, subvolumes, duration):
732746
snapshots = self.snapshots()
733747

734748
for subvolume in subvolumes:
735-
_purge(subvolume, self, duration, snapshots)
749+
_purge(realpath(subvolume), self, duration, snapshots)
736750

737751

738752
def diff(self, snapshot, subvolume):

btrfs-backup/src/deso/btrfs/test/testRepository.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
Repository,
4040
restore,
4141
_findCommonSnapshots,
42+
_relativize,
4243
_snapshots,
4344
sync as syncRepos,
4445
_trail,
@@ -52,6 +53,9 @@
5253
make,
5354
Mount,
5455
)
56+
from deso.cleanup import (
57+
defer,
58+
)
5559
from deso.execute import (
5660
execute,
5761
findCommand,
@@ -60,6 +64,8 @@
6064
glob,
6165
)
6266
from os import (
67+
chdir,
68+
getcwd,
6369
remove,
6470
)
6571
from os.path import (
@@ -96,6 +102,15 @@ def testPathUntrail(self):
96102
self.assertEqual(_untrail("/home/user/"), "/home/user")
97103

98104

105+
def testRelativize(self):
106+
"""Verify that the detection and adjustment of relative paths is sane."""
107+
self.assertEqual(_relativize("."), ".")
108+
self.assertEqual(_relativize("test"), "./test")
109+
self.assertEqual(_relativize("test/"), "./test/")
110+
self.assertEqual(_relativize("../test/"), "../test/")
111+
self.assertEqual(_relativize("~/test"), "~/test")
112+
113+
99114
def testRepositoryInNonExistentDirectory(self):
100115
"""Verify that creation of a repository in a non-existent directory fails."""
101116
directory = "a-non-existent-directory"
@@ -830,5 +845,72 @@ def testRepositoryPurge(self):
830845
self.assertEqual(src.snapshots(), [most_recent])
831846

832847

848+
def runRelativeSubvolumeTest(self, file_repo):
849+
"""Perform a sync and restore using only relative paths for repositories and subvolumes."""
850+
with defer() as d:
851+
# Under all circumstances we should change back the working
852+
# directory. Leaving it in the btrfs root causes the cleanup to
853+
# fail.
854+
# Note that we need to retrieve the current working directory
855+
# outside of the lambda to have it evaluated before we change it.
856+
cwd = getcwd()
857+
d.defer(lambda: chdir(cwd))
858+
859+
with alias(self._mount) as m:
860+
# All subvolumes are specified relative to our btrfs root.
861+
chdir(m.path())
862+
863+
make(m, "root", subvol=True)
864+
make(m, "snapshots")
865+
make(m, "backup")
866+
867+
# Use relative path here.
868+
subvolume = "root"
869+
src = Repository("snapshots")
870+
if not file_repo:
871+
dst = Repository("backup")
872+
else:
873+
recv_filter = [[_DD, "of={file}"]]
874+
dst = FileRepository("backup", ".bin", recv_filter)
875+
876+
# We test all main operations on a repository with relative
877+
# paths here. Start with the sync.
878+
syncRepos([subvolume], src, dst)
879+
880+
snap, = src.snapshots()
881+
make(m, "root", "file1", data=b"test")
882+
# Next the changed file detection.
883+
self.assertNotEqual(src.diff(snap["path"], subvolume), [])
884+
885+
# We need a second snapshot to test purging.
886+
syncRepos([subvolume], src, dst)
887+
888+
if not file_repo:
889+
# And test purging. Note that purging is currently unavailable
890+
# for file repositories.
891+
dst.purge([subvolume], timedelta(minutes=0))
892+
else:
893+
# For file repositories we need to adjust the destination
894+
# slightly because now we send from it (so we need a different
895+
# filter).
896+
send_filter = [[_DD, "if={file}"]]
897+
dst = FileRepository("backup", ".bin", send_filter)
898+
899+
# And last but not least test restoration.
900+
execute(*delete(m.path(subvolume)))
901+
execute(*delete(src.path(snap["path"])))
902+
restore([subvolume], dst, src)
903+
904+
905+
def testRepositoryRelativeSubvolumes(self):
906+
"""Verify that normal repositories can handle relative subvolume paths."""
907+
self.runRelativeSubvolumeTest(False)
908+
909+
910+
def testFileRepositoryRelativeSubvolumes(self):
911+
"""Verify that file repositories can handle relative subvolume paths."""
912+
self.runRelativeSubvolumeTest(True)
913+
914+
833915
if __name__ == "__main__":
834916
main()

0 commit comments

Comments
 (0)