dar-backup 0.6.13.1__py3-none-any.whl → 0.6.14__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.13.1"
1
+ __version__ = "0.6.14"
dar_backup/dar_backup.py CHANGED
@@ -27,11 +27,11 @@ from dar_backup.config_settings import ConfigSettings
27
27
  from dar_backup.util import list_backups
28
28
  from dar_backup.util import run_command
29
29
  from dar_backup.util import setup_logging
30
+ from dar_backup.util import get_logger
30
31
  from dar_backup.util import BackupError
31
32
  from dar_backup.util import RestoreError
32
33
 
33
34
 
34
- RESULT = True
35
35
  logger = None
36
36
 
37
37
  def generic_backup(type: str, command: List[str], backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings, args: argparse.Namespace):
@@ -86,30 +86,37 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
86
86
 
87
87
 
88
88
 
89
- def find_files_with_paths(xml_root: ET.Element):
89
+ def find_files_with_paths(xml_doc: str):
90
90
  """
91
- Finds files within an XML element and returns a list of file paths with their sizes.
91
+ Finds files within an XML element and returns a list of tuples (file path, size).
92
92
 
93
93
  Args:
94
- xml_root (Element): The root element of the XML.
94
+ xml_root: str The XML generated by dar -l <archive> -Txml.
95
95
 
96
96
  Returns:
97
- list: A list of tuples containing file paths and their sizes.
97
+ list: A list of tuples (file path, size).
98
98
  """
99
- logger.debug("Generating list of tuples with file paths and sizes for File elements in dar xml output")
100
- files = []
101
- current_path = []
99
+ #get_logger().debug("Generating list of tuples with file paths and sizes for File elements in dar xml output")
100
+ xml_doc = re.sub(r'<!DOCTYPE[^>]*>', '', xml_doc)
101
+ root = ET.fromstring(xml_doc)
102
102
 
103
- for elem in xml_root.iter():
104
- if elem.tag == "Directory":
105
- current_path.append(elem.get('name'))
106
- elif elem.tag == "File":
107
- file_path = ("/".join(current_path + [elem.get('name')]), elem.get('size'))
108
- files.append(file_path)
109
- elif elem.tag == "Directory" and elem.get('Name') in current_path:
110
- current_path.pop()
103
+ files_list = []
111
104
 
112
- return files
105
+ def iterate_dir(element, current_path=""):
106
+ for child in element:
107
+ if child.tag == 'Directory':
108
+ dir_name = child.get('name')
109
+ new_path = f"{current_path}/{dir_name}" if current_path else dir_name
110
+ iterate_dir(child, new_path)
111
+
112
+ elif child.tag == 'File':
113
+ file_name = child.get('name')
114
+ file_size = child.get('size')
115
+ file_path = f"{current_path}/{file_name}" if current_path else file_name
116
+ files_list.append((file_path, file_size))
117
+
118
+ iterate_dir(root)
119
+ return files_list
113
120
 
114
121
 
115
122
  def find_files_between_min_and_max_size(backed_up_files: list[(str, str)], config_settings: ConfigSettings):
@@ -226,8 +233,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
226
233
  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):
227
234
  logger.info(f"Success: file '{restored_file_path}' matches the original")
228
235
  else:
229
- logger.error(f"Failure: file '{restored_file_path}' did not match the original")
230
- result = False
236
+ raise BackupError(f"Failure: file '{restored_file_path}' did not match the original")
231
237
  except PermissionError:
232
238
  result = False
233
239
  logger.exception(f"Permission error while comparing files, continuing....")
@@ -246,6 +252,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
246
252
  restore_dir (str): The directory where the backup should be restored to.
247
253
  selection (str, optional): A selection criteria to restore specific files or directories. Defaults to None.
