dar-backup 0.6.17__py3-none-any.whl → 0.6.19__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/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.6.17"
1
+ __version__ = "0.6.19"
dar_backup/clean_log.py CHANGED
@@ -11,6 +11,8 @@ See section 15 and section 16 in the supplied "LICENSE" file
11
11
 
12
12
  This script can be used to remove (much of) the logged output from `dar`.
13
13
  When `dar` verbose options are enabled, quite a lot of information is emitted.
14
+
15
+ If a rerex is matched, the entire line is removed (change in v2-beta-0.6.19).
14
16
  """
15
17
 
16
18
 
@@ -19,7 +21,7 @@ import re
19
21
  import os
20
22
  import sys
21
23
 
22
- from . import __about__ as about
24
+ from dar_backup import __about__ as about
23
25
  from dar_backup.config_settings import ConfigSettings
24
26
 
25
27
  LICENSE = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
@@ -32,7 +34,7 @@ def clean_log_file(log_file_path, dry_run=False):
32
34
 
33
35
  if not os.path.isfile(log_file_path):
34
36
  print(f"File '{log_file_path}' not found!")
35
- sys.exit(1)
37
+ sys.exit(127)
36
38
 
37
39
  if not os.access(log_file_path, os.R_OK):
38
40
  print(f"No read permission for '{log_file_path}'")
@@ -49,13 +51,18 @@ def clean_log_file(log_file_path, dry_run=False):
49
51
  temp_file_path = log_file_path + ".tmp"
50
52
 
51
53
  patterns = [
54
+ r"INFO\s*-\s*Inspecting\s*directory",
55
+ r"INFO\s*-\s*Finished\s*Inspecting",
52
56
  r"INFO\s*-\s*<File",
57
+ r"INFO\s*-\s*</File",
53
58
  r"INFO\s*-\s*<Attributes",
59
+ r"INFO\s*-\s*</Attributes",
54
60
  r"INFO\s*-\s*</Directory",
55
61
  r"INFO\s*-\s*<Directory",
56
- r"INFO\s*-\s*</File",
57
- r"INFO\s*-\s*Inspecting\s*directory",
58
- r"INFO\s*-\s*Finished\s*Inspecting"
62
+ r"INFO\s*-\s*<Catalog",
63
+ r"INFO\s*-\s*</Catalog",
64
+ r"INFO\s*-\s*<Symlink",
65
+ r"INFO\s*-\s*</Symlink",
59
66
  ]
60
67
 
61
68
  try:
@@ -70,9 +77,9 @@ def clean_log_file(log_file_path, dry_run=False):
70
77
  if dry_run:
71
78
  print(f"Would remove: {original_line.strip()}") # Print full line for dry-run
72
79
  matched = True # Mark that a pattern matched
73
- line = re.sub(pattern, "", line).strip() # Remove only matched part
80
+ break # No need to check other patterns if one matches
74
81
 
75
- if not dry_run and line: # In normal mode, only write non-empty lines
82
+ if not dry_run and not matched: # In normal mode, only write non-empty lines
76
83
  outfile.write(line.rstrip() + "\n")
77
84
 
78
85
  if dry_run and matched:
dar_backup/cleanup.py CHANGED
@@ -13,6 +13,7 @@ This script removes old DIFF and INCR archives + accompanying .par2 files accord
13
13
  [AGE] settings in the configuration file.
14
14
  """
15
15
 
16
+ import argcomplete
16
17
  import argparse
17
18
  import logging
18
19
  import os
@@ -27,10 +28,11 @@ from typing import Dict, List, NamedTuple
27
28
 
28
29
  from . import __about__ as about
29
30
  from dar_backup.config_settings import ConfigSettings
30
- from dar_backup.util import extract_error_lines
31
31
  from dar_backup.util import list_backups
32
32
  from dar_backup.util import setup_logging
33
33
  from dar_backup.util import get_logger
34
+ from dar_backup.util import requirements
35
+ from dar_backup.util import backup_definition_completer, list_archive_completer
34
36
 
35
37
  from dar_backup.command_runner import CommandRunner
36
38
  from dar_backup.command_runner import CommandResult
