dar-backup 0.8.1__py3-none-any.whl → 0.8.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/Changelog.md +18 -0
- dar_backup/__about__.py +1 -1
- dar_backup/command_runner.py +94 -15
- dar_backup/dar_backup.py +5 -5
- {dar_backup-0.8.1.dist-info → dar_backup-0.8.2.dist-info}/METADATA +1 -1
- {dar_backup-0.8.1.dist-info → dar_backup-0.8.2.dist-info}/RECORD +9 -9
- {dar_backup-0.8.1.dist-info → dar_backup-0.8.2.dist-info}/WHEEL +0 -0
- {dar_backup-0.8.1.dist-info → dar_backup-0.8.2.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.8.1.dist-info → dar_backup-0.8.2.dist-info}/licenses/LICENSE +0 -0
dar_backup/Changelog.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
<!-- markdownlint-disable MD024 -->
|
|
2
2
|
# dar-backup Changelog
|
|
3
3
|
|
|
4
|
+
## v2-beta-0.8.2 - 2025-07-17
|
|
5
|
+
|
|
6
|
+
Github link: [v2-beta-0.8.2](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.2/v2)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Security hardening: CommandRunner now performs strict command-line sanitization
|
|
11
|
+
- Disallows potentially dangerous characters (e.g. ;, &, |) in command arguments
|
|
12
|
+
- Prevents injection-style misuse when restoring specific files or invoking custom commands
|
|
13
|
+
|
|
14
|
+
- Documentation:
|
|
15
|
+
- New [README section](https://github.com/per2jensen/dar-backup?tab=readme-ov-file#limitations-on-file-names-with-special-characters) explains filename restrictions and safe workarounds (e.g. restoring directly with dar, if needed)
|
|
16
|
+
- Includes a Markdown table listing all disallowed characters
|
|
17
|
+
|
|
18
|
+
- Test suite:
|
|
19
|
+
- Existing test cases updated to comply with the new sanitization rules
|
|
20
|
+
- Additional tests ensure CommandRunner handles large binary output and edge cases cleanly
|
|
21
|
+
|
|
4
22
|
## v2-beta-0.8.1 - 2025-07-16
|
|
5
23
|
|
|
6
24
|
Github link: [v2-beta-0.8.1](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.1/v2)
|
dar_backup/__about__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.8.
|
|
1
|
+
__version__ = "0.8.2"
|
|
2
2
|
|
|
3
3
|
__license__ = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
4
4
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
dar_backup/command_runner.py
CHANGED
|
@@ -5,33 +5,76 @@ import logging
|
|
|
5
5
|
import traceback
|
|
6
6
|
import threading
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
8
10
|
import sys
|
|
9
11
|
import tempfile
|
|
10
12
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
|
|
11
|
-
from typing import List, Optional
|
|
13
|
+
from typing import List, Optional, Union
|
|
12
14
|
from dar_backup.util import get_logger
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def is_safe_arg(arg: str) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Check if the argument is safe by rejecting dangerous shell characters.
|
|
20
|
+
"""
|
|
21
|
+
return not re.search(r'[;&|><`$\n]', arg)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sanitize_cmd(cmd: List[str]) -> List[str]:
|
|
25
|
+
"""
|
|
26
|
+
Validate and sanitize a list of command-line arguments.
|
|
27
|
+
Ensures all elements are strings and do not contain dangerous shell characters.
|
|
28
|
+
Raises ValueError if any argument is unsafe.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
if not isinstance(cmd, list):
|
|
32
|
+
raise ValueError("Command must be a list of strings")
|
|
33
|
+
for arg in cmd:
|
|
34
|
+
if not isinstance(arg, str):
|
|
35
|
+
raise ValueError(f"Invalid argument type: {arg} (must be string)")
|
|
36
|
+
if not is_safe_arg(arg):
|
|
37
|
+
raise ValueError(f"Unsafe argument detected: {arg}")
|
|
38
|
+
return cmd
|
|
39
|
+
|
|
40
|
+
def _safe_str(s):
|
|
41
|
+
if isinstance(s, bytes):
|
|
42
|
+
return f"<{len(s)} bytes of binary data>"
|
|
43
|
+
return s
|
|
44
|
+
|
|
45
|
+
|
|
15
46
|
class CommandResult:
|
|
16
|
-
def __init__(
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
returncode: int,
|
|
50
|
+
stdout: Union[str, bytes],
|
|
51
|
+
stderr: Union[str, bytes],
|
|
52
|
+
stack: Optional[str] = None,
|
|
53
|
+
note: Optional[str] = None
|
|
54
|
+
):
|
|
17
55
|
self.returncode = returncode
|
|
18
56
|
self.stdout = stdout
|
|
19
57
|
self.stderr = stderr
|
|
20
58
|
self.stack = stack
|
|
59
|
+
self.note = note
|
|
60
|
+
|
|
61
|
+
|
|
21
62
|
|
|
22
63
|
def __repr__(self):
|
|
23
64
|
return f"<CommandResult returncode={self.returncode}\nstdout={self.stdout}\nstderr={self.stderr}\nstack={self.stack}>"
|
|
24
|
-
|
|
65
|
+
|
|
25
66
|
|
|
26
67
|
def __str__(self):
|
|
27
68
|
return (
|
|
28
|
-
|
|
69
|
+
"CommandResult:\n"
|
|
29
70
|
f" Return code: {self.returncode}\n"
|
|
30
|
-
f"
|
|
31
|
-
f"
|
|
32
|
-
f"
|
|
71
|
+
f" Note: {self.note if self.note else '<none>'}\n"
|
|
72
|
+
f" STDOUT: {_safe_str(self.stdout)}\n"
|
|
73
|
+
f" STDERR: {_safe_str(self.stderr)}\n"
|
|
74
|
+
f" Stacktrace: {self.stack if self.stack else '<none>'}"
|
|
33
75
|
)
|
|
34
76
|
|
|
77
|
+
|
|
35
78
|
class CommandRunner:
|
|
36
79
|
def __init__(
|
|
37
80
|
self,
|
|
@@ -46,6 +89,7 @@ class CommandRunner:
|
|
|
46
89
|
if not self.logger or not self.command_logger:
|
|
47
90
|
self.logger_fallback()
|
|
48
91
|
|
|
92
|
+
|
|
49
93
|
def logger_fallback(self):
|
|
50
94
|
"""
|
|
51
95
|
Setup temporary log files
|
|
@@ -83,9 +127,31 @@ class CommandRunner:
|
|
|
83
127
|
capture_output: bool = True,
|
|
84
128
|
text: bool = True
|
|
85
129
|
) -> CommandResult:
|
|
130
|
+
self._text_mode = text
|
|
86
131
|
timeout = timeout or self.default_timeout
|
|
87
132
|
|
|
88
|
-
|
|
133
|
+
cmd_sanitized = None
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
cmd_sanitized = sanitize_cmd(cmd)
|
|
137
|
+
except ValueError as e:
|
|
138
|
+
stack = traceback.format_exc()
|
|
139
|
+
self.logger.error(f"Command sanitation failed: {e}")
|
|
140
|
+
return CommandResult(
|
|
141
|
+
returncode=-1,
|
|
142
|
+
note=f"Sanitizing failed: command: {' '.join(cmd)}",
|
|
143
|
+
stdout='',
|
|
144
|
+
stderr=str(e),
|
|
145
|
+
stack=stack,
|
|
146
|
+
|
|
147
|
+
)
|
|
148
|
+
finally:
|
|
149
|
+
cmd = cmd_sanitized
|
|
150
|
+
|
|
151
|
+
#command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
|
|
152
|
+
command = f"Executing command: {' '.join(shlex.quote(arg) for arg in cmd)} (timeout={timeout}s)"
|
|
153
|
+
|
|
154
|
+
|
|
89
155
|
self.command_logger.info(command)
|
|
90
156
|
self.logger.debug(command)
|
|
91
157
|
|
|
@@ -115,9 +181,13 @@ class CommandRunner:
|
|
|
115
181
|
chunk = stream.read(1024)
|
|
116
182
|
if not chunk:
|
|
117
183
|
break
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
184
|
+
if self._text_mode:
|
|
185
|
+
decoded = chunk.decode('utf-8', errors='replace')
|
|
186
|
+
lines.append(decoded)
|
|
187
|
+
self.command_logger.log(level, decoded.strip())
|
|
188
|
+
else:
|
|
189
|
+
lines.append(chunk)
|
|
190
|
+
# Avoid logging raw binary data to prevent garbled logs
|
|
121
191
|
except Exception as e:
|
|
122
192
|
self.logger.warning(f"stream_output decode error: {e}")
|
|
123
193
|
finally:
|
|
@@ -150,18 +220,27 @@ class CommandRunner:
|
|
|
150
220
|
t.join()
|
|
151
221
|
|
|
152
222
|
|
|
223
|
+
|
|
224
|
+
if self._text_mode:
|
|
225
|
+
stdout_combined = ''.join(stdout_lines)
|
|
226
|
+
stderr_combined = ''.join(stderr_lines)
|
|
227
|
+
else:
|
|
228
|
+
stdout_combined = b''.join(stdout_lines)
|
|
229
|
+
stderr_combined = b''.join(stderr_lines)
|
|
230
|
+
|
|
231
|
+
|
|
153
232
|
if check and process.returncode != 0:
|
|
154
233
|
self.logger.error(f"Command failed with exit code {process.returncode}")
|
|
155
234
|
return CommandResult(
|
|
156
235
|
process.returncode,
|
|
157
|
-
|
|
158
|
-
|
|
236
|
+
stdout_combined,
|
|
237
|
+
stderr_combined,
|
|
159
238
|
stack=traceback.format_stack()
|
|
160
239
|
)
|
|
161
240
|
|
|
162
241
|
return CommandResult(
|
|
163
242
|
process.returncode,
|
|
164
|
-
|
|
165
|
-
|
|
243
|
+
stdout_combined,
|
|
244
|
+
stderr_combined
|
|
166
245
|
)
|
|
167
246
|
|
dar_backup/dar_backup.py
CHANGED
|
@@ -248,7 +248,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
248
248
|
PermissionError: If a permission error occurs while comparing files.
|
|
249
249
|
"""
|
|
250
250
|
result = True
|
|
251
|
-
command = ['dar', '-t', backup_file, '-Q']
|
|
251
|
+
command = ['dar', '-t', backup_file, '-N', '-Q']
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
log_basename = os.path. dirname(config_settings.logfile_location)
|
|
@@ -315,7 +315,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
315
315
|
for restored_file_path in random_files:
|
|
316
316
|
try:
|
|
317
317
|
args.verbose and logger.info(f"Restoring file: '{restored_file_path}' from backup to: '{config_settings.test_restore_dir}' for file comparing")
|
|
318
|
-
command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-Q', '-B', args.darrc, 'restore-options']
|
|
318
|
+
command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '--noconf', '-Q', '-B', args.darrc, 'restore-options']
|
|
319
319
|
args.verbose and logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
320
320
|
process = runner.run(command, timeout = config_settings.command_timeout_secs)
|
|
321
321
|
if process.returncode != 0:
|
|
@@ -347,7 +347,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
|
|
|
347
347
|
results: List[tuple] = []
|
|
348
348
|
try:
|
|
349
349
|
backup_file = os.path.join(config_settings.backup_dir, backup_name)
|
|
350
|
-
command = ['dar', '-x', backup_file, '-Q', '-D']
|
|
350
|
+
command = ['dar', '-x', backup_file, '--noconf', '-Q', '-D']
|
|
351
351
|
if restore_dir:
|
|
352
352
|
if not os.path.exists(restore_dir):
|
|
353
353
|
os.makedirs(restore_dir)
|
|
@@ -390,7 +390,7 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
|
|
|
390
390
|
logger.debug(f"Getting backed up files in xml from DAR archive: '{backup_name}'")
|
|
391
391
|
backup_path = os.path.join(backup_dir, backup_name)
|
|
392
392
|
try:
|
|
393
|
-
command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
|
|
393
|
+
command = ['dar', '-l', backup_path, '--noconf', '-am', '-as', "-Txml" , '-Q']
|
|
394
394
|
logger.debug(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
395
395
|
command_result = runner.run(command)
|
|
396
396
|
# Parse the XML data
|
|
@@ -418,7 +418,7 @@ def list_contents(backup_name, backup_dir, selection=None):
|
|
|
418
418
|
backup_path = os.path.join(backup_dir, backup_name)
|
|
419
419
|
|
|
420
420
|
try:
|
|
421
|
-
command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
|
|
421
|
+
command = ['dar', '-l', backup_path, '--noconf', '-am', '-as', '-Q']
|
|
422
422
|
if selection:
|
|
423
423
|
selection_criteria = shlex.split(selection)
|
|
424
424
|
command.extend(selection_criteria)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dar-backup
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: A script to do full, differential and incremental backups using dar. Some files are restored from the backups during verification, after which par2 redundancy files are created. The script also has a cleanup feature to remove old backups and par2 files.
|
|
5
5
|
Project-URL: GPG Public Key, https://keys.openpgp.org/search?q=dar-backup@pm.me
|
|
6
6
|
Project-URL: Homepage, https://github.com/per2jensen/dar-backup/tree/main/v2
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
-
dar_backup/Changelog.md,sha256=
|
|
2
|
+
dar_backup/Changelog.md,sha256=kJHH12ETI46nzZURvhKZvZR4RndnY8KP22ORfN5u3iA,12988
|
|
3
3
|
dar_backup/README.md,sha256=S8wpgaqa2LzXXZiQEzoOV1qjrpf9m1baYmvzGYtgEcE,59990
|
|
4
|
-
dar_backup/__about__.py,sha256=
|
|
4
|
+
dar_backup/__about__.py,sha256=HtwR6RuPdVHxYDraJvSQ0J9gRO3qkMMealFqBS1CfGc,344
|
|
5
5
|
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
dar_backup/clean_log.py,sha256=pmmyPmLWbm3_3sHwJt9V_xBwUF8v015iS17ypJAGAZ4,6023
|
|
7
7
|
dar_backup/cleanup.py,sha256=_ggDcpMCB1MhXStvYussp_PdGfhIFtEutT5BNrNkMSY,13297
|
|
8
|
-
dar_backup/command_runner.py,sha256=
|
|
8
|
+
dar_backup/command_runner.py,sha256=cwthuNU4N1vzAjri0uh2x32vmCLu1B2S3OxGExpCGRE,7840
|
|
9
9
|
dar_backup/config_settings.py,sha256=2UAHvatrVO4ark6lCn2Q7qBvZN6DUMK2ftlNrKpzlWc,5792
|
|
10
10
|
dar_backup/dar-backup.conf,sha256=46V2zdvjj_aThFY11wWlffjmoiChYmatdf5DXCsmmao,1208
|
|
11
11
|
dar_backup/dar-backup.conf.j2,sha256=z3epGo6nB_Jh3liTOp93wJO_HKUsf7qghe9cdtFH7cY,2021
|
|
12
|
-
dar_backup/dar_backup.py,sha256=
|
|
12
|
+
dar_backup/dar_backup.py,sha256=JW1k0LuQhf_y2f1K1pIu1-PmKw4bGSrCB_MBoPvFWuM,43057
|
|
13
13
|
dar_backup/dar_backup_systemd.py,sha256=PwAc2H2J3hQLWpnC6Ib95NZYtB2G2NDgkSblfLj1n10,3875
|
|
14
14
|
dar_backup/demo.py,sha256=bxEq_nJwHuQydERPppkvhotg1fdwBX_CE33m5fX_kxw,7945
|
|
15
15
|
dar_backup/demo_backup_def.j2,sha256=hQW2Glp0QGV3Kt8cwjS0mpOCdyzjVlpgbgL6LpXTKJA,1793
|
|
@@ -18,8 +18,8 @@ dar_backup/installer.py,sha256=xSXh77qquIZbUTSY3AbhERQbS7bnrPE__M_yqpszdhM,6883
|
|
|
18
18
|
dar_backup/manager.py,sha256=d1zliFpSuWc8JhjKqLMC-xOhp5kSIcfeGkMZOVuZcM0,27143
|
|
19
19
|
dar_backup/rich_progress.py,sha256=SfwFxebBl6jnDQMUQr4McknkW1yQWaJVo1Ju1OD3okA,3221
|
|
20
20
|
dar_backup/util.py,sha256=iTOGsZyIdkvh9tIu7hD_IXi-9HO6GhVgqact5GGInEY,26063
|
|
21
|
-
dar_backup-0.8.
|
|
22
|
-
dar_backup-0.8.
|
|
23
|
-
dar_backup-0.8.
|
|
24
|
-
dar_backup-0.8.
|
|
25
|
-
dar_backup-0.8.
|
|
21
|
+
dar_backup-0.8.2.dist-info/METADATA,sha256=YgHdGcT3ZcaCUYxtRwaArlHj4xelOH0DVumw2bgGwTk,102681
|
|
22
|
+
dar_backup-0.8.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
dar_backup-0.8.2.dist-info/entry_points.txt,sha256=pOK9M8cHeAcGIatrYzkm_1O89kPk0enyYONALYjFBx4,286
|
|
24
|
+
dar_backup-0.8.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
25
|
+
dar_backup-0.8.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|