248
254
  """
255
+ results: List[tuple] = []
249
256
  try:
250
257
  backup_file = os.path.join(config_settings.backup_dir, backup_name)
251
258
  command = ['dar', '-x', backup_file, '-Q', '-D']
@@ -274,6 +281,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
274
281
  except Exception as e:
275
282
  raise RestoreError(f"Unexpected error during restore: {e}") from e
276
283
 
284
+ return results
277
285
 
278
286
 
279
287
  def get_backed_up_files(backup_name: str, backup_dir: str):
@@ -292,15 +300,9 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
292
300
  try:
293
301
  command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
294
302
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
295
- process = run_command(command)
303
+ command_result = run_command(command)
296
304
  # Parse the XML data
297
- root = ET.fromstring(process.stdout)
298
- output = None # help gc
299
- # Extract full paths and file size for all <file> elements
300
- file_paths = find_files_with_paths(root)
301
- root = None # help gc
302
- logger.trace(str(process))
303
- logger.trace(file_paths)
305
+ file_paths = find_files_with_paths(command_result.stdout)
304
306
  return file_paths
305
307
  except subprocess.CalledProcessError as e:
306
308
  logger.error(f"Error listing backed up files from DAR archive: '{backup_name}'")
@@ -371,7 +373,7 @@ def create_backup_command(backup_type: str, backup_file: str, darrc: str, backup
371
373
 
372
374
 
373
375
 
374
- def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, backup_type: str):
376
+ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, backup_type: str) -> List[str]:
375
377
  """
376
378
  Perform backup operation.
377
379
 
@@ -379,21 +381,27 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
379
381
  args: Command-line arguments.
380
382
  config_settings: An instance of the ConfigSettings class.
381
383
  backup_type: Type of backup (FULL, DIFF, INCR).
384
+
385
+ Returns:
386
+ List[tuples] - each tuple consists of (<str message>, <exit code>)
382
387
  """
383
- logger.debug(f"perform_backup({backup_type}) started")
384
388
  backup_definitions = []
389
+ results: List[tuple] = []
385
390
 
386
391
  # Gather backup definitions
387
392
  if args.backup_definition:
388
393
  if '_' in args.backup_definition:
389
- logger.error(f"Skipping backup definition: '{args.backup_definition}' due to '_' in name")
390
- return
394
+ msg = f"Skipping backup definition: '{args.backup_definition}' due to '_' in name"
395
+ logger.error(msg)
396
+ return results.append((msg, 1))
391
397
  backup_definitions.append((os.path.basename(args.backup_definition).split('.')[0], os.path.join(config_settings.backup_d_dir, args.backup_definition)))
392
398
  else:
393
399
  for root, _, files in os.walk(config_settings.backup_d_dir):
394
400
  for file in files:
395
401
  if '_' in file:
396
- logger.error(f"Skipping backup definition: '{file}' due to '_' in name")
402
+ msg = f"Skipping backup definition: '{file} due to '_' in: name"
403
+ logger.error(msg)
404
+ results.append((msg, 1))
397
405
  continue
398
406
  backup_definitions.append((file.split('.')[0], os.path.join(root, file)))
399
407
 
@@ -403,7 +411,9 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
403
411
  backup_file = os.path.join(config_settings.backup_dir, f"{backup_definition}_{backup_type}_{date}")
404
412
 
405
413
  if os.path.exists(backup_file + '.1.dar'):
406
- logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
414
+ msg = f"Backup file {backup_file}.1.dar already exists. Skipping backup [1]."
415
+ logger.error(msg)
416
+ results.append((msg, 1))
407
417
  continue
408
418
 
409
419
  latest_base_backup = None
@@ -414,15 +424,18 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
414
424
  latest_base_backup = os.path.join(config_settings.backup_dir, args.alternate_reference_archive)
415
425
  logger.info(f"Using alternate reference archive: {latest_base_backup}")
416
426
  if not os.path.exists(latest_base_backup + '.1.dar'):
417
- logger.error(f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist, exiting.")
418
- exit(1)
427
+ msg = f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist, exiting..."
428
+ logger.error(msg)
429
+ results.append((msg, 1))
430
+ return results
419
431
  else:
420
432
  base_backups = sorted(
421
433
  [f for f in os.listdir(config_settings.backup_dir) if f.startswith(f"{backup_definition}_{base_backup_type}_") and f.endswith('.1.dar')],
422
434
  key=lambda x: datetime.strptime(x.split('_')[-1].split('.')[0], '%Y-%m-%d')
423
435
  )
424
436
  if not base_backups:
425
- logger.warning(f"No {base_backup_type} backup found for {backup_definition}. Skipping {backup_type} backup.")
437
+ msg = f"No {base_backup_type} backup found for {backup_definition}. Skipping {backup_type} backup."
438
+ results.append((msg, 1))
426
439
  continue
427
440
  latest_base_backup = os.path.join(config_settings.backup_dir, base_backups[-1].rsplit('.', 2)[0])
428
441
 
@@ -437,19 +450,19 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
437
450
  if verify_result:
438
451
  logger.info("Verification completed successfully.")
439
452
  else:
440
- logger.error("Verification failed.")
441
-
442
-
453
+ msg = f"Verification of '{backup_file}' failed."
454
+ logger.error(msg)
455
+ results.append((msg, 1))
443
456
  logger.info("Generate par2 redundancy files.")
444
457
  generate_par2_files(backup_file, config_settings, args)
445
458
  logger.info("par2 files completed successfully.")
446
459
 
447
460
  except Exception as e:
448
- global RESULT
449
- RESULT = False
461
+ results.aapend((repr(e), 1))
450
462
  logger.exception(f"Error during {backup_type} backup process, continuing to next backup definition.")
451
463
 
452
-
464
+ logger.trace(f"perform_backup() results[]: {results}")
465
+ return results
453
466
 
454
467
  def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args):
455
468
  """
