dar-backup 0.5.17__py3-none-any.whl → 0.6.1__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.5.17"
1
+ __version__ = "0.6.1"
@@ -14,6 +14,7 @@ class ConfigSettings:
14
14
  max_size_verification_mb (int): The maximum size for verification in megabytes.
15
15
  min_size_verification_mb (int): The minimum size for verification in megabytes.
16
16
  no_files_verification (int): The number of files for verification.
17
+ command_timeout_secs (int): The timeout in seconds for commands.
17
18
  backup_dir (str): The directory for backups.
18
19
  test_restore_dir (str): The directory for test restores.
19
20
  backup_d_dir (str): The directory for backup.d.
@@ -38,6 +39,7 @@ class ConfigSettings:
38
39
  self.max_size_verification_mb = int(self.config['MISC']['MAX_SIZE_VERIFICATION_MB'])
39
40
  self.min_size_verification_mb = int(self.config['MISC']['MIN_SIZE_VERIFICATION_MB'])
40
41
  self.no_files_verification = int(self.config['MISC']['NO_FILES_VERIFICATION'])
42
+ self.command_timeout_secs = int(self.config['MISC']['COMMAND_TIMEOUT_SECS'])
41
43
  self.backup_dir = self.config['DIRECTORIES']['BACKUP_DIR']
42
44
  self.test_restore_dir = self.config['DIRECTORIES']['TEST_RESTORE_DIR']
43
45
  self.backup_d_dir = self.config['DIRECTORIES']['BACKUP.D_DIR']
dar_backup/dar_backup.py CHANGED
@@ -33,7 +33,7 @@ from dar_backup.util import RestoreError
33
33
  logger = None
34
34
 
35
35
 
36
- def backup(backup_file: str, backup_definition: str, darrc: str):
36
+ def backup(backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings):
37
37
  """
38
38
  Performs a full backup using the 'dar' command.
39
39
 
@@ -63,31 +63,26 @@ def backup(backup_file: str, backup_definition: str, darrc: str):
63
63
  command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-Q', "compress-exclusion", "verbose"]
64
64
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
65
65
  try:
66
- process = run_command(command)
67
- stdout, stderr = process.communicate()
66
+ process = run_command(command, config_settings.command_timeout_secs)
67
+ logger.info("Back from run_command")
68
68
  if process.returncode == 0:
69
69
  logger.info("FULL backup completed successfully.")
70
70
  elif process.returncode == 5:
71
71
  logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
72
72
  else:
73
- logger.error("dar return code: ", process.returncode)
74
- logger.error("stdout: ", stdout)
75
- logger.error("stderr: ", stderr)
76
- raise Exception(stderr)
73
+ raise Exception(str(process))
77
74
  except subprocess.CalledProcessError as e:
78
75
  logger.error(f"Backup command failed: {e}")
79
76
  raise BackupError(f"Backup command failed: {e}") from e
80
77
  except Exception as e:
81
78
  logger.exception(f"Unexpected error during backup")
82
- logger.error("stderr:", stderr)
83
- logger.error("stdout: ", stdout)
84
79
  raise BackupError(f"Unexpected error during backup: {e}") from e
85
80
 
86
81
 
87
82
 
88
83
 
89
84
 
90
- def differential_backup(backup_file: str, backup_definition: str, base_backup_file: str, darrc: str):
85
+ def differential_backup(backup_file: str, backup_definition: str, base_backup_file: str, darrc: str, config_settings: ConfigSettings):
91
86
  """
92
87
  Creates a differential backup based on a specified base backup.
93
88
 
