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

Commit 9911edb

Browse files
committed
Factor out replaceFileString functions
Another task to be performed in a different fashion depending on whether a list of filter commands is a "simple" pipeline or a spring is the replacement of '{file}' string with the actual file name when performing the backup or restore operation. This change factors out a new function that performs the proper replacement, depending on the type of the commands to execute. The behavior is largely different between the two types: in case of a pipeline we replicate the '{file}' string for all the files we want to expand it to and perform the replacement. Associated (long) options are replicated as well. For springs, however, we clone the entire command and substitute the string. The reason for this behavior is that the introduction of spring support was based on the premise that certain programs cannot handle multiple files. So, in such a case we have to invoke the program multiple times instead--with each instance operating on a different file.
1 parent 3502d29 commit 9911edb

File tree

3 files changed

+143
-24
lines changed

3 files changed

+143
-24
lines changed

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

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919

2020
"""Functionality related to handling commands in various forms."""
2121

22+
from copy import (
23+
deepcopy,
24+
)
25+
from deso.execute import (
26+
formatCommands,
27+
)
28+
2229

2330
def isSpring(command):
2431
"""Check if a command array actually describes a spring."""
@@ -55,12 +62,33 @@ def checkFileString(command):
5562
return _checkFileStringInCommand(command)
5663

5764

58-
def replaceFileString(command, files):
59-
"""Replace the {file} string in a command with the actual file name.
65+
def _replaceFileStringInSpring(commands, files):
66+
"""Replace the {file} string in a spring with the actual file name(s)."""
67+
def replicate(command, file_):
68+
"""Replicate and command containing a {file} string and replace it with the actual file."""
69+
replica = deepcopy(command)
70+
result = _replaceFileStringInCommand(replica, [file_])
71+
assert result, replica
72+
return replica
6073

61-
Note: Any replacement is performed on the actual data, i.e., without
62-
making a copy beforehand.
63-
"""
74+
assert isSpring(commands), commands
75+
76+
# Note that we do not insist here on the {file} string being located
77+
# in the first command of the spring. Not sure about use cases where
78+
# it would not be there but we leave that open to users.
79+
for i, command in enumerate(commands):
80+
if "{file}" in " ".join(command):
81+
# For a spring we replicate the entire command containing the
82+
# '{file}' string (as opposed to the option associated with it, if
83+
# any) and insert a duplicate into the list of commands.
84+
commands[i:i+1] = [replicate(command, f) for f in files]
85+
return True
86+
87+
return False
88+
89+
90+
def _replaceFileStringInCommand(command, files):
91+
"""Replace the {file} string in a command with the actual file name(s)."""
6492
# TODO: This function replicates the argument that contains the {file}
6593
# string to allow not only for lists of snapshot files in a
6694
# consecutive fashion (i.e., "/bin/cat {file}" is expanded to
@@ -86,3 +114,29 @@ def replaceFileString(command, files):
86114
return True
87115