@@ -593,18 +606,23 @@ INCR back of a single backup definition in backup.d
593
606
 
594
607
  def requirements(type: str, config_setting: ConfigSettings):
595
608
  """
596
- Perform PREREQ or POSTREQ requisites.
609
+ Perform PREREQ or POSTREQ requirements.
597
610
 
598
611
  Args:
599
612
  type (str): The type of prereq (PREREQ, POSTREQ).
600
613
  config_settings (ConfigSettings): An instance of the ConfigSettings class.
601
614
 
602
615
  Raises:
603
- RuntimeError: If a subprocess invoked during the backup process exits with a non-zero status.
616
+ RuntimeError: If a subprocess returns anything but zero.
617
+
618
+ subprocess.CalledProcessError: if CalledProcessError is raised in subprocess.run(), let it bobble up.
604
619
  """
605
- if str is None or config_setting is None:
606
- logger.error(f"requirements: {type} or config_setting is None, existing")
607
- raise RuntimeError(f"requirements: {type} or config_setting is None, existing")
620
+ if type is None or config_setting is None:
621
+ raise RuntimeError(f"requirements: 'type' or config_setting is None")
622
+
623
+ allowed_types = ['PREREQ', 'POSTREQ']
624
+ if type not in allowed_types:
625
+ raise RuntimeError(f"requirements: {type} not in: {allowed_types}")
608
626
 
609
627
 
610
628
  logger.info(f"Performing {type}")
@@ -624,7 +642,8 @@ def requirements(type: str, config_setting: ConfigSettings):
624
642
 
625
643
 
626
644
  def main():
627
- global logger, RESULT
645
+ global logger
646
+ results: List[(str,int)] = [] # a list op tuples (<msg>, <exit code>)
628
647
 
629
648
  MIN_PYTHON_VERSION = (3, 9)
630
649
  if version_info < MIN_PYTHON_VERSION:
@@ -737,26 +756,27 @@ def main():
737
756
  if args.list:
738
757
  list_backups(config_settings.backup_dir, args.backup_definition)
739
758
  elif args.full_backup and not args.differential_backup and not args.incremental_backup:
740
- perform_backup(args, config_settings, "FULL")
759
+ results.extend(perform_backup(args, config_settings, "FULL"))
741
760
  elif args.differential_backup and not args.full_backup and not args.incremental_backup:
