dar-backup 0.8.0__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 CHANGED
@@ -1,6 +1,32 @@
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
+
22
+ ## v2-beta-0.8.1 - 2025-07-16
23
+
24
+ Github link: [v2-beta-0.8.1](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.1/v2)
25
+
26
+ ### Added
27
+
28
+ - FIX: runner now logs an error and fills more data into the returned CommandResult.
29
+
4
30
  ## v2-beta-0.8.0 - 2025-06-13
5
31
 
6
32
  Github link: [v2-beta-0.8.0](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.8.0/v2)
dar_backup/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![PyPI version](https://img.shields.io/pypi/v/dar-backup.svg)](https://pypi.org/project/dar-backup/)
10
10
  [![PyPI downloads](https://img.shields.io/badge/dynamic/json?color=blue&label=PyPI%20downloads&query=total&url=https%3A%2F%2Fraw.githubusercontent.com%2Fper2jensen%2Fdar-backup%2Fmain%2Fdownloads.json)](https://pypi.org/project/dar-backup/)
11
11
  [![# clones](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/badge_clones.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
12
- [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
12
+ [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png) <sub>🎯 Stats powered by [ClonePulse](https://github.com/per2jensen/clonepulse)</sub>
13
13
 
14
14
  The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
15
15
  the heavy lifting, together with the [parchive](https://github.com/Parchive/par2cmdline) suite in these scripts.
dar_backup/__about__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.8.0"
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.
@@ -2,23 +2,77 @@
2
2
 
3
3
  import subprocess
4
4
  import logging
5
+ import traceback
5
6
  import threading
6
7
  import os
8
+ import re
9
+ import shlex
7
10
  import sys
8
11
  import tempfile
9
12
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
10
- from typing import List, Optional
13
+ from typing import List, Optional, Union
11
14
  from dar_backup.util import get_logger
12
15
 
13
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
+
14
46
  class CommandResult:
15
- def __init__(self, returncode: int, stdout: str, stderr: str):
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
+ ):
16
55
  self.returncode = returncode
17
56
  self.stdout = stdout
18
57
  self.stderr = stderr
58
+ self.stack = stack
59
+ self.note = note
60
+
61
+
19
62
 
20
63
  def __repr__(self):
21
- return f"<CommandResult returncode={self.returncode}>"
64
+ return f"<CommandResult returncode={self.returncode}\nstdout={self.stdout}\nstderr={self.stderr}\nstack={self.stack}>"
65
+
66
+
67
+ def __str__(self):
68
+ return (
69
+ "CommandResult:\n"
70
+ f" Return code: {self.returncode}\n"
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>'}"
75
+ )
22
76
 
23
77
 
24
78
  class CommandRunner:
@@ -35,6 +89,7 @@ class CommandRunner:
35
89
  if not self.logger or not self.command_logger:
36
90
  self.logger_fallback()
37
91
 
92
+
38
93
  def logger_fallback(self):
39
94
  """
40
95
  Setup temporary log files
@@ -72,24 +127,53 @@ class CommandRunner:
72
127
  capture_output: bool = True,
73
128
  text: bool = True
74
129
  ) -> CommandResult:
130
+ self._text_mode = text
75
131
  timeout = timeout or self.default_timeout
76
132
 
77
- #log the command to be executed
78
- command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
79
- self.command_logger.info(command) # log to command logger
80
- self.logger.debug(command) # log to main logger if "--log-level debug"
81
-
82
- process = subprocess.Popen(
83
- cmd,
84
- stdout=subprocess.PIPE if capture_output else None,
85
- stderr=subprocess.PIPE if capture_output else None,
86
- text=False,
87
- bufsize=-1
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
+
155
+ self.command_logger.info(command)
156
+ self.logger.debug(command)
89
157
 
90
158
  stdout_lines = []
91
159
  stderr_lines = []
92
160
 
161
+ try:
162
+ process = subprocess.Popen(
163
+ cmd,
164
+ stdout=subprocess.PIPE if capture_output else None,
165
+ stderr=subprocess.PIPE if capture_output else None,
166
+ text=False,
167
+ bufsize=-1
168
+ )
169
+ except Exception as e:
170
+ stack = traceback.format_exc()
171
+ return CommandResult(
172
+ returncode=-1,
173
+ stdout='',
174
+ stderr=str(e),
175
+ stack=stack
176
+ )
93
177
 
94
178
  def stream_output(stream, lines, level):
95
179
  try:
@@ -97,9 +181,13 @@ class CommandRunner:
97
181
  chunk = stream.read(1024)
98
182
  if not chunk:
99
183
  break
100
- decoded = chunk.decode('utf-8', errors='replace')
101
- lines.append(decoded)
102
- self.command_logger.log(level, decoded.strip())
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
103
191
  except Exception as e:
104
192
  self.logger.warning(f"stream_output decode error: {e}")
105
193
  finally:
@@ -123,11 +211,36 @@ class CommandRunner:
123
211
  process.kill()
124
212
  self.logger.error(f"Command timed out: {' '.join(cmd)}")
125
213
  return CommandResult(-1, ''.join(stdout_lines), ''.join(stderr_lines))
214
+ except Exception as e:
215
+ stack = traceback.format_exc()
216
+ self.logger.error(f"Command execution failed: {' '.join(cmd)} with error: {e}")
217
+ return CommandResult(-1, ''.join(stdout_lines), ''.join(stderr_lines), stack)
126
218
 
127
219
  for t in threads:
128
220
  t.join()
129
221
 
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
+
130
232
  if check and process.returncode != 0:
131
233
  self.logger.error(f"Command failed with exit code {process.returncode}")
234
+ return CommandResult(
235
+ process.returncode,
236
+ stdout_combined,
237
+ stderr_combined,
238
+ stack=traceback.format_stack()
239
+ )
240
+
241
+ return CommandResult(
242
+ process.returncode,
243
+ stdout_combined,
244
+ stderr_combined
245
+ )
132
246
 
133
- return CommandResult(process.returncode, ''.join(stdout_lines), ''.join(stderr_lines))
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.0
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
@@ -727,7 +727,7 @@ Description-Content-Type: text/markdown
727
727
  [![PyPI version](https://img.shields.io/pypi/v/dar-backup.svg)](https://pypi.org/project/dar-backup/)
728
728
  [![PyPI downloads](https://img.shields.io/badge/dynamic/json?color=blue&label=PyPI%20downloads&query=total&url=https%3A%2F%2Fraw.githubusercontent.com%2Fper2jensen%2Fdar-backup%2Fmain%2Fdownloads.json)](https://pypi.org/project/dar-backup/)
729
729
  [![# clones](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/badge_clones.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
730
- [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png)
730
+ [![Milestone](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/per2jensen/dar-backup/main/v2/doc/badges/milestone_badge.json)](https://github.com/per2jensen/dar-backup/blob/main/v2/doc/weekly_clones.png) <sub>🎯 Stats powered by [ClonePulse](https://github.com/per2jensen/clonepulse)</sub>
731
731
 
732
732
  The wonderful 'dar' [Disk Archiver](https://github.com/Edrusb/DAR) is used for
733
733
  the heavy lifting, together with the [parchive](https://github.com/Parchive/par2cmdline) suite in these scripts.
@@ -1,15 +1,15 @@
1
1
  dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
2
- dar_backup/Changelog.md,sha256=CpUyWEnzVvn2otCGTp-N4R_-0zlr1kFe_Y0yQ-xQEZg,11872
3
- dar_backup/README.md,sha256=HWrcpd_nhzhL2quLlxp7Xd0i05GxZvJMu5nSw8lJKLk,59901
4
- dar_backup/__about__.py,sha256=4n6dyFqIrCJMAEZWvi2IbAzYCvkz4sxqN2wbVMbVWNk,344
2
+ dar_backup/Changelog.md,sha256=kJHH12ETI46nzZURvhKZvZR4RndnY8KP22ORfN5u3iA,12988
3
+ dar_backup/README.md,sha256=S8wpgaqa2LzXXZiQEzoOV1qjrpf9m1baYmvzGYtgEcE,59990
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=IUPYYBsaDFBp0q81Rt2xB9ucuK9eu5bXziRKXhI5YZM,4500
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=pgUQ1wZ5_yocRJJDRa4AkVyI7I8Z5WBY5vD5a4ASkmA,43001
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.0.dist-info/METADATA,sha256=_CBNbG6I0C8C18V3ZfZPGWuMSnVD0UrXlswQCK-OnCY,102592
22
- dar_backup-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- dar_backup-0.8.0.dist-info/entry_points.txt,sha256=pOK9M8cHeAcGIatrYzkm_1O89kPk0enyYONALYjFBx4,286
24
- dar_backup-0.8.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
- dar_backup-0.8.0.dist-info/RECORD,,
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,,