88116
return False
117+
118+
119+
def replaceFileString(command, files):
120+
"""Replace the {file} string in a command with the actual snapshot name.
121+
122+
Note: Any replacement is performed on the actual data, i.e., without
123+
making a copy beforehand.
124+
"""
125+
def raiseError(commands):
126+
"""Raise an error telling the user that no {file} string was found."""
127+
error = "Replacement string {{file}} not found in: \"{cmd}\""
128+
error = error.format(cmd=formatCommands(commands))
129+
raise NameError(error)
130+
131+
assert isinstance(files, list), files
132+
133+
# The command might actually be a list of commands in case of a
134+
# spring. In that case our replacement looks slightly different.
135+
if isSpring(command):
136+
if not _replaceFileStringInSpring(command, files):
137+
raiseError(command)
138+
else:
139+
if not _replaceFileStringInCommand(command, files):
140+
raiseError([command])
141+
142+
return command

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

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949
)
5050
from deso.execute import (
5151
execute,
52-
formatCommands,
5352
pipeline,
5453
)
5554
from os import (
@@ -855,22 +854,13 @@ def parents(self, snapshot, subvolume, snapshots=None, dst_snapshots=None):
855854

856855
def _filterPipeline(self, index, snapshots):
857856
"""Create a pipeline out of the filter commands."""
858-
def replaceFiles(command, snapshots):
859-
"""Replace the {file} string in a command with the actual snapshot name."""
860-
if not replaceFileString(command, snapshots):
861-
error = "Replacement string {{file}} not found in command: \"{cmd}\""
862-
error = error.format(cmd=formatCommands(command))
863-
raise NameError(error)
864-
else:
865-
return command
866-
867857
# Convert all relative snapshot paths into absolute ones with the
868858
# appropriate extension.
869859
with alias(self._extension) as ext:
870860
snapshots = ["%s%s" % (self.path(s), ext) for s in snapshots]
871861

872862
commands = deepcopy(self._filters)
873-
func = lambda: replaceFiles(commands[index], snapshots)
863+
func = lambda: replaceFileString(commands[index], snapshots)
874864
# At least one command in the filters must contain a string {file}
875865
# which is now replaced by the actual snapshot name.
876866
commands[index] = self.command(func)

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

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
checkFileString,
2424
isSpring,
2525
replaceFileString,
26+
_replaceFileStringInCommand,
27+
_replaceFileStringInSpring,
2628
)
2729
from unittest import (
2830
main,
@@ -62,31 +64,104 @@ def testCheckFileString(self):
6264
self.assertFalse(checkFileString(command))
6365

6466

65-
def testReplaceFileString(self):
66-
"""Verify that the replaceFileString function works as expected."""
67-
self.assertFalse(replaceFileString(["cat"], ["test"]), None)
68-
self.assertFalse(replaceFileString(["cat", "-o"], ["test"]), None)
67+
def testReplaceFileStringInCommand(self):
68+
"""Verify that the _replaceFileStringInCommand function works as expected."""
69+
self.assertFalse(_replaceFileStringInCommand(["cat"], ["test"]), None)
70+
self.assertFalse(_replaceFileStringInCommand(["cat", "-o"], ["test"]), None)
6971

7072
command = ["cat", "{file}"]
7173
expected = ["cat", "test"]
72-
self.assertTrue(replaceFileString(command, ["test"]))
74+
self.assertTrue(_replaceFileStringInCommand(command, ["test"]))
7375
self.assertEqual(command, expected)
7476

7577
command = ["cat", "-o", "{file}", "-a", "test2"]
7678
expected = ["cat", "-o", "test", "-a", "test2"]
77-
self.assertTrue(replaceFileString(command, ["test"]))
79+
self.assertTrue(_replaceFileStringInCommand(command, ["test"]))
7880
self.assertEqual(command, expected)
7981

8082
command = ["cat", "--a-long-option={file}", "-a", "test2"]
8183
expected = ["cat", "--a-long-option=test", "-a", "test2"]
82-
self.assertTrue(replaceFileString(command, ["test"]))
84+
self.assertTrue(_replaceFileStringInCommand(command, ["test"]))
8385
self.assertEqual(command, expected)
8486

8587
command = ["cat", "--input={file}", "-a", "test3"]
8688
expected = ["cat", "--input=test1", "--input=test2", "-a", "test3"]
87-
self.assertTrue(replaceFileString(command, ["test1", "test2"]))
89+
self.assertTrue(_replaceFileStringInCommand(command, ["test1", "test2"]))
8890
self.assertEqual(command, expected)
8991

9092

93+
def testReplaceFileStringInSpring(self):
94+
"""Verify that the _replaceFileStringInSpring function works as expected."""
95+
commands = [["cat", "test"]]
96+
self.assertFalse(_replaceFileStringInSpring(commands, ["test"]))
97+
98+
commands = [["cat", "-o", "test"]]
99+
self.assertFalse(_replaceFileStringInSpring(commands, ["test"]))
100+
101+
commands = [["cat", "{file}"]]
102+
expected = [["cat", "test"]]
103+
self.assertTrue(_replaceFileStringInSpring(commands, ["test"]))
104+
self.assertEqual(commands, expected)
105+
106+
commands = [["cat", "-o", "{file}", "-a", "test2"]]
107+
expected = [
108+
["cat", "-o", "test1", "-a", "test2"],
109+
["cat", "-o", "test2", "-a", "test2"],
110+
["cat", "-o", "test3", "-a", "test2"],
111+
]
112+
files = ["test1", "test2", "test3"]
113+
self.assertTrue(_replaceFileStringInSpring(commands, files))
114+
self.assertEqual(commands, expected)
115+
116+
commands = [["cat", "--a-long-option={file}"]]
117+
expected = [
118+
["cat", "--a-long-option=1"],
119+
["cat", "--a-long-option=2"],
120+
["cat", "--a-long-option=42"],
121+
]
122+
self.assertTrue(_replaceFileStringInSpring(commands, ["1", "2", "42"]))
123+
self.assertEqual(commands, expected)
124+
125+
# We also allow command other than the first to contain the {file}
126+
# string.
127+
commands = [["cat", "test"], ["cat", "--a-long-option={file}"]]
128+
expected = [
129+
["cat", "test"],
130+
["cat", "--a-long-option=1"],
131+
["cat", "--a-long-option=2"],
132+
["cat", "--a-long-option=42"],
133+
]
134+
self.assertTrue(_replaceFileStringInSpring(commands, ["1", "2", "42"]))
135+
self.assertEqual(commands, expected)
136+
137+
138+
def testReplaceFileStringSuccess(self):
139+
"""Verify that the replaceFileString function works as expected."""
140+
# Test the function with a "normal" command.
141+
command = ["cat", "{file}"]
142+
expected = ["cat", "test"]
143+
self.assertEqual(replaceFileString(command, ["test"]), expected)
144+
145+
# Test the function with a spring.
146+
command = [["cat", "{file}"], ["echo", "success"]]
147+
expected = [["cat", "test"], ["echo", "success"]]
148+
self.assertEqual(replaceFileString(command, ["test"]), expected)
149+
150+
151+
def testReplaceFileStringFailure(self):
152+
"""Verify that the replaceFileString function throws correct errors."""
153+
regex = r"Replacement string.*in: \"%s\"$"
154+
command = [
155+
["cat", "test1"],
156+
["cat", "test2"],
157+
]
158+
with self.assertRaisesRegex(NameError, regex % "cat test1 | cat test2"):
159+
replaceFileString(command, ["test"])
160+
161+
command = ["echo", "data"]
162+
with self.assertRaisesRegex(NameError, regex % "echo data"):
163+
replaceFileString(command, ["test"])
164+
165+
91166
if __name__ == "__main__":
92167
main()

0 commit comments

Comments
 (0)