742
- perform_backup(args, config_settings, "DIFF")
761
+ results.extend(perform_backup(args, config_settings, "DIFF"))
743
762
  elif args.incremental_backup and not args.full_backup and not args.differential_backup:
744
- perform_backup(args, config_settings, "INCR")
763
+ results.extend(perform_backup(args, config_settings, "INCR"))
764
+ logger.debug(f"results from perform_backup(): {results}")
745
765
  elif args.list_contents:
746
766
  list_contents(args.list_contents, config_settings.backup_dir, args.selection)
747
767
  elif args.restore:
748
768
  logger.debug(f"Restoring {args.restore} to {restore_dir}")
749
- restore_backup(args.restore, config_settings, restore_dir, args.darrc, args.selection)
769
+ results.extend(restore_backup(args.restore, config_settings, restore_dir, args.darrc, args.selection))
750
770
  else:
751
771
  parser.print_help()
752
772
 
773
+ logger.debug(f"results[]: {results}")
774
+
753
775
  requirements('POSTREQ', config_settings)
754
776
 
755
777
  except Exception as e:
756
- logger.exception("An error occurred")
757
778
  logger.error("Exception details:", exc_info=True)
758
- args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
759
- RESULT = False
779
+ results.append((repr(e), 1))
760
780
  finally:
761
781
  end_time=int(time())
762
782
  logger.info(f"END TIME: {end_time}")
@@ -767,9 +787,26 @@ def main():
767
787
  logger.info(f"Removed filtered .darrc: {args.darrc}")
768
788
 
769
789
 
770
- if RESULT:
771
- exit(0)
772
- else:
790
+ error = False
791
+ logger.debug(f"results[]: {results}")
792
+ if results:
793
+ i = 0
794
+ for result in results:
795
+ if isinstance(result, tuple) and len(result) == 2:
796
+ msg, exit_code = result
797
+ logger.debug(f"exit code: {exit_code}, msg: {msg}")
798
+ if exit_code == 1:
799
+ error = True
800
+ args.verbose and print(msg)
801
+ else:
802
+ logger.error(f"not correct result type: {result}, which must be a tuple (<msg>, <exit_code>)")
803
+ i=i+1
804
+ if error:
805
+ args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
773
806
  exit(1)
807
+ else:
808
+ args.verbose and print("\033[1m\033[32mSuccess\033[0m all backups completed")
809
+ exit(0)
810
+
774
811
  if __name__ == "__main__":
775
812
  main()