@@ -120,16 +115,12 @@ def differential_backup(backup_file: str, backup_definition: str, base_backup_fi
120
115
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
121
116
  try:
122
117
  process = run_command(command)
123
- stdout, stderr = process.communicate()
124
118
  if process.returncode == 0:
125
119
  logger.info("DIFF backup completed successfully.")
126
120
  elif process.returncode == 5:
127
121
  logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
128
122
  else:
129
- logger.error("dar return code: ", process.returncode)
130
- logger.error("stdout: ", stdout)
131
- logger.error("stderr: ", stderr)
132
- raise Exception(stderr)
123
+ raise Exception(str(process))
133
124
  except subprocess.CalledProcessError as e:
134
125
  logger.error(f"Differential backup command failed: {e}")
135
126
  raise DifferentialBackupError(f"Differential backup command failed: {e}") from e
@@ -139,7 +130,7 @@ def differential_backup(backup_file: str, backup_definition: str, base_backup_fi
139
130
  raise DifferentialBackupError(f"Unexpected error during differential backup: {e}") from e
140
131
 
141
132
 
142
- def incremental_backup(backup_file: str, backup_definition: str, last_backup_file: str, darrc: str):
133
+ def incremental_backup(backup_file: str, backup_definition: str, last_backup_file: str, darrc: str, config_settings: ConfigSettings):
143
134
  """
144
135
  Creates an incremental backup based on the last backup file.
145
136
 
@@ -173,23 +164,18 @@ def incremental_backup(backup_file: str, backup_definition: str, last_backup_fil
173
164
  command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', last_backup_file, '-Q', "compress-exclusion", "verbose"]
174
165
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
175
166
  try:
176
- process = run_command(command)
177
- stdout, stderr = process.communicate()
167
+ process = run_command(command, config_settings.command_timeout_secs)
178
168
  if process.returncode == 0:
179
169
  logger.info("INCR backup completed successfully.")
180
170
  elif process.returncode == 5:
181
171
  logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
182
172
  else:
183
- logger.error("dar return code: ", process.returncode)
184
- logger.error("stdout: ", stdout)
185
- logger.error("stderr: ", stderr)
186
- raise Exception(stderr)
173
+ raise Exception(str(process))
187
174
  except subprocess.CalledProcessError as e:
188
175
  logger.error(f"Incremental backup command failed: {e}")
189
176
  raise IncrementalBackupError(f"Incremental backup command failed: {e}") from e
190
177
  except Exception as e:
191
178
  logger.exception(f"Unexpected error during incremental backup")
192
- logger.error("Exception details:", exc_info=True)
193
179
  raise IncrementalBackupError(f"Unexpected error during incremental backup: {e}") from e
194
180
 
195
181
 
@@ -285,12 +271,11 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
285
271
  result = True
286
272
  command = ['dar', '-t', backup_file, '-Q']
287
273
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
288
- process = run_command(command)
289
- stdout, stderr = process.communicate()
274
+ process = run_command(command, config_settings.command_timeout_secs)
290
275
  if process.returncode == 0:
291
276
  logger.info("Archive integrity test passed.")
292
277
  else:
293
- raise Exception(stderr)
278
+ raise Exception(str(process))
294
279
 
295
280
  if args.do_not_compare:
296
281
  return result
@@ -324,11 +309,9 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
324
309
  logger.info(f"Restoring file: '{restored_file_path}' from backup to: '{config_settings.test_restore_dir}' for file comparing")
325
310
  command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-O', '-Q']
326
311
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
327
- process = run_command(command)
328
- stdout, stderr = process.communicate()
312
+ process = run_command(command, config_settings.command_timeout_secs)
329
313
  if process.returncode != 0:
330
- logger.error(f"Restore failed, dar return code: {process.returncode}.")
331
- raise Exception(stderr)
314
+ raise Exception(str(process))
332
315
 
333
316
  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):
334
317
  logger.info(f"Success: file '{restored_file_path}' matches the original")
@@ -343,7 +326,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
343
326
 
344
327
 
345
328
 
346
- def restore_backup(backup_name: str, backup_dir: str, restore_dir: str, selection: str =None):
329
+ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_dir: str, selection: str =None):
347
330
  """
348
331
  Restores a backup file to a specified directory.
349
332
 
@@ -353,7 +336,7 @@ def restore_backup(backup_name: str, backup_dir: str, restore_dir: str, selectio
353
336
  restore_dir (str): The directory where the backup should be restored to.
354
337
  selection (str, optional): A selection criteria to restore specific files or directories. Defaults to None.
355
338
  """
356
- backup_file = os.path.join(backup_dir, backup_name)
339
+ backup_file = os.path.join(config_settings.backup_dir, backup_name)
357
340
  command = ['dar', '-x', backup_file, '-O', '-Q', '-D']
358
341
  if restore_dir:
359
342
  if not os.path.exists(restore_dir):
@@ -364,13 +347,14 @@ def restore_backup(backup_name: str, backup_dir: str, restore_dir: str, selectio
364
347
  command.extend(selection_criteria)
365
348
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
366
349
  try:
367
- run_command(command)
350
+ process = run_command(command, config_settings.command_timeout_secs)
351
+ if process.returncode == 0:
352
+ logger.info(f"Restore completed successfully to: '{restore_dir}'")
353
+ else:
354
+ raise Exception(str(process))
368
355
  except subprocess.CalledProcessError as e:
369
- logger.error("Exception details:", exc_info=True)
370
356
  raise RestoreError(f"Restore command failed: {e}") from e
371
357
  except Exception as e:
372
- logger.exception(f"Unexpected error during restore")
373
- logger.error("Exception details:", exc_info=True)
374
358
  raise RestoreError(f"Unexpected error during restore: {e}") from e
375
359
 
376
360
 
@@ -388,19 +372,24 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
388
372
  """
389
373
  logger.debug(f"Getting backed up files from DAR archive in xml: '{backup_name}'")
390
374
  backup_path = os.path.join(backup_dir, backup_name)
391
- command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
392
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
393
- process = run_command(command)
394
- stdout, stderr = process.communicate()
395
- # Parse the XML data
396
- root = ET.fromstring(stdout)
397
- output = None # help gc
398
- # Extract full paths and file size for all <File> elements
399
- file_paths = find_files_with_paths(root)
400
- root = None # help gc
401
- logger.trace(f"Backed up files in dar archive: '{backup_name}'")
402
- logger.trace(file_paths)
403
- return file_paths
375
+ try:
376
+ command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
377
+ logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
378
+ process = run_command(command)
379
+ # Parse the XML data
380
+ root = ET.fromstring(process.stdout)
381
+ output = None # help gc
382
+ # Extract full paths and file size for all <File> elements
383
+ file_paths = find_files_with_paths(root)
384
+ root = None # help gc
385
+ logger.trace(str(process))
386
+ logger.trace(file_paths)
387
+ return file_paths
388
+ except subprocess.CalledProcessError as e:
389
+ logger.error(f"Error listing backed up files from DAR archive: '{backup_name}'")
390
+ raise BackupError(f"Error listing backed up files from DAR archive: '{backup_name}'") from e
391
+ except Exception as e:
392
+ raise RuntimeError(f"Unexpected error listing backed up files from DAR archive: '{backup_name}'") from e
404
393
 
405
394
 
406
395
  def list_contents(backup_name, backup_dir, selection=None):
@@ -416,16 +405,26 @@ def list_contents(backup_name, backup_dir, selection=None):
416
405
  None
417
406
  """
418
407
  backup_path = os.path.join(backup_dir, backup_name)
419
- command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
420
- if selection:
421
- selection_criteria = shlex.split(selection)
422
- command.extend(selection_criteria)
423
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
424
- process = run_command(command)
425
- stdout, stderr = process.communicate()
426
- for line in stdout.splitlines():
427
- if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
428
- print(line)
408
+ try:
409
+ command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
410
+ if selection:
411
+ selection_criteria = shlex.split(selection)
412
+ command.extend(selection_criteria)
413
+ logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
414
+ process = run_command(command)
415
+ stdout,stderr = process.stdout, process.stderr
416
+ if process.returncode != 0:
417
+ logger.error(f"Error listing contents of backup: '{backup_name}'")
418
+ raise subprocess.CalledProcessError(str(process))
419
+ for line in stdout.splitlines():
420
+ if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
421
+ print(line)
422
+ except subprocess.CalledProcessError as e:
423
+ logger.error(f"Error listing contents of backup: '{backup_name}'")
424
+ raise BackupError(f"Error listing contents of backup: '{backup_name}'") from e
425
+ except Exception as e:
426
+ raise RuntimeError(f"Unexpected error listing contents of backup: '{backup_name}'") from e
427
+
429
428
 
430
429
 
431
430
  def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, backup_type: str):
@@ -484,7 +483,7 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
484
483
  continue
485
484
 
486
485
  if backup_type == 'FULL':
487
- backup(backup_file, backup_definition_path, args.darrc)
486
+ backup(backup_file, backup_definition_path, args.darrc, config_settings)
488
487
  else:
489
488
  base_backup_type = 'FULL' if backup_type == 'DIFF' else 'DIFF'
490
489
 
@@ -505,9 +504,9 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
505
504
  latest_base_backup = os.path.join(config_settings.backup_dir, base_backups[-1].rsplit('.', 2)[0])
506
505
 
507
506
  if backup_type == 'DIFF':
508
- differential_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc)
507
+ differential_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc, config_settings)
509
508
  elif backup_type == 'INCR':
510
- incremental_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc)
509
+ incremental_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc, config_settings)
511
510
 
512
511
  logger.info("Starting verification...")
513
512
  result = verify(args, backup_file, backup_definition_path, config_settings)
@@ -524,7 +523,7 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
524
523
  logger.exception(f"Error during {backup_type} backup process, continuing on next backup definition")
525
524
  logger.error("Exception details:", exc_info=True)
526
525
 
527
- def generate_par2_files(backup_file: str, configSettings: ConfigSettings):
526
+ def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
528
527
  """
529
528
  Generate PAR2 files for a given backup file in the specified backup directory.
530
529
 
@@ -538,13 +537,13 @@ def generate_par2_files(backup_file: str, configSettings: ConfigSettings):
538
537
  Returns:
539
538
  None
540
539
  """
541
- for filename in os.listdir(configSettings.backup_dir):
540
+ for filename in os.listdir(config_settings.backup_dir):
542
541
  if os.path.basename(backup_file) in filename:
543
542
  # Construct the full path to the file
544
- file_path = os.path.join(configSettings.backup_dir, filename)
543
+ file_path = os.path.join(config_settings.backup_dir, filename)
545
544
  # Run the par2 command to generate redundancy files with error correction
546
- command = ['par2', 'create', f'-r{configSettings.error_correction_percent}', '-q', '-q', file_path]
547
- process = run_command(command)
545
+ command = ['par2', 'create', f'-r{config_settings.error_correction_percent}', '-q', '-q', file_path]
546
+ process = run_command(command, config_settings.command_timeout_secs)
548
547
  if process.returncode != 0:
549
548
  logger.error(f"Error generating par2 files for {file_path}")
550
549
  raise subprocess.CalledProcessError(process.returncode, command)
@@ -628,14 +627,15 @@ def requirements(type: str, config_setting: ConfigSettings):
628
627
  for key in sorted(config_setting.config[type].keys()):
629
628
  script = config_setting.config[type][key]
630
629
  try:
631
- result = subprocess.run(script, shell=True, check=True)
630
+ result = subprocess.run(script, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=True)
632
631
  logger.info(f"{type} {key}: '{script}' run, return code: {result.returncode}")
633
632
  logger.info(f"{type} stdout:\n{result.stdout}")
633
+ if result.returncode != 0:
634
+ logger.error(f"{type} stderr:\n{result.stderr}")
635
+ raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
634
636
  except subprocess.CalledProcessError as e:
635
637
  logger.error(f"Error executing {key}: '{script}': {e}")
636
- if result:
637
- logger.error(f"{type} stderr:\n{result.stderr}")
638
- raise e
638
+ raise e
639
639
 
640
640
 
641
641
  def main():
@@ -658,8 +658,8 @@ def main():
658
658
  parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
659
659
  parser.add_argument('--list-contents', help="List the contents of the specified archive.")
660
660
  parser.add_argument('--selection', help="dar file selection for listing/restoring specific files/directories.")
661
- parser.add_argument('-r', '--restore', help="Restore specified archive.")
662
- parser.add_argument('--restore-dir', help="Directory to restore files to.")
661
+ parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
662
+ parser.add_argument('--restore-dir', nargs=1, type=str, help="Directory to restore files to.")
663
663
  parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
664
664
  parser.add_argument('--log-level', type=str, help="`debug` or `trace`", default="info")
665
665
  parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
@@ -739,29 +739,24 @@ def main():
739
739
  list_contents(args.list_contents, config_settings.backup_dir, args.selection)
740
740
  elif args.restore:
741
741
  restore_dir = args.restore_dir if args.restore_dir else config_settings.test_restore_dir
742
- restore_backup(args.restore, config_settings.backup_dir, restore_dir, args.selection)
742
+ restore_backup(args.restore, config_settings, restore_dir, args.selection)
743
743
  else:
744
744
  parser.print_help()
745
745
 
746
746
  requirements('POSTREQ', config_settings)
747
747
 
748
-
748
+ args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
749
+ sys.exit(0)
749
750
  except Exception as e:
750
751
  logger.exception("An error occurred")
751
752
  logger.error("Exception details:", exc_info=True)
752
-
753
- end_time=int(time())
754
- logger.info(f"END TIME: {end_time}")
755
-
756
- error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
757
- if len(error_lines) > 0:
758
753
  args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
759
- for line in error_lines:
760
- args.verbose and print(line)
761
- sys.exit(1)
762
- else:
763
- args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
764
- sys.exit(0)
754
+ sys.exit(1)
755
+ finally:
756
+ end_time=int(time())
757
+ logger.info(f"END TIME: {end_time}")
758
+
759
+ # error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
765
760
 
766
761
 
767
762
  if __name__ == "__main__":
dar_backup/manager.py CHANGED
@@ -66,7 +66,7 @@ def create_db(backup_def: str, config_settings: ConfigSettings):
66
66
  logger.info(f'Database created: "{database_path}"')
67
67
  else:
68
68
  logger.error(f'Something went wrong creating the database: "{database_path}"')
69
- stdout, stderr = process.communicate()
69
+ stdout, stderr = process.stdout, process.stderr
70
70
  logger.error(f"stderr: {stderr}")
71
71
  logger.error(f"stdout: {stdout}")
72
72
 
@@ -80,7 +80,7 @@ def list_db(backup_def: str, config_settings: ConfigSettings):
80
80
  return 1
81
81
  command = ['dar_manager', '--base', database_path, '--list']
82
82
  process = run_command(command)
83
- stdout, stderr = process.communicate()
83
+ stdout, stderr = process.stdout, process.stderr
84
84
  if process.returncode != 0:
85
85
  logger.error(f'Error listing catalogs for: "{database_path}"')
86
86
  logger.error(f"stderr: {stderr}")
@@ -111,7 +111,7 @@ def add_specific_archive(archive: str, config_settings: ConfigSettings):
111
111
 
112
112
  command = ['dar_manager', '--base', database_path, "--add", archive_path, "-ai", "-Q"]
113
113
  process = run_command(command)
114
- stdout, stderr = process.communicate()
114
+ stdout, stderr = process.stdout, process.stderr
115
115
 
116
116
  if process.returncode == 0:
117
117
  logger.info(f'"{archive_path}" added to it\'s catalog')
dar_backup/util.py CHANGED
@@ -7,7 +7,7 @@ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW,
7
7
  not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8
8
  See section 15 and section 16 in the supplied "LICENSE" file
9
9
  """
10
-
10
+ import typing
11
11
  import locale
12
12
  import logging
13
13
  import os
@@ -18,6 +18,8 @@ import sys
18
18
  import traceback
19
19
  from datetime import datetime
20
20
 
21
+ from typing import NamedTuple
22
+
21
23
  logger=None
22
24
 
23
25
  class BackupError(Exception):
@@ -37,7 +39,9 @@ class RestoreError(Exception):
37
39
  pass
38
40
 
39
41
 
40
- def setup_logging(log_file: str, log_level: str, log_to_stdout=False) -> logging.Logger:
42
+
43
+
44
+ def setup_logging(log_file: str, log_level: str, log_to_stdout: bool=False, logger_name: str=__name__) -> logging.Logger:
41
45
  """
42
46
  log_level can be set to "debug" or "trace" for more verbose logging.
43
47
  """
@@ -64,17 +68,19 @@ def setup_logging(log_file: str, log_level: str, log_to_stdout=False) -> logging
64
68
  level_used = TRACE_LEVEL_NUM
65
69
  logger.setLevel(TRACE_LEVEL_NUM)
66
70
 
71
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
72
+
73
+ file_handler = logging.FileHandler(log_file)
74
+ file_handler.setLevel(level_used)
75
+ file_handler.setFormatter(formatter)
76
+ logger.addHandler(file_handler)
77
+
67
78
  if log_to_stdout:
68
- # Create a handler for the console
69
79
  stdout_handler = logging.StreamHandler(sys.stdout)
70
80
  stdout_handler.setLevel(level_used)
71
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
72
81
  stdout_handler.setFormatter(formatter)
73
82
  logger.addHandler(stdout_handler)
74
83
 
75
- logging.basicConfig(filename=log_file, level=level_used,
76
- format='%(asctime)s - %(levelname)s - %(message)s')
77
-
78
84
  except Exception as e:
79
85
  print("logging not initialized, exiting.")
80
86
  traceback.print_exc()
@@ -83,30 +89,58 @@ def setup_logging(log_file: str, log_level: str, log_to_stdout=False) -> logging
83
89
  return logger
84
90
 
85
91
 
92
+ class CommandResult(NamedTuple):
93
+ """
94
+ The reult of the run_command() function.
95
+ """
96
+ process: subprocess.CompletedProcess
97
+ stdout: str
98
+ stderr: str
99
+ returncode: int
100
+ timeout: int
101
+ command: list[str]
102
+
103
+ def __str__(self):
104
+ return f"CommandResult: [Return Code: '{self.returncode}', Stdout: '{self.stdout}', Stderr: '{self.stderr}', Timeout: '{self.timeout}', Command: '{self.command}']"
86
105
 
87
106
 
88
- def run_command(command: list[str]) -> subprocess.CompletedProcess:
107
+ def run_command(command: list[str], timeout: int=30) -> typing.NamedTuple:
89
108
  """
90
109
  Executes a given command via subprocess and captures its output.
91
110
 
92
111
  Args:
93
112
  command (list): The command to be executed, represented as a list of strings.
113
+ timeout (int): The maximum time in seconds to wait for the command to complete.Defaults to 30 seconds.
94
114
 
95
115
  Returns:
96
- subprocess.CompletedProcess
116
+ a typing.NamedTuple of class dar-backup.util.CommandResult with the following properties:
117
+ - process: of type subprocess.CompletedProcess: The result of the command execution.
118
+ - stdout: of type str: The standard output of the command.
119
+ - stderr: of type str: The standard error of the command.
120
+ - returncode: of type int: The return code of the command.
121
+ - timeout: of type int: The timeout value in seconds used to run the command.
122
+ - command: of type list[str): The command executed.
97
123
 
98
124
  Raises:
125
+ subprocess.TimeoutExpired: if the command execution times out (see `timeout` parameter).
99
126
  Exception: raise exceptions during command runs.
100
127
  """
128
+ stdout = None
129
+ stderr = None
101
130
  try:
102
- logger.trace(f"Running command: {command}")
131
+ logger.debug(f"Running command: {command}")
103
132
  process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
104
- process.wait() # Popen() and wait() used, for potential future use of stdout and stderr while a subprocess is running.
133
+ stdout, stderr = process.communicate(timeout) # Wait with timeout
134
+ result = CommandResult(process=process, stdout=stdout, stderr=stderr, returncode=process.returncode, timeout=timeout, command=command)
135
+ logger.debug(f"Command result: {str(result)}")
136
+ except subprocess.TimeoutExpired:
137
+ process.terminate()
138
+ logger.error(f"Command: '{command}' timed out and was terminated.")
139
+ raise
105
140
  except Exception as e:
106
141
  logger.error(f"Error running command: {command}", exc_info=True)
107
142
  raise
108
-
109
- return process
143
+ return result
110
144
 
111
145
 
112
146
  def extract_error_lines(log_file_path: str, start_time: str, end_time: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dar-backup
3
- Version: 0.5.17
3
+ Version: 0.6.1
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: Homepage, https://github.com/per2jensen/dar-backup
6
6
  Project-URL: Issues, https://github.com/per2jensen/dar-backup/issues
@@ -44,8 +44,11 @@ Description-Content-Type: text/markdown
44
44
  Read more here: https://www.gnu.org/licenses/gpl-3.0.en.html, or have a look at the ["LICENSE"](https://github.com/per2jensen/dar-backup/blob/main/LICENSE) file in this repository.
45
45
 
46
46
  # Status
47
- As of August 8, 2024 I am using the alpha versions of `dar-backup` (alpha-0.5.9 onwards) in my automated backup routine
47
+ As of August 8, 2024 I am using the alpha versions of `dar-backup` (alpha-0.5.9 onwards) in my automated backup routine.
48
48
 
49
+ **Breaking change in version 0.6.0**
50
+
51
+ Version 0.6.0 and forwards requires the config variable *COMMAND_TIMEOUT_SECS* in the config file.
49
52
 
50
53
  # Homepage - Github
51
54
  This 'dar-backup' package lives at: https://github.com/per2jensen/dar-backup
@@ -79,6 +82,11 @@ MAX_SIZE_VERIFICATION_MB = 20
79
82
  MIN_SIZE_VERIFICATION_MB = 1
80
83
  NO_FILES_VERIFICATION = 5
81
84
 
85
+ # timeout in seconds for backup, test, restore and par2 operations
86
+ # The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
87
+ # If a timeout is not specified when using the util.run_command(), a default timeout of 30 secs is used.
88
+ COMMAND_TIMEOUT_SECS = 86400
89
+
82
90
  [DIRECTORIES]
83
91
  BACKUP_DIR = /home/user/mnt/dir/
84
92
  BACKUP.D_DIR = /home/user/.config/dar-backup/backup.d/
@@ -181,7 +189,7 @@ alias db=". ~/tmp/venv/bin/activate; dar-backup -v"
181
189
  Typing `db` at the command line gives this
182
190
  ````
183
191
  (venv) user@machine:~$ db
184
- dar-backup alpha-0.5.12
192
+ dar-backup 0.5.17
185
193
  dar-backup.py source code is here: https://github.com/per2jensen/dar-backup
186
194
  Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
187
195
  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
@@ -190,10 +198,11 @@ See section 15 and section 16 in the supplied "LICENSE" file.
190
198
 
191
199
  `dar-backup -h` gives the usage output:
192
200
  ````
193
- (venv) user@machine:~$ dar-backup -h
194
-
195
- usage: dar-backup [-h] [-F] [-D] [-I] [-d BACKUP_DEFINITION] [-c CONFIG_FILE] [--darrc DARRC] [--examples] [-l] [--list-contents LIST_CONTENTS]
196
- [--selection SELECTION] [-r RESTORE] [--restore-dir RESTORE_DIR] [--verbose] [--log-level LOG_LEVEL] [--do-not-compare] [-v]
201
+ usage: dar-backup [-h] [-F] [-D] [-I] [-d BACKUP_DEFINITION]
202
+ [--alternate-reference-archive ALTERNATE_REFERENCE_ARCHIVE] [-c CONFIG_FILE] [--darrc DARRC]
203
+ [--examples] [-l] [--list-contents LIST_CONTENTS] [--selection SELECTION] [-r RESTORE]
204
+ [--restore-dir RESTORE_DIR] [--verbose] [--log-level LOG_LEVEL] [--log-stdout]
205
+ [--do-not-compare] [-v]
197
206
 
198
207
  Backup and verify using dar backup definitions.
199
208
 
@@ -206,6 +215,8 @@ options:
206
215
  Perform incremental backup.
207
216
  -d BACKUP_DEFINITION, --backup-definition BACKUP_DEFINITION
208
217
  Specific 'recipe' to select directories and files.
218
+ --alternate-reference-archive ALTERNATE_REFERENCE_ARCHIVE
219
+ DIFF or INCR compared to specified archive.
209
220
  -c CONFIG_FILE, --config-file CONFIG_FILE
210
221
  Path to 'dar-backup.conf'
211
222
  --darrc DARRC Optional path to .darrc
@@ -222,8 +233,10 @@ options:
222
233
  --verbose Print various status messages to screen
223
234
  --log-level LOG_LEVEL
224
235
  `debug` or `trace`
236
+ --log-stdout also print log messages to stdout
225
237
  --do-not-compare do not compare restores to file system
226
- -v, --version Show version information.
238
+ -v, --version Show version and license information.
239
+
227
240
  ````
228
241
 
229
242
  ## 4
@@ -424,7 +437,7 @@ WantedBy=timers.target
424
437
  # list contents of an archive
425
438
  ```
426
439
  . <the virtual evn>/bin/activate
427
- dar-backup --list-contents example --selection "-X '*.xmp' -I '*2024-06-16*' -g home/pj/tmp/LUT-play"
440
+ dar-backup --list-contents example_FULL_2024-06-23 --selection "-X '*.xmp' -I '*2024-06-16*' -g home/pj/tmp/LUT-play"
428
441
  deactivate
429
442
  ```
430
443
  gives
@@ -533,18 +546,43 @@ Nice :-)
533
546
 
534
547
  # Restoring
535
548
 
549
+ ## default location for restores
550
+ dar-backup will use the TEST_RESTORE_DIR location as the Root for restores, if the --restore-dir option has not been supplied.
551
+
552
+ See example below to see where files are restored to.
553
+
554
+ ## --restore-dir option
555
+ When the --restore-dir option is used for restoring, a directory must be supplied.
556
+
557
+ The directory supplied functions as the Root of the restore operation.
558
+
559
+ **Example**:
560
+
561
+ A backup has been taken using this backup definition:
562
+ ```
563
+ -R /
564
+ -g home/user/Documents
565
+ ```
566
+
567
+ When restoring and using `/tmp` for --restore-dir, the restored files can be found in `/tmp/home/user/Documents`
568
+
536
569
  ## a single file
537
570
  ```
538
571
  . <the virtual env>/bin/activate
539
- # the path/to/file is relative to the Root when the backup was taken
540
572
  dar-backup --restore <archive_name> --selection "-g path/to/file"
541
573
  deactivate
542
574
  ```
575
+ ## a directory
576
+ ```
577
+ . <the virtual env>/bin/activate
578
+ dar-backup --restore <archive_name> --selection "-g path/to/directory"
579
+ deactivate
580
+ ```
581
+
543
582
 
544
583
  ## .NEF from a specific date
545
584
  ```
546
585
  . <the virtual env>/bin/activate
547
- # the path/to/file is relative to the Root when the backup was taken
548
586
  dar-backup --restore <archive_name> --selection "-X '*.xmp' -I '*2024-06-16*' -g home/pj/tmp/LUT-play"
549
587
  deactivate
550
588
  ```
@@ -0,0 +1,13 @@
1
+ dar_backup/.darrc,sha256=ggex9N6eETOS6u003_QRRJMeWbveQfkT1lDBt0XpU-I,2112
2
+ dar_backup/__about__.py,sha256=XvHFZM0padtrqitt9-p2enlBUGqc6vGvWNLx2iJv09g,21
3
+ dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ dar_backup/cleanup.py,sha256=DgmxSUwKrLLIQuYSIY_yTRhIuOMgI6ivjlQuH4u3wX4,9057
5
+ dar_backup/config_settings.py,sha256=CBMUhLOOZ-x7CRdS3vBDk4TYaGqC4N1Ot8IMH-qPaI0,3617
6
+ dar_backup/dar_backup.py,sha256=xnDuqkpo8q_X1FLKID9I_s__FQozl5EIrwNHdIJAd_0,37525
7
+ dar_backup/manager.py,sha256=lkw1ZAIdxY7WedLPKZMnHpuq_QbjkUdcG61ooiwUYpo,10197
8
+ dar_backup/util.py,sha256=6lPCFHr3MDdaLWAW9EDMZ4jdL7pt8rki-5dOXcesmP8,8955
9
+ dar_backup-0.6.1.dist-info/METADATA,sha256=m3P_iVauaMno0fAhc4iJgO_DS9FobsqV5StQzUdU6wY,22496
10
+ dar_backup-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ dar_backup-0.6.1.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
12
+ dar_backup-0.6.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
+ dar_backup-0.6.1.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- dar_backup/.darrc,sha256=ggex9N6eETOS6u003_QRRJMeWbveQfkT1lDBt0XpU-I,2112
2
- dar_backup/__about__.py,sha256=JiHkNWaQtd0jZh8vy9fMhajaON_1Y2QqWuee4eqXxd4,22
3
- dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- dar_backup/cleanup.py,sha256=DgmxSUwKrLLIQuYSIY_yTRhIuOMgI6ivjlQuH4u3wX4,9057
5
- dar_backup/config_settings.py,sha256=-PhSj0Y9lVF6YaVkkG17XT-qmnFFt-XQXBdzxhIFNkc,3455
6
- dar_backup/dar_backup.py,sha256=NKLBLA7Ma6sU72zqsRvFa0ckrrJ-iJJ_2muxtxSjTNs,36903
7
- dar_backup/manager.py,sha256=0SZbk9rekPRWwJgQwTO2Xj57v3G11kQ4i4hCSb0OZEQ,10168
8
- dar_backup/util.py,sha256=a460ISsVwc93cSuUlInCMjNNhK5D48zlopzpysver9I,7416
9
- dar_backup-0.5.17.dist-info/METADATA,sha256=JZxdM7vyX6Zn_H85hasxHaEvdZxRIE8LpLSvKHTYWPs,21132
10
- dar_backup-0.5.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- dar_backup-0.5.17.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
12
- dar_backup-0.5.17.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
- dar_backup-0.5.17.dist-info/RECORD,,