@@ -158,20 +160,47 @@ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW,
158
160
  See section 15 and section 16 in the supplied "LICENSE" file.''')
159
161
 
160
162
 
163
+ def confirm_full_archive_deletion(archive_name: str, test_mode=False) -> bool:
164
+ try:
165
+ if test_mode:
166
+ confirmation = os.environ.get("CLEANUP_TEST_DELETE_FULL")
167
+ if confirmation is None:
168
+ raise RuntimeError("envvar 'CLEANUP_TEST_DELETE_FULL' not set")
169
+ print(f"Simulated confirmation for FULL archive: {confirmation}")
170
+ else:
171
+ confirmation = inputimeout(
172
+ prompt=f"Are you sure you want to delete the FULL archive '{archive_name}'? (yes/no): ",
173
+ timeout=30)
174
+ if confirmation is None:
175
+ logger.info(f"No confirmation received for FULL archive: {archive_name}. Skipping deletion.")
176
+ return False
177
+ return confirmation.strip().lower() == "yes"
178
+ except TimeoutOccurred:
179
+ logger.info(f"Timeout waiting for confirmation for FULL archive: {archive_name}. Skipping deletion.")
180
+ return False
181
+ except KeyboardInterrupt:
182
+ logger.info(f"User interrupted confirmation for FULL archive: {archive_name}. Skipping deletion.")
183
+ return False
184
+
185
+
186
+
161
187
  def main():
162
188
  global logger, runner
163
189
 
164
190
  parser = argparse.ArgumentParser(description="Cleanup old archives according to AGE configuration.")
165
- parser.add_argument('-d', '--backup-definition', help="Specific backup definition to cleanup.")
191
+ parser.add_argument('-d', '--backup-definition', help="Specific backup definition to cleanup.").completer = backup_definition_completer
166
192
  parser.add_argument('-c', '--config-file', '-c', type=str, help="Path to 'dar-backup.conf'", default='~/.config/dar-backup/dar-backup.conf')
167
193
  parser.add_argument('-v', '--version', action='store_true', help="Show version information.")
168
194
  parser.add_argument('--alternate-archive-dir', type=str, help="Cleanup in this directory instead of the default one.")
169
- parser.add_argument('--cleanup-specific-archives', type=str, help="Comma separated list of archives to cleanup")
195
+ parser.add_argument('--cleanup-specific-archives', type=str, help="Comma separated list of archives to cleanup").completer = list_archive_completer
170
196
  parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
171
197
  parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
172
198
  parser.add_argument('--log-level', type=str, help="`debug` or `trace`, default is `info`", default="info")
173
199
  parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
174
200
  parser.add_argument('--test-mode', action='store_true', help='Read envvars in order to run some pytest cases')
201
+
202
+ argcomplete.autocomplete(parser)
203
+
175
204
  args = parser.parse_args()
176
205
 
177
206
  args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
@@ -206,21 +235,9 @@ def main():
206
235
  args.verbose and (print(f"--alternate-archive-dir: {args.alternate_archive_dir}"))
207
236
  args.verbose and (print(f"--cleanup-specific-archives:{args.cleanup_specific_archives}"))
208
237
 
209
- # run PREREQ scripts
210
- if 'PREREQ' in config_settings.config:
211
- for key in sorted(config_settings.config['PREREQ'].keys()):
212
- script = config_settings.config['PREREQ'][key]
213
- try:
214
- result = subprocess.run(script, shell=True, check=True)
215
- logger.info(f"PREREQ {key}: '{script}' run, return code: {result.returncode}")
216
- logger.info(f"PREREQ stdout:\n{result.stdout}")
217
- except subprocess.CalledProcessError as e:
218
- logger.error(f"Error executing {key}: '{script}': {e}")
219
- if result:
220
- logger.error(f"PREREQ stderr:\n{result.stderr}")
221
- print(f"Error executing {script}: {e}")
222
- sys.exit(1)
223
238
 
239
+ # run PREREQ scripts
240
+ requirements('PREREQ', config_settings)
224
241
 
225
242
  if args.alternate_archive_dir:
226
243
  if not os.path.exists(args.alternate_archive_dir):
@@ -231,37 +248,15 @@ def main():
231
248
  sys.exit(1)
232
249
  config_settings.backup_dir = args.alternate_archive_dir
233
250
 
251
+ if args.cleanup_specific_archives is None and args.test_mode:
252
+ logger.info("No --cleanup-specific-archives provided; skipping specific archive deletion in test mode.")
234
253
 
235
254
  if args.cleanup_specific_archives:
236
255
  logger.info(f"Cleaning up specific archives: {args.cleanup_specific_archives}")
237
256
  archive_names = args.cleanup_specific_archives.split(',')
238
257
  for archive_name in archive_names:
239
258
  if "_FULL_" in archive_name:
240
- try:
241
- try:
242
- # used for pytest cases
243
- if args.test_mode:
244
- confirmation = os.environ.get("CLEANUP_TEST_DELETE_FULL")
245
- if confirmation == None:
246
- raise RuntimeError("envvar 'CLEANUP_TEST_DELETE_FULL' not set")
247
-
248
- else:
249
- confirmation = inputimeout(
250
- prompt=f"Are you sure you want to delete the FULL archive '{archive_name}'? (yes/no): ",
251
- timeout=30)
252
- if confirmation == None:
253
- continue
254
- else:
255
- confirmation = confirmation.strip().lower()
256
- except TimeoutOccurred:
257
- logger.info(f"Timeout waiting for confirmation for FULL archive: {archive_name}. Skipping deletion.")
258
- continue
259
- except KeyboardInterrupt:
260
- logger.info(f"User interrupted confirmation for FULL archive: {archive_name}. Skipping deletion.")
261
- continue
262
-
263
- if confirmation != 'yes':
264
- logger.info(f"User did not answer 'yes' to confirm deletion of FULL archive: {archive_name}. Skipping deletion.")
259
+ if not confirm_full_archive_deletion(archive_name, args.test_mode):
265
260
  continue
266
261
  logger.info(f"Deleting archive: {archive_name}")
267
262
  delete_archive(config_settings.backup_dir, archive_name.strip(), args)
@@ -280,10 +275,13 @@ def main():
280
275
  delete_old_backups(config_settings.backup_dir, config_settings.diff_age, 'DIFF', args, definition)
281
276
  delete_old_backups(config_settings.backup_dir, config_settings.incr_age, 'INCR', args, definition)
282
277
 
278
+ # run POST scripts
279
+ requirements('POSTREQ', config_settings)
280
+
283
281
 
284
282
  end_time=int(time())
285
283
  logger.info(f"END TIME: {end_time}")
286
-
284
+ sys.exit(0)
287
285
 
288
286
  if __name__ == "__main__":
289
287
  main()
@@ -3,8 +3,11 @@ import logging
3
3
  import threading
4
4
  import os
5
5
  import sys
6
+ import tempfile
6
7
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
7
8
  from typing import List, Optional
9
+ from dar_backup.util import get_logger
10
+
8
11
 
9
12
  class CommandResult:
10
13
  def __init__(self, returncode: int, stdout: str, stderr: str):
@@ -15,6 +18,7 @@ class CommandResult:
15
18
  def __repr__(self):
16
19
  return f"<CommandResult returncode={self.returncode}>"
17
20
 
21
+
18
22
  class CommandRunner:
19
23
  def __init__(
20
24
  self,
@@ -22,10 +26,41 @@ class CommandRunner:
22
26
  command_logger: Optional[logging.Logger] = None,
23
27
  default_timeout: int = 30
24
28
  ):
25
- self.logger = logger or logging.getLogger(__name__)
26
- self.command_logger = command_logger or self.logger
29
+ self.logger = logger or get_logger()
30
+ self.command_logger = command_logger or get_logger(command_output_logger=True)
27
31
  self.default_timeout = default_timeout
28
32
 
33
+ if not self.logger or not self.command_logger:
34
+ self.logger_fallback()
35
+
36
+ def logger_fallback(self):
37
+ """
38
+ Setup temporary log files
39
+ """
40
+ main_log = tempfile.NamedTemporaryFile(delete=False)
41
+ command_log = tempfile.NamedTemporaryFile(delete=False)
42
+
43
+ logger = logging.getLogger("command_runner_fallback_main_logger")
44
+ command_logger = logging.getLogger("command_runner_fallback_command_logger")
45
+ logger.setLevel(logging.DEBUG)
46
+ command_logger.setLevel(logging.DEBUG)
47
+
48
+ main_handler = logging.FileHandler(main_log.name)
49
+ command_handler = logging.FileHandler(command_log.name)
50
+
51
+ logger.addHandler(main_handler)
52
+ command_logger.addHandler(command_handler)
53
+
54
+ self.logger = logger
55
+ self.command_logger = command_logger
56
+ self.default_timeout = 30
57
+ self.logger.info("CommandRunner initialized with fallback loggers")
58
+ self.command_logger.info("CommandRunner initialized with fallback loggers")
59
+
60
+ print(f"[WARN] Using fallback loggers:\n Main log: {main_log.name}\n Command log: {command_log.name}", file=sys.stderr)
61
+
62
+
63
+
29
64
  def run(
30
65
  self,
31
66
  cmd: List[str],
@@ -36,24 +71,39 @@ class CommandRunner:
36
71
  text: bool = True
37
72
  ) -> CommandResult:
38
73
  timeout = timeout or self.default_timeout
39
- self.logger.debug(f"Executing command: {' '.join(cmd)} (timeout={timeout}s)")
74
+
75
+ #log the command to be executed
76
+ command = f"Executing command: {' '.join(cmd)} (timeout={timeout}s)"
77
+ self.command_logger.info(command) # log to command logger
78
+ self.logger.debug(command) # log to main logger if "--log-level debug"
40
79
 
41
80
  process = subprocess.Popen(
42
81
  cmd,
43
82
  stdout=subprocess.PIPE if capture_output else None,
44
83
  stderr=subprocess.PIPE if capture_output else None,
45
- text=text,
46
- bufsize=1
84
+ text=False,
85
+ bufsize=-1
47
86
  )
48
87
 
49
88
  stdout_lines = []
50
89
  stderr_lines = []
51
90
 
91
+
52
92
  def stream_output(stream, lines, level):
53
- for line in iter(stream.readline, ''):
54
- lines.append(line)
55
- self.command_logger.log(level, line.strip())
56
- stream.close()
93
+ try:
94
+ while True:
95
+ chunk = stream.read(1024)
96
+ if not chunk:
97
+ break
98
+ decoded = chunk.decode('utf-8', errors='replace')
99
+ lines.append(decoded)
100
+ self.command_logger.log(level, decoded.strip())
101
+ except Exception as e:
102
+ self.logger.warning(f"stream_output decode error: {e}")
103
+ finally:
104
+ stream.close()
105
+
106
+
57
107
 
58
108
  threads = []
59
109
  if capture_output and process.stdout:
dar_backup/dar_backup.py CHANGED
@@ -1,5 +1,21 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ """
4
+ installer.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/installer.py
5
+ This script is part of dar-backup, a backup solution for Linux using dar and systemd.
6
+
7
+ Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
8
+
9
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW,
10
+ not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+ See section 15 and section 16 in the supplied "LICENSE" file
12
+
13
+ This script can be used to control `dar` to backup parts of or the whole system.
14
+ """
15
+
16
+
17
+
18
+ import argcomplete
3
19
  import argparse
4
20
  import filecmp
5
21
 
@@ -11,6 +27,7 @@ import shutil
11
27
  import subprocess
12
28
  import xml.etree.ElementTree as ET
13
29
  import tempfile
30
+ import threading
14
31
 
15
32
  from argparse import ArgumentParser
16
33
  from datetime import datetime
@@ -20,6 +37,7 @@ from sys import stderr
20
37
  from sys import argv
21
38
  from sys import version_info
22
39
  from time import time
40
+ from threading import Event
23
41
  from typing import List
24
42
 
25
43
  from . import __about__ as about
@@ -29,10 +47,16 @@ from dar_backup.util import setup_logging
29
47
  from dar_backup.util import get_logger
30
48
  from dar_backup.util import BackupError
31
49
  from dar_backup.util import RestoreError
50
+ from dar_backup.util import requirements
51
+ from dar_backup.util import get_binary_info
52
+ from dar_backup.util import backup_definition_completer, list_archive_completer
32
53
 
33
54
  from dar_backup.command_runner import CommandRunner
34
55
  from dar_backup.command_runner import CommandResult
35
56
 
57
+ from dar_backup.rich_progress import show_log_driven_bar
58
+
59
+ from argcomplete.completers import FilesCompleter
36
60
 
37
61
  logger = None
38
62
  runner = None
@@ -67,7 +91,31 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
67
91
 
68
92
  logger.info(f"===> Starting {type} backup for {backup_definition}")
69
93
  try:
70
- process = runner.run(command, timeout = config_settings.command_timeout_secs)
94
+ log_basename = os.path. dirname(config_settings.logfile_location)
95
+ logfile = os.path.basename(config_settings.logfile_location)[:-4] + "-commands.log"
96
+ log_path = os.path.join( log_basename, logfile)
97
+ logger.debug(f"Commands log file: {log_path}")
98
+
99
+ # wrap a progress bar around the dar command
100
+ stop_event = Event()
101
+ session_marker = f"=== START BACKUP SESSION: {int(time())} ==="
102
+ get_logger(command_output_logger=True).info(session_marker)
103
+
104
+ progress_thread = threading.Thread(
105
+ target=show_log_driven_bar,
106
+ args=(log_path, stop_event, session_marker),
107
+ daemon=True
108
+ )
109
+ progress_thread.start()
110
+ try:
111
+ process = runner.run(command, timeout = config_settings.command_timeout_secs)
112
+ except Exception as e:
113
+ print(f"[!] Backup failed: {e}")
114
+ raise
115
+ finally:
116
+ stop_event.set()
117
+ progress_thread.join()
118
+
71
119
  if process.returncode == 0:
72
120
  logger.info(f"{type} backup completed successfully.")
73
121
  elif process.returncode == 5:
@@ -194,7 +242,33 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
194
242
  """
195
243
  result = True
196
244
  command = ['dar', '-t', backup_file, '-Q']
197
- process = runner.run(command, timeout = config_settings.command_timeout_secs)
245
+
246
+
247
+ log_basename = os.path. dirname(config_settings.logfile_location)
248
+ logfile = os.path.basename(config_settings.logfile_location)[:-4] + "-commands.log"
249
+ log_path = os.path.join( log_basename, logfile)
250
+
251
+ # wrap a progress bar around the dar command
252
+ stop_event = Event()
253
+ session_marker = f"=== START BACKUP SESSION: {int(time())} ==="
254
+ get_logger(command_output_logger=True).info(session_marker)
255
+
256
+ progress_thread = threading.Thread(
257
+ target=show_log_driven_bar,
258
+ args=(log_path, stop_event, session_marker),
259
+ daemon=True
260
+ )
261
+ progress_thread.start()
262
+ try:
263
+ process = runner.run(command, timeout = config_settings.command_timeout_secs)
264
+ except Exception as e:
265
+ print(f"[!] Backup failed: {e}")
266
+ raise
267
+ finally:
268
+ stop_event.set()
269
+ progress_thread.join()
270
+
271
+
198
272
  if process.returncode == 0:
199
273
  logger.info("Archive integrity test passed.")
200
274
  else:
@@ -243,7 +317,8 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
243
317
  if filecmp.cmp(os.path.join(config_settings.test_restore_dir, restored_file_path.lstrip("/")), os.path.join(root_path, restored_file_path.lstrip("/")), shallow=False):
244
318
  args.verbose and logger.info(f"Success: file '{restored_file_path}' matches the original")
245
319
  else:
246
- raise BackupError(f"Failure: file '{restored_file_path}' did not match the original")
320
+ result = False
321
+ logger.error(f"Failure: file '{restored_file_path}' did not match the original")
247
322
  except PermissionError:
248
323
  result = False
249
324
  logger.exception(f"Permission error while comparing files, continuing....")
@@ -402,7 +477,8 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
402
477
  if '_' in args.backup_definition:
403
478
  msg = f"Skipping backup definition: '{args.backup_definition}' due to '_' in name"
404
479
  logger.error(msg)
405
- return results.append((msg, 1))
480
+ results.append((msg, 1))
481
+ return results
406
482
  backup_definitions.append((os.path.basename(args.backup_definition).split('.')[0], os.path.join(config_settings.backup_d_dir, args.backup_definition)))
407
483
  else:
408
484
  for root, _, files in os.walk(config_settings.backup_d_dir):
@@ -453,6 +529,10 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
453
529
 
454
530
  # Perform backup
455
531
  backup_result = generic_backup(backup_type, command, backup_file, backup_definition_path, args.darrc, config_settings, args)
532
+ if not isinstance(backup_result, list) or not all(isinstance(i, tuple) and len(i) == 2 for i in backup_result):
533
+ logger.error("Unexpected return format from generic_backup")
534
+ backup_result = [("Unexpected return format from generic_backup", 1)]
535
+
456
536
  results.extend(backup_result)
457
537
 
458
538
  logger.info("Starting verification...")
@@ -620,41 +700,53 @@ INCR back of a single backup definition in backup.d
620
700
 
621
701
 
622
702
 
623
- def requirements(type: str, config_setting: ConfigSettings):
703
+ def print_markdown(source: str, from_string: bool = False, pretty: bool = True):
624
704
  """
625
- Perform PREREQ or POSTREQ requirements.
626
-
705
+ Print Markdown content either from a file or directly from a string.
706
+
627
707
  Args:
628
- type (str): The type of prereq (PREREQ, POSTREQ).
629
- config_settings (ConfigSettings): An instance of the ConfigSettings class.
708
+ source: Path to the file or Markdown string itself.
709
+ from_string: If True, treat `source` as Markdown string instead of file path.
710
+ pretty: If True, render with rich formatting if available.
711
+ """
712
+ import os
713
+ import sys
630
714
 
631
- Raises:
632
- RuntimeError: If a subprocess returns anything but zero.
715
+ content = ""
716
+ if from_string:
717
+ content = source
718
+ else:
719
+ if not os.path.exists(source):
720
+ print(f"❌ File not found: {source}")
721
+ sys.exit(1)
722
+ with open(source, "r", encoding="utf-8") as f:
723
+ content = f.read()
724
+
725
+ if pretty:
726
+ try:
727
+ from rich.console import Console
728
+ from rich.markdown import Markdown
729
+ console = Console()
730
+ console.print(Markdown(content))
731
+ except ImportError:
732
+ print("⚠️ 'rich' not installed. Falling back to plain text.\n")
733
+ print(content)
734
+ else:
735
+ print(content)
736
+
737
+
738
+
739
+ def print_changelog(path: str = None, pretty: bool = True):
740
+ if path is None:
741
+ path = Path(__file__).parent / "Changelog.md"
742
+ print_markdown(str(path), pretty=pretty)
743
+
744
+
745
+ def print_readme(path: str = None, pretty: bool = True):
746
+ if path is None:
747
+ path = Path(__file__).parent / "README.md"
748
+ print_markdown(str(path), pretty=pretty)
633
749
 
634
- subprocess.CalledProcessError: if CalledProcessError is raised in subprocess.run(), let it bobble up.
635
- """
636
- if type is None or config_setting is None:
637
- raise RuntimeError(f"requirements: 'type' or config_setting is None")
638
-
639
- allowed_types = ['PREREQ', 'POSTREQ']
640
- if type not in allowed_types:
641
- raise RuntimeError(f"requirements: {type} not in: {allowed_types}")
642
-
643
-
644
- logger.debug(f"Performing {type}")
645
- if type in config_setting.config:
646
- for key in sorted(config_setting.config[type].keys()):
647
- script = config_setting.config[type][key]
648
- try:
649
- result = subprocess.run(script, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=True)
650
- logger.debug(f"{type} {key}: '{script}' run, return code: {result.returncode}")
651
- logger.debug(f"{type} stdout:\n{result.stdout}")
652
- if result.returncode != 0:
653
- logger.error(f"{type} stderr:\n{result.stderr}")
654
- raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
655
- except subprocess.CalledProcessError as e:
656
- logger.error(f"Error executing {key}: '{script}': {e}")
657
- raise e
658
750
 
659
751
 
660
752
  def main():
@@ -670,23 +762,29 @@ def main():
670
762
  parser.add_argument('-F', '--full-backup', action='store_true', help="Perform a full backup.")
671
763
  parser.add_argument('-D', '--differential-backup', action='store_true', help="Perform differential backup.")
672
764
  parser.add_argument('-I', '--incremental-backup', action='store_true', help="Perform incremental backup.")
673
- parser.add_argument('-d', '--backup-definition', help="Specific 'recipe' to select directories and files.")
674
- parser.add_argument('--alternate-reference-archive', help="DIFF or INCR compared to specified archive.")
765
+ parser.add_argument('-d', '--backup-definition', help="Specific 'recipe' to select directories and files.").completer = backup_definition_completer
766
+ parser.add_argument('--alternate-reference-archive', help="DIFF or INCR compared to specified archive.").completer = list_archive_completer
675
767
  parser.add_argument('-c', '--config-file', type=str, help="Path to 'dar-backup.conf'", default='~/.config/dar-backup/dar-backup.conf')
676
768
  parser.add_argument('--darrc', type=str, help='Optional path to .darrc')
677
- parser.add_argument('--examples', action="store_true", help="Examples of using dar-backup.py.")
678
- parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
679
- parser.add_argument('--list-contents', help="List the contents of the specified archive.")
769
+ parser.add_argument('-l', '--list', action='store_true', help="List available archives.").completer = list_archive_completer
770
+ parser.add_argument('--list-contents', help="List the contents of the specified archive.").completer = list_archive_completer
680
771
  parser.add_argument('--selection', help="dar file selection for listing/restoring specific files/directories.")
681
772
  # parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
682
- parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.")
773
+ parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.").completer = list_archive_completer
683
774
  parser.add_argument('--restore-dir', type=str, help="Directory to restore files to.")
684
775
  parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
685
776
  parser.add_argument('--suppress-dar-msg', action='store_true', help="cancel dar options in .darrc: -vt, -vs, -vd, -vf and -va")
686
777
  parser.add_argument('--log-level', type=str, help="`debug` or `trace`", default="info")
687
778
  parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
688
779
  parser.add_argument('--do-not-compare', action='store_true', help="do not compare restores to file system")
780
+ parser.add_argument('--examples', action="store_true", help="Examples of using dar-backup.py.")
781
+ parser.add_argument("--readme", action="store_true", help="Print README.md to stdout and exit.")
782
+ parser.add_argument("--readme-pretty", action="store_true", help="Print README.md to stdout with Markdown styling and exit.")
783
+ parser.add_argument("--changelog", action="store_true", help="Print Changelog.md to stdout and exit.")
784
+ parser.add_argument("--changelog-pretty", action="store_true", help="Print Changelog.md to stdout with Markdown styling and exit.")
689
785
  parser.add_argument('-v', '--version', action='store_true', help="Show version and license information.")
786
+
787
+ argcomplete.autocomplete(parser)
690
788
  args = parser.parse_args()
691
789
 
692
790
  if args.version:
@@ -695,6 +793,20 @@ def main():
695
793
  elif args.examples:
696
794
  show_examples()
697
795
  exit(0)
796
+ elif args.readme:
797
+ print_readme(None, pretty=False)
798
+ exit(0)
799
+ elif args.readme_pretty:
800
+ print_readme(None, pretty=True)
801
+ exit(0)
802
+ elif args.changelog:
803
+ print_changelog(None, pretty=False)
804
+ exit(0)
805
+ elif args.changelog_pretty:
806
+ print_changelog(None, pretty=True)
807
+ exit(0)
808
+
809
+
698
810
 
699
811
  if not args.config_file:
700
812
  print(f"Config file not specified, exiting", file=stderr)
@@ -740,6 +852,9 @@ def main():
740
852
  logger.info(f"START TIME: {start_time}")
741
853
  logger.debug(f"`args`:\n{args}")
742
854
  logger.debug(f"`config_settings`:\n{config_settings}")
855
+ dar_manager_properties = get_binary_info(command='dar')
856
+ logger.debug(f"dar path: {dar_manager_properties['path']}")
857
+ logger.debug(f"dar version: {dar_manager_properties['version']}")
743
858
 
744
859
  file_dir = os.path.normpath(os.path.dirname(__file__))
745
860
  args.verbose and (print(f"Script directory: {file_dir}"))