dar_backup/manager.py CHANGED
@@ -233,7 +233,7 @@ def add_specific_archive(archive: str, config_settings: ConfigSettings, director
233
233
  database_path = os.path.realpath(os.path.join(config_settings.backup_dir, database))
234
234
  logger.info(f'Add "{archive_path}" to catalog: "{database}"')
235
235
 
236
- command = ['dar_manager', '--base', database_path, "--add", archive_path, "-ai", "-Q"]
236
+ command = ['dar_manager', '--base', database_path, "--add", archive_path, "-Q"]
237
237
  process = run_command(command)
238
238
  stdout, stderr = process.stdout, process.stderr
239
239
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dar-backup
3
- Version: 0.6.13.1
3
+ Version: 0.6.14
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/tree/main/v2
6
6
  Project-URL: Changelog, https://github.com/per2jensen/dar-backup/blob/main/v2/Changelog.md
@@ -1355,6 +1355,8 @@ In order to not clutter that log file with the output of commands being run, a n
1355
1355
  - FULL, DIFF and INCR backups.
1356
1356
  - cleanup.
1357
1357
 
1358
+ - fix --log-stdout spams console with command output
1359
+
1358
1360
  ## Reference
1359
1361
 
1360
1362
  ### dar-backup
@@ -0,0 +1,16 @@
1
+ dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
2
+ dar_backup/__about__.py,sha256=gpxZhHkmXQLM1tz9VrFJMN82NzCMmjk_6Z0FrzPb8BE,22
3
+ dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ dar_backup/clean_log.py,sha256=cGhtKYnQJ2ceNQfw5XcCln_WNBasbmlfhO3kRydjDNk,5196
5
+ dar_backup/cleanup.py,sha256=1g2si9-jPEL8T4OKaiGSKFGsF6rWh-Ke1-zQHE7HaPc,11703
6
+ dar_backup/config_settings.py,sha256=uicCq6FnpxPFzbv7xfYSXNnQf1tfLk1Z3VIO9M71fsE,4659
7
+ dar_backup/dar-backup.conf,sha256=-wXqP4vj5TS7cCfMJN1nbk-1Sqkq00Tg22ySQXynUF4,902
8
+ dar_backup/dar_backup.py,sha256=8RcSVsqRy-BUCjJBroo0oKC8ZabZ5HXZrRFwxZV82b4,37360
9
+ dar_backup/installer.py,sha256=ehp4KSgTc8D9Edsyve5v3NY2MuDbuTFYQQPgou8woV8,4331
10
+ dar_backup/manager.py,sha256=sQl0xdWwBgui11S9Ekg0hOSC4gt89nz_Z8Bt8IPXCDw,21640
11
+ dar_backup/util.py,sha256=lfgfxC_C6x3I8f9vzyxpQE7P-7rm6tTEr3P-2uWlVtQ,12278
12
+ dar_backup-0.6.14.dist-info/METADATA,sha256=P8yWZZJ8WDrSMmN8UCbEoKZU2y76L_-05Y2vPmubNOA,72750
13
+ dar_backup-0.6.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ dar_backup-0.6.14.dist-info/entry_points.txt,sha256=Z7P5BUbhtJxo8_nB9qNIMay2eGDbsMKB3Fjwv3GMa4g,202
15
+ dar_backup-0.6.14.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
+ dar_backup-0.6.14.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
2
- dar_backup/__about__.py,sha256=WhY38gJ9InHtiokbumhF5t6Q_JtwytMg5oigYIZ6CZ0,24
3
- dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- dar_backup/clean_log.py,sha256=cGhtKYnQJ2ceNQfw5XcCln_WNBasbmlfhO3kRydjDNk,5196
5
- dar_backup/cleanup.py,sha256=1g2si9-jPEL8T4OKaiGSKFGsF6rWh-Ke1-zQHE7HaPc,11703
6
- dar_backup/config_settings.py,sha256=uicCq6FnpxPFzbv7xfYSXNnQf1tfLk1Z3VIO9M71fsE,4659
7
- dar_backup/dar-backup.conf,sha256=-wXqP4vj5TS7cCfMJN1nbk-1Sqkq00Tg22ySQXynUF4,902
8
- dar_backup/dar_backup.py,sha256=yJjDqyPBWsjsMZeMbEgLzLt0N8R2y-YZGdfxvZjm8gs,35723
9
- dar_backup/installer.py,sha256=ehp4KSgTc8D9Edsyve5v3NY2MuDbuTFYQQPgou8woV8,4331
10
- dar_backup/manager.py,sha256=VBeZEIETDL_Lxhmo-T47xSQAxbOPGU-ZLI7GFXJx-Go,21647
11
- dar_backup/util.py,sha256=lfgfxC_C6x3I8f9vzyxpQE7P-7rm6tTEr3P-2uWlVtQ,12278
12
- dar_backup-0.6.13.1.dist-info/METADATA,sha256=zWP8JXewEWB5x-dbTjnDBJ8PBFiFdwBPtkptaSP9Fg4,72698
13
- dar_backup-0.6.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- dar_backup-0.6.13.1.dist-info/entry_points.txt,sha256=Z7P5BUbhtJxo8_nB9qNIMay2eGDbsMKB3Fjwv3GMa4g,202
15
- dar_backup-0.6.13.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
16
- dar_backup-0.6.13.1.dist-info/RECORD,,