dar-backup 0.6.1__py3-none-any.whl → 0.6.3__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.1"
1
+ __version__ = "0.6.3"
dar_backup/dar_backup.py CHANGED
@@ -17,56 +17,49 @@ from argparse import ArgumentParser
17
17
  from datetime import datetime
18
18
  from pathlib import Path
19
19
  from time import time
20
+ from typing import List
20
21
 
21
22
  from . import __about__ as about
22
23
  from dar_backup.config_settings import ConfigSettings
23
24
  from dar_backup.util import list_backups
24
25
  from dar_backup.util import run_command
25
26
  from dar_backup.util import setup_logging
26
- from dar_backup.util import extract_error_lines
27
27
  from dar_backup.util import BackupError
28
- from dar_backup.util import DifferentialBackupError
29
- from dar_backup.util import IncrementalBackupError
30
28
  from dar_backup.util import RestoreError
31
29
 
32
30
 
33
31
  logger = None
34
32
 
35
-
36
- def backup(backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings):
33
+ def generic_backup(type: str, command: List[str], backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings):
37
34
  """
38
- Performs a full backup using the 'dar' command.
35
+ Performs a backup using the 'dar' command.
39
36
 
40
37
  This function initiates a full backup operation by constructing and executing a command
41
38
  with the 'dar' utility. It checks if the backup file already exists to avoid overwriting
42
39
  previous backups. If the backup file does not exist, it proceeds with the backup operation.
43
40
 
44
41
  Args:
42
+ type (str): The type of backup (FULL, DIFF, INCR).
43
+ command (List[str]): The command to execute for the backup operation.
45
44
  backup_file (str): The base name of the backup file. The actual backup will be saved
46
45
  as '{backup_file}.1.dar'.
47
46
  backup_definition (str): The path to the backup definition file. This file contains
48
47
  specific instructions for the 'dar' utility, such as which
49
48
  directories to include or exclude.
49
+ darrc (str): The path to the '.darrc' configuration file.
50
+ config_settings (ConfigSettings): An instance of the ConfigSettings class.
51
+
50
52
 
51
- Note:
52
- This function logs an error and returns early if the backup file already exists.
53
- It logs the command being executed and reports upon successful completion of the backup.
54
53
 
55
54
  Raises:
56
55
  BackupError: If an error occurs during the backup process.
57
56
  """
58
- if os.path.exists(backup_file + '.1.dar'):
59
- logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
60
- return
61
-
62
- logger.info(f"===> Starting FULL backup for {backup_definition}")
63
- command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-Q', "compress-exclusion", "verbose"]
57
+ logger.info(f"===> Starting {type} backup for {backup_definition}")
64
58
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
65
59
  try:
66
60
  process = run_command(command, config_settings.command_timeout_secs)
67
- logger.info("Back from run_command")
68
61
  if process.returncode == 0:
69
- logger.info("FULL backup completed successfully.")
62
+ logger.info(f"{type} backup completed successfully.")
70
63
  elif process.returncode == 5:
71
64
  logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
72
65
  else:
@@ -78,106 +71,6 @@ def backup(backup_file: str, backup_definition: str, darrc: str, config_setting
78
71
  logger.exception(f"Unexpected error during backup")
79
72
  raise BackupError(f"Unexpected error during backup: {e}") from e
80
73
 
81
-
82
-
83
-
84
-
85
- def differential_backup(backup_file: str, backup_definition: str, base_backup_file: str, darrc: str, config_settings: ConfigSettings):
86
- """
87
- Creates a differential backup based on a specified base backup.
88
-
89
- This function performs a differential backup by comparing the current state of the data
90
- against a specified base backup file. It captures only the changes made since that base
91
- backup, resulting in a smaller and faster backup process compared to a full backup.
92
-
93
- Args:
94
- backup_file (str): The base name for the differential backup file. The actual backup
95
- will be saved as '{backup_file}.1.dar'.
96
- backup_definition (str): The path to the backup definition file. This file contains
97
- specific instructions for the 'dar' utility, such as which
98
- directories to include or exclude.
99
- base_backup_file (str): The base name of the full backup file that serves as the
100
- reference point for the differential backup.
101
-
102
- Note:
103
- This function logs an error and returns early if the differential backup file already exists.
104
- It logs the command being executed and reports upon successful completion of the differential backup.
105
-
106
- Raises:
107
- DifferentialBackupError: If the differential backup command fails or encounters an unexpected error.
108
- """
109
- if os.path.exists(backup_file + '.1.dar'):
110
- logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
111
- return
112
-
113
- logger.info(f"===> Starting DIFF backup for {backup_definition}")
114
- command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', base_backup_file, '-Q', "compress-exclusion", "verbose"]
115
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
116
- try:
117
- process = run_command(command)
118
- if process.returncode == 0:
119
- logger.info("DIFF backup completed successfully.")
120
- elif process.returncode == 5:
121
- logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
122
- else:
123
- raise Exception(str(process))
124
- except subprocess.CalledProcessError as e:
125
- logger.error(f"Differential backup command failed: {e}")
126
- raise DifferentialBackupError(f"Differential backup command failed: {e}") from e
127
- except Exception as e:
128
- logger.exception(f"Unexpected error during differential backup")
129
- logger.error("Exception details:", exc_info=True)
130
- raise DifferentialBackupError(f"Unexpected error during differential backup: {e}") from e
131
-
132
-
133
- def incremental_backup(backup_file: str, backup_definition: str, last_backup_file: str, darrc: str, config_settings: ConfigSettings):
134
- """
135
- Creates an incremental backup based on the last backup file.
136
-
137
- This function performs an incremental backup by comparing the current state of the data
138
- against the last backup file, whether it's a full backup or the most recent incremental backup.
139
- It captures only the changes made since that last backup, making it efficient for frequent
140
- backups with minimal data changes.
141
-
142
- Args:
143
- backup_file (str): The base name for the incremental backup file. The actual backup
144
- will be saved with a unique identifier to distinguish it from other backups.
145
- backup_definition (str): The path to the backup definition file. This file contains
146
- specific instructions for the 'dar' utility, such as which
147
- directories to include or exclude.
148
- last_backup_file (str): The base name of the last backup file (full or incremental) that
149
- serves as the reference point for the incremental backup.
150
-
151
- Note:
152
- This function checks if the incremental backup file already exists to prevent overwriting
153
- previous backups. It logs the command being executed and reports upon successful completion
154
- of the incremental backup.
155
-
156
- Raises:
157
- IncrementalBackupError: If the incremental backup command fails or an unexpected error occurs.
158
- """
159
- if os.path.exists(backup_file + '.1.dar'):
160
- logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
161
- return
162
-
163
- logger.info(f"===> Starting INCR backup for {backup_definition}")
164
- command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', last_backup_file, '-Q', "compress-exclusion", "verbose"]
165
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
166
- try:
167
- process = run_command(command, config_settings.command_timeout_secs)
168
- if process.returncode == 0:
169
- logger.info("INCR backup completed successfully.")
170
- elif process.returncode == 5:
171
- logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
172
- else:
173
- raise Exception(str(process))
174
- except subprocess.CalledProcessError as e:
175
- logger.error(f"Incremental backup command failed: {e}")
176
- raise IncrementalBackupError(f"Incremental backup command failed: {e}") from e
177
- except Exception as e:
178
- logger.exception(f"Unexpected error during incremental backup")
179
- raise IncrementalBackupError(f"Unexpected error during incremental backup: {e}") from e
180
-
181
74
 
182
75
  # Function to recursively find <File> tags and build their full paths
183
76
  def find_files_with_paths(element: ET, current_path=""):
@@ -331,7 +224,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
331
224
  Restores a backup file to a specified directory.
332
225
 
333
226
  Args:
334
- backup_name (str): The name of the backup file.
227
+ backup_name (str): The base name of the backup file, without the "slice number.dar"
335
228
  backup_dir (str): The directory where the backup file is located.
336
229
  restore_dir (str): The directory where the backup should be restored to.
337
230
  selection (str, optional): A selection criteria to restore specific files or directories. Defaults to None.
@@ -342,16 +235,19 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
342
235
  if not os.path.exists(restore_dir):
343
236
  os.makedirs(restore_dir)
344
237
  command.extend(['-R', restore_dir])
238
+ else:
239
+ raise RestoreError("Restore directory ('-R <dir>') not specified")
345
240
  if selection:
346
241
  selection_criteria = shlex.split(selection)
347
242
  command.extend(selection_criteria)
348
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
243
+ logger.info(f"Running restore command: {' '.join(map(shlex.quote, command))}")
349
244
  try:
350
245
  process = run_command(command, config_settings.command_timeout_secs)
351
246
  if process.returncode == 0:
352
247
  logger.info(f"Restore completed successfully to: '{restore_dir}'")
353
248
  else:
354
- raise Exception(str(process))
249
+ logger.error(f"Restore command failed: \n ==> stdout: {process.stdout}, \n ==> stderr: {process.stderr}")
250
+ raise RestoreError(str(process))
355
251
  except subprocess.CalledProcessError as e:
356
252
  raise RestoreError(f"Restore command failed: {e}") from e
357
253
  except Exception as e:
@@ -405,6 +301,7 @@ def list_contents(backup_name, backup_dir, selection=None):
405
301
  None
406
302
  """
407
303
  backup_path = os.path.join(backup_dir, backup_name)
304
+
408
305
  try:
409
306
  command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
410
307
  if selection:
@@ -427,6 +324,32 @@ def list_contents(backup_name, backup_dir, selection=None):
427
324
 
428
325
 
429
326
 
327
+
328
+ def create_backup_command(backup_type: str, backup_file: str, darrc: str, backup_definition_path: str, latest_base_backup: str = None) -> List[str]:
329
+ """
330
+ Generate the backup command for the specified backup type.
331
+
332
+ Args:
333
+ backup_type (str): The type of backup (FULL, DIFF, INCR).
334
+ backup_file (str): The backup file path. Example: /path/to/example_2021-01-01_FULL
335
+ darrc (str): Path to the .darrc configuration file.
336
+ backup_definition_path (str): Path to the backup definition file.
337
+ latest_base_backup (str, optional): Path to the latest base backup for DIFF or INCR types.
338
+
339
+ Returns:
340
+ List[str]: The constructed backup command.
341
+ """
342
+ base_command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition_path, '-Q', "compress-exclusion", "verbose"]
343
+
344
+ if backup_type in ['DIFF', 'INCR']:
345
+ if not latest_base_backup:
346
+ raise ValueError(f"Base backup is required for {backup_type} backups.")
347
+ base_command.extend(['-A', latest_base_backup])
348
+
349
+ return base_command
350
+
351
+
352
+
430
353
  def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, backup_type: str):
431
354
  """
432
355
  Perform backup operation.
@@ -434,32 +357,12 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
434
357
  Args:
435
358
  args: Command-line arguments.
436
359
  config_settings: An instance of the ConfigSettings class.
437
- backup_d: Directory containing backup definitions.
438
- backup_dir: Directory to store backup files.
439
- test_restore_dir: Directory to test restore backup files.
440
360
  backup_type: Type of backup (FULL, DIFF, INCR).
441
- min_size_verification_mb: Minimum size for verification in MB.
442
- max_size_verification_mb: Maximum size for verification in MB.
443
- no_files_verification: Flag indicating whether to skip file verification.
444
-
445
- Returns:
446
- None
447
-
448
- Raises:
449
- FileNotFoundError: If `backup_d` does not exist or a specified backup definition file does not exist.
450
- PermissionError: If there is insufficient permission to access directories or files specified.
451
- OSError: For various system-related errors, such as exhaustion of file descriptors.
452
- ValueError: If there is an issue with the format string in `datetime.now().strftime`.
453
- subprocess.CalledProcessError: If a subprocess invoked during the backup process exits with a non-zero status.
454
- Exception: Catches any unexpected exceptions that may occur during the backup process.
455
-
456
- Note:
457
- This function assumes that any exceptions raised by the `backup` function or related subprocesses are handled
458
- within those functions or propagated up to be handled by the caller of `perform_backup`.
459
361
  """
460
362
  logger.debug(f"perform_backup({backup_type}) started")
461
363
  backup_definitions = []
462
364
 
365
+ # Gather backup definitions
463
366
  if args.backup_definition:
464
367
  if '_' in args.backup_definition:
465
368
  logger.error(f"Skipping backup definition: '{args.backup_definition}' due to '_' in name")
@@ -482,16 +385,15 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
482
385
  logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
483
386
  continue
484
387
 
485
- if backup_type == 'FULL':
486
- backup(backup_file, backup_definition_path, args.darrc, config_settings)
487
- else:
388
+ latest_base_backup = None
389
+ if backup_type in ['DIFF', 'INCR']:
488
390
  base_backup_type = 'FULL' if backup_type == 'DIFF' else 'DIFF'
489
-
391
+
490
392
  if args.alternate_reference_archive:
491
- latest_base_backup = os.path.join(config_settings.backup_dir, args.alternate_reference_archive) # expects alternerate reference archive to be without slice number
393
+ latest_base_backup = os.path.join(config_settings.backup_dir, args.alternate_reference_archive)
492
394
  logger.info(f"Using alternate reference archive: {latest_base_backup}")
493
395
  if not os.path.exists(latest_base_backup + '.1.dar'):
494
- logger.error(f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist, exciting.")
396
+ logger.error(f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist, exiting.")
495
397
  sys.exit(1)
496
398
  else:
497
399
  base_backups = sorted(
@@ -503,33 +405,36 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
503
405
  continue
504
406
  latest_base_backup = os.path.join(config_settings.backup_dir, base_backups[-1].rsplit('.', 2)[0])
505
407
 
506
- if backup_type == 'DIFF':
507
- differential_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc, config_settings)
508
- elif backup_type == 'INCR':
509
- incremental_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc, config_settings)
408
+ # Generate the backup command
409
+ command = create_backup_command(backup_type, backup_file, args.darrc, backup_definition_path, latest_base_backup)
410
+
411
+ # Perform backup
412
+ generic_backup(backup_type, command, backup_file, backup_definition_path, args.darrc, config_settings)
510
413
 
511
414
  logger.info("Starting verification...")
512
415
  result = verify(args, backup_file, backup_definition_path, config_settings)
513
416
  if result:
514
417
  logger.info("Verification completed successfully.")
515
418
  else:
516
- logger.error("Verification failed.")
419
+ logger.error("Verification failed.")
420
+
517
421
  if config_settings.par2_enabled:
518
- logger.info("Generate par2 redundancy files")
519
- generate_par2_files(backup_file, config_settings) # do this even if verification failed, because verification could fail on an open file.
422
+ logger.info("Generate par2 redundancy files.")
423
+ generate_par2_files(backup_file, config_settings, args)
520
424
  logger.info("par2 files completed successfully.")
521
- # we want to continue with other backup definitions, thus only logging an error
522
425
  except Exception as e:
523
- logger.exception(f"Error during {backup_type} backup process, continuing on next backup definition")
524
- logger.error("Exception details:", exc_info=True)
426
+ logger.exception(f"Error during {backup_type} backup process, continuing to next backup definition.")
427
+
428
+
525
429
 
526
- def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
430
+ def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args):
527
431
  """
528
432
  Generate PAR2 files for a given backup file in the specified backup directory.
529
433
 
530
434
  Args:
531
435
  backup_file (str): The name of the backup file.
532
- backup_dir (str): The path to the backup directory.
436
+ config_settings: The configuration settings object.
437
+ args: The command-line arguments object.
533
438
 
534
439
  Raises:
535
440
  subprocess.CalledProcessError: If the par2 command fails to execute.
@@ -537,19 +442,39 @@ def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
537
442
  Returns:
538
443
  None
539
444
  """
445
+ # Regular expression to match DAR slice files
446
+ dar_slice_pattern = re.compile(rf"{re.escape(os.path.basename(backup_file))}\.([0-9]+)\.dar")
447
+
448
+ # List of DAR slice files to be processed
449
+ dar_slices: List[str] = []
450
+
540
451
  for filename in os.listdir(config_settings.backup_dir):
541
- if os.path.basename(backup_file) in filename:
542
- # Construct the full path to the file
543
- file_path = os.path.join(config_settings.backup_dir, filename)
544
- # Run the par2 command to generate redundancy files with error correction
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)
547
- if process.returncode != 0:
548
- logger.error(f"Error generating par2 files for {file_path}")
549
- raise subprocess.CalledProcessError(process.returncode, command)
550
- logger.debug(f"par2 files generated for {file_path}")
452
+ match = dar_slice_pattern.match(filename)
453
+ if match:
454
+ dar_slices.append(filename)
551
455
 
456
+ # Sort the DAR slices based on the slice number
457
+ dar_slices.sort(key=lambda x: int(dar_slice_pattern.match(x).group(1)))
458
+ number_of_slices = len(dar_slices)
459
+ counter = 1
552
460
 
461
+ for slice_file in dar_slices:
462
+ file_path = os.path.join(config_settings.backup_dir, slice_file)
463
+
464
+ if args.verbose or args.log_level == "debug" or args.log_level == "trace":
465
+ logger.info(f"{counter}/{number_of_slices}: Now generating par2 files for {file_path}")
466
+
467
+ # Run the par2 command to generate redundancy files with error correction
468
+ command = ['par2', 'create', f'-r{config_settings.error_correction_percent}', '-q', '-q', file_path]
469
+ process = run_command(command, config_settings.command_timeout_secs)
470
+
471
+ if process.returncode == 0:
472
+ if args.verbose or args.log_level == "debug" or args.log_level == "trace":
473
+ logger.info(f"{counter}/{number_of_slices}: Done")
474
+ else:
475
+ logger.error(f"Error generating par2 files for {file_path}")
476
+ raise subprocess.CalledProcessError(process.returncode, command)
477
+ counter += 1
553
478
 
554
479
 
555
480
 
@@ -658,8 +583,9 @@ def main():
658
583
  parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
659
584
  parser.add_argument('--list-contents', help="List the contents of the specified archive.")
660
585
  parser.add_argument('--selection', help="dar file selection for listing/restoring specific files/directories.")
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.")
586
+ # parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
587
+ parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.")
588
+ parser.add_argument('--restore-dir', type=str, help="Directory to restore files to.")
663
589
  parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
664
590
  parser.add_argument('--log-level', type=str, help="`debug` or `trace`", default="info")
665
591
  parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
@@ -708,7 +634,10 @@ def main():
708
634
  args.verbose and (print(f"Alternate ref archive: {args.alternate_reference_archive}"))
709
635
  args.verbose and (print(f"Backup.d dir: {config_settings.backup_d_dir}"))
710
636
  args.verbose and (print(f"Backup dir: {config_settings.backup_dir}"))
711
- args.verbose and (print(f"Test restore dir: {config_settings.test_restore_dir}"))
637
+
638
+ restore_dir = args.restore_dir if args.restore_dir else config_settings.test_restore_dir
639
+ args.verbose and (print(f"Restore dir: {restore_dir}"))
640
+
712
641
  args.verbose and (print(f"Logfile location: {config_settings.logfile_location}"))
713
642
  args.verbose and (print(f".darrc location: {args.darrc}"))
714
643
  args.verbose and (print(f"PAR2 enabled: {config_settings.par2_enabled}"))
@@ -735,10 +664,9 @@ def main():
735
664
  elif args.incremental_backup and not args.full_backup and not args.differential_backup:
736
665
  perform_backup(args, config_settings, "INCR")
737
666
  elif args.list_contents:
738
- print(f"Listing contents of {args.list_contents}")
739
667
  list_contents(args.list_contents, config_settings.backup_dir, args.selection)
740
668
  elif args.restore:
741
- restore_dir = args.restore_dir if args.restore_dir else config_settings.test_restore_dir
669
+ logger.debug(f"Restoring {args.restore} to {restore_dir}")
742
670
  restore_backup(args.restore, config_settings, restore_dir, args.selection)
743
671
  else:
744
672
  parser.print_help()
dar_backup/manager.py CHANGED
@@ -21,8 +21,9 @@
21
21
  """
22
22
 
23
23
 
24
- import os
25
24
  import argparse
25
+ import os
26
+ import re
26
27
  import sys
27
28
 
28
29
 
@@ -30,7 +31,9 @@ from . import __about__ as about
30
31
  from dar_backup.config_settings import ConfigSettings
31
32
  from dar_backup.util import run_command
32
33
  from dar_backup.util import setup_logging
34
+ from datetime import datetime
33
35
  from time import time
36
+ from typing import Dict, List, NamedTuple
34
37
 
35
38
  # Constants
36
39
  SCRIPTNAME = os.path.basename(__file__)
@@ -70,9 +73,20 @@ def create_db(backup_def: str, config_settings: ConfigSettings):
70
73
  logger.error(f"stderr: {stderr}")
71
74
  logger.error(f"stdout: {stdout}")
72
75
 
76
+ return process.returncode
73
77
 
74
78
 
75
- def list_db(backup_def: str, config_settings: ConfigSettings):
79
+ def list_catalogs(backup_def: str, config_settings: ConfigSettings) -> NamedTuple:
80
+ """
81
+ Returns:
82
+ a typing.NamedTuple of class dar-backup.util.CommandResult with the following properties:
83
+ - process: of type subprocess.CompletedProcess: The result of the command execution.
84
+ - stdout: of type str: The standard output of the command.
85
+ - stderr: of type str: The standard error of the command.
86
+ - returncode: of type int: The return code of the command.
87
+ - timeout: of type int: The timeout value in seconds used to run the command.
88
+ - command: of type list[str): The command executed.
89
+ """
76
90
  database = f"{backup_def}{DB_SUFFIX}"
77
91
  database_path = os.path.join(config_settings.backup_dir, database)
78
92
  if not os.path.exists(database_path):
@@ -87,27 +101,101 @@ def list_db(backup_def: str, config_settings: ConfigSettings):
87
101
  logger.error(f"stdout: {stdout}")
88
102
  else:
89
103
  print(stdout)
90
- sys.exit(process.returncode)
104
+ return process
105
+
106
+
107
+ def cat_no_for_name(archive: str, config_settings: ConfigSettings) -> int:
108
+ """
109
+ Find the catalog number for the given archive name
110
+
111
+ Returns:
112
+ - the found number, if the archive catalog is present in the database
113
+ - "-1" if the archive is not found
114
+ """
115
+ backup_def = backup_def_from_archive(archive)
116
+ process = list_catalogs(backup_def, config_settings)
117
+ if process.returncode != 0:
118
+ logger.error(f"Error listing catalogs for backup def: '{backup_def}'")
119
+ return -1
120
+ line_no = 1
121
+ for line in process.stdout.splitlines():
122
+ #print(f"{line_no}: {line}")
123
+ line_no += 1
124
+ search = re.search(f"\s+(\d+)\s+.*?({archive}).*", line)
125
+ if search:
126
+ #print("FOUND")
127
+ logger.info(f"Found archive: '{archive}', catalog #: '{search.group(1)}'")
128
+ return int(search.group(1))
129
+ return -1
130
+
131
+
132
+
133
+
134
+
135
+ def list_catalog_contents(catalog_number: int, backup_def: str, config_settings: ConfigSettings):
136
+ """
137
+ List the contents of catalog # in catalog database for given backup definition
138
+ """
139
+ database = f"{backup_def}{DB_SUFFIX}"
140
+ database_path = os.path.join(config_settings.backup_dir, database)
141
+ if not os.path.exists(database_path):
142
+ logger.error(f'Database not found: "{database_path}"')
143
+ return 1
144
+ command = ['dar_manager', '--base', database_path, '-u', f"{catalog_number}"]
145
+ process = run_command(command)
146
+ stdout, stderr = process.stdout, process.stderr
147
+ if process.returncode != 0:
148
+ logger.error(f'Error listing catalogs for: "{database_path}"')
149
+ logger.error(f"stderr: {stderr}")
150
+ logger.error(f"stdout: {stdout}")
151
+ else:
152
+ print(stdout)
153
+ return process.returncode
154
+
155
+
156
+ def find_file(file, backup_def, config_settings):
157
+ """
158
+ Find a specific file
159
+ """
160
+ database = f"{backup_def}{DB_SUFFIX}"
161
+ database_path = os.path.join(config_settings.backup_dir, database)
162
+ if not os.path.exists(database_path):
163
+ logger.error(f'Database not found: "{database_path}"')
164
+ return 1
165
+ command = ['dar_manager', '--base', database_path, '-f', f"{file}"]
166
+ process = run_command(command)
167
+ stdout, stderr = process.stdout, process.stderr
168
+ if process.returncode != 0:
169
+ logger.error(f'Error finding file: {file} in: "{database_path}"')
170
+ logger.error(f"stderr: {stderr}")
171
+ logger.error(f"stdout: {stdout}")
172
+ else:
173
+ print(stdout)
174
+ return process.returncode
91
175
 
92
176
 
93
- def add_specific_archive(archive: str, config_settings: ConfigSettings):
177
+ def add_specific_archive(archive: str, config_settings: ConfigSettings, directory: str =None) -> int:
94
178
  # sanity check - does dar backup exist?
179
+ if not directory:
180
+ directory = config_settings.backup_dir
95
181
  archive = os.path.basename(archive) # remove path if it was given
96
- archive_path = os.path.join(config_settings.backup_dir, f'{archive}.1.dar')
97
- if not os.path.exists(archive_path):
98
- logger.error(f'dar backup: "{archive_path}" not found, exiting')
99
- sys.exit(1)
182
+ archive_path = os.path.join(directory, f'{archive}')
183
+
184
+ archive_test_path = os.path.join(directory, f'{archive}.1.dar')
185
+ if not os.path.exists(archive_test_path):
186
+ logger.error(f'dar backup: "{archive_test_path}" not found, exiting')
187
+ return 1
100
188
 
101
189
  # sanity check - does backup definition exist?
102
190
  backup_definition = archive.split('_')[0]
103
191
  backup_def_path = os.path.join(config_settings.backup_d_dir, backup_definition)
104
192
  if not os.path.exists(backup_def_path):
105
193
  logger.error(f'backup definition "{backup_definition}" not found (--add-specific-archive option probably not correct), exiting')
106
- sys.exit(1)
194
+ return 1
107
195
 
108
196
  database = f"{backup_definition}{DB_SUFFIX}"
109
197
  database_path = os.path.realpath(os.path.join(config_settings.backup_dir, database))
110
- logger.info(f'Add "{archive_path}" to catalog "{database}"')
198
+ logger.info(f'Add "{archive_path}" to catalog: "{database}"')
111
199
 
112
200
  command = ['dar_manager', '--base', database_path, "--add", archive_path, "-ai", "-Q"]
113
201
  process = run_command(command)
@@ -122,7 +210,99 @@ def add_specific_archive(archive: str, config_settings: ConfigSettings):
122
210
  logger.error(f"stderr: {stderr}")
123
211
  logger.error(f"stdout: {stdout}")
124
212
 
125
- sys.exit(process.returncode)
213
+ return process.returncode
214
+
215
+
216
+
217
+ def add_directory(args: argparse.ArgumentParser, config_settings: ConfigSettings) -> None:
218
+ """
219
+ Loop over the DAR archives in the given directory args.add_dir in increasing order by date and add them to their catalog database.
220
+
221
+ Args:
222
+ args (argparse.ArgumentParser): The command-line arguments object containing the add_dir attribute.
223
+ config_settings (ConfigSettings): The configuration settings object.
224
+
225
+ This function performs the following steps:
226
+ 1. Checks if the specified directory exists. If not, raises a RuntimeError.
227
+ 2. Uses a regular expression to match DAR archive files with base names in the format <string>_{FULL, DIFF, INCR}_YYYY-MM-DD.
228
+ 3. Lists the DAR archives in the specified directory and extracts their base names and dates.
229
+ 4. Sorts the DAR archives by date.
230
+ 5. Loops over the sorted DAR archives and adds each archive to its catalog database using the add_specific_archive function.
231
+
232
+ Example:
233
+ args = argparse.ArgumentParser()
234
+ args.add_dir = '/path/to/dar/archives'
235
+ config_settings = ConfigSettings()
236
+ add_directory(args, config_settings)
237
+ """
238
+ if not os.path.exists(args.add_dir):
239
+ raise RuntimeError(f"Directory {args.add_dir} does not exist")
240
+
241
+ # Regular expression to match DAR archive files with base name and date in the format <string>_{FULL, DIFF, INCR}_YYYY-MM-DD
242
+ #dar_pattern = re.compile(r'^(.*?_(FULL|DIFF|INCR)_(\d{4}-\d{2}-\d{2}))\.\d+\.dar$')
243
+ dar_pattern = re.compile(r'^(.*?_(FULL|DIFF|INCR)_(\d{4}-\d{2}-\d{2}))\.1.dar$') # just read slice #1 of an archive
244
+ # List of DAR archives with their dates and base names
245
+ dar_archives = []
246
+
247
+ for filename in os.listdir(args.add_dir):
248
+ logger.debug(f"check if '{filename}' is a dar archive slice #1?")
249
+ match = dar_pattern.match(filename)
250
+ if match:
251
+ base_name = match.group(1)
252
+ date_str = match.group(3)
253
+ date_obj = datetime.strptime(date_str, '%Y-%m-%d')
254
+ dar_archives.append((date_obj, base_name))
255
+ logger.debug(f" -> yes: base name: {base_name}, date: {date_str}")
256
+
257
+ if not dar_archives or len(dar_archives) == 0:
258
+ logger.info(f"No 'dar' archives found in directory {args.add_dir}")
259
+ return
260
+
261
+ # Sort the DAR archives by date
262
+ dar_archives.sort()
263
+
264
+ # Loop over the sorted DAR archives and process them
265
+ result: List[Dict] = []
266
+ for date_obj, base_name in dar_archives:
267
+ logger.info(f"Adding dar archive: '{base_name}' to it's catalog database")
268
+ result_archive = add_specific_archive(base_name, config_settings, args.add_dir)
269
+ result.append({ f"{base_name}" : result_archive})
270
+ if result_archive != 0:
271
+ logger.error(f"Something went wrong added {base_name} to it's catalog")
272
+
273
+ logger.debug(f"Results adding archives found in: '{args.add_dir}': result")
274
+
275
+
276
+ def backup_def_from_archive(archive: str) -> str:
277
+ """
278
+ return the backup definition from archive name
279
+ """
280
+ search = re.search("(.*?)_", archive)
281
+ backup_def = search.group(1)
282
+ logger.debug(f"backup definition: '{backup_def}' from given archive '{archive}'")
283
+ return backup_def
284
+
285
+
286
+
287
+ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> int:
288
+ backup_def = backup_def_from_archive(archive)
289
+ database_path = os.path.join(config_settings.backup_dir, f"{backup_def}{DB_SUFFIX}")
290
+ cat_no = cat_no_for_name(archive, config_settings)
291
+ if cat_no > 0:
292
+ command = ['dar_manager', '--base', database_path, "--delete", str(cat_no)]
293
+ process = run_command(command)
294
+ else:
295
+ logger.error(f"archive: '{archive}' not found in in't catalog database: {database_path}")
296
+ return cat_no
297
+
298
+ if process.returncode == 0:
299
+ logger.info(f"'{archive}' removed from it's catalog")
300
+ else:
301
+ logger.error(process.stdout)
302
+ logger.error(process.sterr)
303
+
304
+ return process.returncode
305
+
126
306
 
127
307
 
128
308
 
@@ -142,7 +322,9 @@ def main():
142
322
  parser.add_argument('-d', '--backup-def', type=str, help='Restrict to work only on this backup definition')
143
323
  parser.add_argument('--add-specific-archive', type=str, help='Add this archive to catalog database')
144
324
  parser.add_argument('--remove-specific-archive', type=str, help='Remove this archive from catalog database')
145
- parser.add_argument('--list-db', action='store_true', help='List catalogs in databases')
325
+ parser.add_argument('--list-catalog', action='store_true', help='List catalogs in databases for all backup definitions')
326
+ parser.add_argument('--list-catalog-contents', type=int, help="List contents of a catalog. Argument is the 'archive #', '-d <definition>' argument is also required")
327
+ parser.add_argument('--find-file', type=str, help="List catalogs containing <path>/file. '-d <definition>' argument is also required")
146
328
  parser.add_argument('--verbose', action='store_true', help='Be more verbose')
147
329
  parser.add_argument('--log-level', type=str, help="`debug` or `trace`, default is `info`", default="info")
148
330
  parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
@@ -196,6 +378,13 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
196
378
  logger.error("you can't add and remove archives in the same operation, exiting")
197
379
  sys.exit(1)
198
380
 
381
+ if args.add_dir and args.add_specific_archive:
382
+ logger.error("you cannot add both a directory and an archive")
383
+ sys.exit(1)
384
+
385
+ if args.backup_def and not args.backup_def.strip():
386
+ logger.error(f"No backup definition given to --backup-def")
387
+
199
388
  if args.backup_def:
200
389
  backup_def_path = os.path.join(config_settings.backup_d_dir, args.backup_def)
201
390
  if not os.path.exists(backup_def_path):
@@ -203,6 +392,16 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
203
392
  sys.exit(1)
204
393
 
205
394
 
395
+ if args.list_catalog_contents and not args.backup_def:
396
+ logger.error(f"--list-catalog-contents requires the --backup-def, exiting")
397
+ sys.exit(1)
398
+
399
+ if args.find_file and not args.backup_def:
400
+ logger.error(f"--find-file requires the --backup-def, exiting")
401
+ sys.exit(1)
402
+
403
+
404
+
206
405
  # Modify config settings based on the arguments
207
406
  if args.alternate_archive_dir:
208
407
  if not os.path.exists(args.alternate_archive_dir):
@@ -213,35 +412,49 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
213
412
 
214
413
  if args.create_db:
215
414
  if args.backup_def:
216
- create_db(args.backup_def, config_settings)
415
+ sys.exit(create_db(args.backup_def, config_settings))
217
416
  else:
218
417
  for root, dirs, files in os.walk(config_settings.backup_d_dir):
219
418
  for file in files:
220
419
  current_backupdef = os.path.basename(file)
221
- create_db(current_backupdef, config_settings)
222
- sys.exit(0)
420
+ logger.debug(f"Create catalog db for backup definition: '{current_backupdef}'")
421
+ result = create_db(current_backupdef, config_settings)
422
+ if result != 0:
423
+ sys.exit(result)
223
424
 
224
425
  if args.add_specific_archive:
225
- add_specific_archive(args.add_specific_archive, config_settings)
426
+ sys.exit(add_specific_archive(args.add_specific_archive, config_settings))
226
427
 
227
428
  if args.add_dir:
228
- # Implement add directory logic
229
- pass
429
+ sys.exit(add_directory(args, config_settings))
430
+
230
431
 
231
432
  if args.remove_specific_archive:
232
- # Implement remove specific archive logic
233
- pass
433
+ sys.exit(remove_specific_archive(args.remove_specific_archive, config_settings))
434
+
234
435
 
235
436
 
236
- if args.list_db:
437
+ if args.list_catalog:
237
438
  if args.backup_def:
238
- list_db(args.backup_def, config_settings)
439
+ process = list_catalogs(args.backup_def, config_settings)
440
+ result = process.returncode
239
441
  else:
442
+ result = 0
240
443
  for root, dirs, files in os.walk(config_settings.backup_d_dir):
241
444
  for file in files:
242
445
  current_backupdef = os.path.basename(file)
243
- list_db(current_backupdef, config_settings)
244
- sys.exit(0)
446
+ if list_catalogs(current_backupdef, config_settings).returncode != 0:
447
+ result = 1
448
+ sys.exit(result)
449
+
450
+ if args.list_catalog_contents:
451
+ result = list_catalog_contents(args.list_catalog_contents, args.backup_def, config_settings)
452
+ sys.exit(result)
453
+
454
+ if args.find_file:
455
+ result = find_file(args.find_file, args.backup_def, config_settings)
456
+ sys.exit(result)
457
+
245
458
 
246
459
  if __name__ == "__main__":
247
460
  main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dar-backup
3
- Version: 0.6.1
3
+ Version: 0.6.3
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
@@ -0,0 +1,13 @@
1
+ dar_backup/.darrc,sha256=ggex9N6eETOS6u003_QRRJMeWbveQfkT1lDBt0XpU-I,2112
2
+ dar_backup/__about__.py,sha256=_SsQ0ZcyZbUqlFFT370nQxs8UER9D0oW_EmCr4Q-hx4,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=YZoVu9NJX3_WIkQIG8EMLSK3-VWdslI0c2XKrM2Un38,32214
7
+ dar_backup/manager.py,sha256=Y1dQ7CRGQx5sGoGrjAO62QfJcuW_0Vod-ZGaQmHInFU,19062
8
+ dar_backup/util.py,sha256=6lPCFHr3MDdaLWAW9EDMZ4jdL7pt8rki-5dOXcesmP8,8955
9
+ dar_backup-0.6.3.dist-info/METADATA,sha256=6120hSt2mUxhn3u8PfbJweoPY4ToWFlAPHAgLozaK7Q,22496
10
+ dar_backup-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ dar_backup-0.6.3.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
12
+ dar_backup-0.6.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
+ dar_backup-0.6.3.dist-info/RECORD,,
@@ -1,13 +0,0 @@
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,,