dar-backup 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dar_backup/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.6.0"
1
+ __version__ = "0.6.2"
dar_backup/dar_backup.py CHANGED
@@ -17,181 +17,60 @@ 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
- stdout, stderr = process.stdout, process.stderr
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:
73
- logger.error("dar return code: ", process.returncode)
74
- logger.error("stdout: ", stdout)
75
- logger.error("stderr: ", stderr)
76
- raise Exception(stderr)
66
+ raise Exception(str(process))
77
67
  except subprocess.CalledProcessError as e:
78
68
  logger.error(f"Backup command failed: {e}")
79
69
  raise BackupError(f"Backup command failed: {e}") from e
80
70
  except Exception as e:
81
71
  logger.exception(f"Unexpected error during backup")
82
- logger.error("stderr:", stderr)
83
- logger.error("stdout: ", stdout)
84
72
  raise BackupError(f"Unexpected error during backup: {e}") from e
85
73
 
86
-
87
-
88
-
89
-
90
- def differential_backup(backup_file: str, backup_definition: str, base_backup_file: str, darrc: str, config_settings: ConfigSettings):
91
- """
92
- Creates a differential backup based on a specified base backup.
93
-
94
- This function performs a differential backup by comparing the current state of the data
95
- against a specified base backup file. It captures only the changes made since that base
96
- backup, resulting in a smaller and faster backup process compared to a full backup.
97
-
98
- Args:
99
- backup_file (str): The base name for the differential backup file. The actual backup
100
- will be saved as '{backup_file}.1.dar'.
101
- backup_definition (str): The path to the backup definition file. This file contains
102
- specific instructions for the 'dar' utility, such as which
103
- directories to include or exclude.
104
- base_backup_file (str): The base name of the full backup file that serves as the
105
- reference point for the differential backup.
106
-
107
- Note:
108
- This function logs an error and returns early if the differential backup file already exists.
109
- It logs the command being executed and reports upon successful completion of the differential backup.
110
-
111
- Raises:
112
- DifferentialBackupError: If the differential backup command fails or encounters an unexpected error.
113
- """
114
- if os.path.exists(backup_file + '.1.dar'):
115
- logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
116
- return
117
-
118
- logger.info(f"===> Starting DIFF backup for {backup_definition}")
119
- command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', base_backup_file, '-Q', "compress-exclusion", "verbose"]
120
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
121
- try:
122
- process = run_command(command)
123
- stdout, stderr = process.stdout, process.stderr
124
- if process.returncode == 0:
125
- logger.info("DIFF backup completed successfully.")
126
- elif process.returncode == 5:
127
- logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
128
- else:
129
- logger.error("dar return code: ", process.returncode)
130
- logger.error("stdout: ", stdout)
131
- logger.error("stderr: ", stderr)
132
- raise Exception(stderr)
133
- except subprocess.CalledProcessError as e:
134
- logger.error(f"Differential backup command failed: {e}")
135
- raise DifferentialBackupError(f"Differential backup command failed: {e}") from e
136
- except Exception as e:
137
- logger.exception(f"Unexpected error during differential backup")
138
- logger.error("Exception details:", exc_info=True)
139
- raise DifferentialBackupError(f"Unexpected error during differential backup: {e}") from e
140
-
141
-
142
- def incremental_backup(backup_file: str, backup_definition: str, last_backup_file: str, darrc: str, config_settings: ConfigSettings):
143
- """
144
- Creates an incremental backup based on the last backup file.
145
-
146
- This function performs an incremental backup by comparing the current state of the data
147
- against the last backup file, whether it's a full backup or the most recent incremental backup.
148
- It captures only the changes made since that last backup, making it efficient for frequent
149
- backups with minimal data changes.
150
-
151
- Args:
152
- backup_file (str): The base name for the incremental backup file. The actual backup
153
- will be saved with a unique identifier to distinguish it from other backups.
154
- backup_definition (str): The path to the backup definition file. This file contains
155
- specific instructions for the 'dar' utility, such as which
156
- directories to include or exclude.
157
- last_backup_file (str): The base name of the last backup file (full or incremental) that
158
- serves as the reference point for the incremental backup.
159
-
160
- Note:
161
- This function checks if the incremental backup file already exists to prevent overwriting
162
- previous backups. It logs the command being executed and reports upon successful completion
163
- of the incremental backup.
164
-
165
- Raises:
166
- IncrementalBackupError: If the incremental backup command fails or an unexpected error occurs.
167
- """
168
- if os.path.exists(backup_file + '.1.dar'):
169
- logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
170
- return
171
-
172
- logger.info(f"===> Starting INCR backup for {backup_definition}")
173
- command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', last_backup_file, '-Q', "compress-exclusion", "verbose"]
174
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
175
- try:
176
- process = run_command(command, config_settings.command_timeout_secs)
177
- stdout, stderr = process.stdout, process.stderr
178
- if process.returncode == 0:
179
- logger.info("INCR backup completed successfully.")
180
- elif process.returncode == 5:
181
- logger.warning("Backup completed with some files not backed up, this can happen if files are changed/deleted during the backup.")
182
- else:
183
- logger.error("dar return code: ", process.returncode)
184
- logger.error("stdout: ", stdout)
185
- logger.error("stderr: ", stderr)
186
- raise Exception(stderr)
187
- except subprocess.CalledProcessError as e:
188
- logger.error(f"Incremental backup command failed: {e}")
189
- raise IncrementalBackupError(f"Incremental backup command failed: {e}") from e
190
- except Exception as e:
191
- logger.exception(f"Unexpected error during incremental backup")
192
- logger.error("Exception details:", exc_info=True)
193
- raise IncrementalBackupError(f"Unexpected error during incremental backup: {e}") from e
194
-
195
74
 
196
75
  # Function to recursively find <File> tags and build their full paths
197
76
  def find_files_with_paths(element: ET, current_path=""):
@@ -286,11 +165,10 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
286
165
  command = ['dar', '-t', backup_file, '-Q']
287
166
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
288
167
  process = run_command(command, config_settings.command_timeout_secs)
289
- stdout, stderr = process.stdout, process.stderr
290
168
  if process.returncode == 0:
291
169
  logger.info("Archive integrity test passed.")
292
170
  else:
293
- raise Exception(stderr)
171
+ raise Exception(str(process))
294
172
 
295
173
  if args.do_not_compare:
296
174
  return result
@@ -325,10 +203,8 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
325
203
  command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-O', '-Q']
326
204
  logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
327
205
  process = run_command(command, config_settings.command_timeout_secs)
328
- stdout, stderr = process.stdout, process.stderr
329
206
  if process.returncode != 0:
330
- logger.error(f"Restore failed, dar return code: {process.returncode}.")
331
- raise Exception(stderr)
207
+ raise Exception(str(process))
332
208
 
333
209
  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
210
  logger.info(f"Success: file '{restored_file_path}' matches the original")
@@ -348,7 +224,7 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
348
224
  Restores a backup file to a specified directory.
349
225
 
350
226
  Args:
351
- 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"
352
228
  backup_dir (str): The directory where the backup file is located.
353
229
  restore_dir (str): The directory where the backup should be restored to.
354
230
  selection (str, optional): A selection criteria to restore specific files or directories. Defaults to None.
@@ -359,23 +235,22 @@ def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_di
359
235
  if not os.path.exists(restore_dir):
360
236
  os.makedirs(restore_dir)
361
237
  command.extend(['-R', restore_dir])
238
+ else:
239
+ raise RestoreError("Restore directory ('-R <dir>') not specified")
362
240
  if selection:
363
241
  selection_criteria = shlex.split(selection)
364
242
  command.extend(selection_criteria)
365
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
243
+ logger.info(f"Running restore command: {' '.join(map(shlex.quote, command))}")
366
244
  try:
367
245
  process = run_command(command, config_settings.command_timeout_secs)
368
- stdout, stderr = process.stdout, process.stderr
246
+ if process.returncode == 0:
247
+ logger.info(f"Restore completed successfully to: '{restore_dir}'")
248
+ else:
249
+ logger.error(f"Restore command failed: \n ==> stdout: {process.stdout}, \n ==> stderr: {process.stderr}")
250
+ raise RestoreError(str(process))
369
251
  except subprocess.CalledProcessError as e:
370
- logger.error("Exception details:", exc_info=True)
371
- logger.error(f"stdout: {stdout}")
372
- logger.error(f"stderr: {stderr}")
373
252
  raise RestoreError(f"Restore command failed: {e}") from e
374
253
  except Exception as e:
375
- logger.exception(f"Unexpected error during restore")
376
- logger.error("Exception details:", exc_info=True)
377
- logger.error(f"stdout: {stdout}")
378
- logger.error(f"stderr: {stderr}")
379
254
  raise RestoreError(f"Unexpected error during restore: {e}") from e
380
255
 
381
256
 
@@ -393,19 +268,24 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
393
268
  """
394
269
  logger.debug(f"Getting backed up files from DAR archive in xml: '{backup_name}'")
395
270
  backup_path = os.path.join(backup_dir, backup_name)
396
- command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
397
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
398
- process = run_command(command)
399
- stdout,stderr = process.stdout, process.stderr
400
- # Parse the XML data
401
- root = ET.fromstring(stdout)
402
- output = None # help gc
403
- # Extract full paths and file size for all <File> elements
404
- file_paths = find_files_with_paths(root)
405
- root = None # help gc
406
- logger.trace(f"Backed up files in dar archive: '{backup_name}'")
407
- logger.trace(file_paths)
408
- return file_paths
271
+ try:
272
+ command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
273
+ logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
274
+ process = run_command(command)
275
+ # Parse the XML data
276
+ root = ET.fromstring(process.stdout)
277
+ output = None # help gc
278
+ # Extract full paths and file size for all <File> elements
279
+ file_paths = find_files_with_paths(root)
280
+ root = None # help gc
281
+ logger.trace(str(process))
282
+ logger.trace(file_paths)
283
+ return file_paths
284
+ except subprocess.CalledProcessError as e:
285
+ logger.error(f"Error listing backed up files from DAR archive: '{backup_name}'")
286
+ raise BackupError(f"Error listing backed up files from DAR archive: '{backup_name}'") from e
287
+ except Exception as e:
288
+ raise RuntimeError(f"Unexpected error listing backed up files from DAR archive: '{backup_name}'") from e
409
289
 
410
290
 
411
291
  def list_contents(backup_name, backup_dir, selection=None):
@@ -421,17 +301,53 @@ def list_contents(backup_name, backup_dir, selection=None):
421
301
  None
422
302
  """
423
303
  backup_path = os.path.join(backup_dir, backup_name)
424
- command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
425
- if selection:
426
- selection_criteria = shlex.split(selection)
427
- command.extend(selection_criteria)
428
- logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
429
- process = run_command(command)
430
- stdout,stderr = process.stdout, process.stderr
431
304
 
432
- for line in stdout.splitlines():
433
- if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
434
- print(line)
305
+ try:
306
+ command = ['dar', '-l', backup_path, '-am', '-as', '-Q']
307
+ if selection:
308
+ selection_criteria = shlex.split(selection)
309
+ command.extend(selection_criteria)
310
+ logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
311
+ process = run_command(command)
312
+ stdout,stderr = process.stdout, process.stderr
313
+ if process.returncode != 0:
314
+ logger.error(f"Error listing contents of backup: '{backup_name}'")
315
+ raise subprocess.CalledProcessError(str(process))
316
+ for line in stdout.splitlines():
317
+ if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
318
+ print(line)
319
+ except subprocess.CalledProcessError as e:
320
+ logger.error(f"Error listing contents of backup: '{backup_name}'")
321
+ raise BackupError(f"Error listing contents of backup: '{backup_name}'") from e
322
+ except Exception as e:
323
+ raise RuntimeError(f"Unexpected error listing contents of backup: '{backup_name}'") from e
324
+
325
+
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
+
435
351
 
436
352
 
437
353
  def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, backup_type: str):
@@ -441,32 +357,12 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
441
357
  Args:
442
358
  args: Command-line arguments.
443
359
  config_settings: An instance of the ConfigSettings class.
444
- backup_d: Directory containing backup definitions.
445
- backup_dir: Directory to store backup files.
446
- test_restore_dir: Directory to test restore backup files.
447
360
  backup_type: Type of backup (FULL, DIFF, INCR).
448
- min_size_verification_mb: Minimum size for verification in MB.
449
- max_size_verification_mb: Maximum size for verification in MB.
450
- no_files_verification: Flag indicating whether to skip file verification.
451
-
452
- Returns:
453
- None
454
-
455
- Raises:
456
- FileNotFoundError: If `backup_d` does not exist or a specified backup definition file does not exist.
457
- PermissionError: If there is insufficient permission to access directories or files specified.
458
- OSError: For various system-related errors, such as exhaustion of file descriptors.
459
- ValueError: If there is an issue with the format string in `datetime.now().strftime`.
460
- subprocess.CalledProcessError: If a subprocess invoked during the backup process exits with a non-zero status.
461
- Exception: Catches any unexpected exceptions that may occur during the backup process.
462
-
463
- Note:
464
- This function assumes that any exceptions raised by the `backup` function or related subprocesses are handled
465
- within those functions or propagated up to be handled by the caller of `perform_backup`.
466
361
  """
467
362
  logger.debug(f"perform_backup({backup_type}) started")
468
363
  backup_definitions = []
469
364
 
365
+ # Gather backup definitions
470
366
  if args.backup_definition:
471
367
  if '_' in args.backup_definition:
472
368
  logger.error(f"Skipping backup definition: '{args.backup_definition}' due to '_' in name")
@@ -489,16 +385,15 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
489
385
  logger.error(f"Backup file {backup_file}.1.dar already exists. Skipping backup.")
490
386
  continue
491
387
 
492
- if backup_type == 'FULL':
493
- backup(backup_file, backup_definition_path, args.darrc, config_settings)
494
- else:
388
+ latest_base_backup = None
389
+ if backup_type in ['DIFF', 'INCR']:
495
390
  base_backup_type = 'FULL' if backup_type == 'DIFF' else 'DIFF'
496
-
391
+
497
392
  if args.alternate_reference_archive:
498
- 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)
499
394
  logger.info(f"Using alternate reference archive: {latest_base_backup}")
500
395
  if not os.path.exists(latest_base_backup + '.1.dar'):
501
- 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.")
502
397
  sys.exit(1)
503
398
  else:
504
399
  base_backups = sorted(
@@ -510,33 +405,36 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
510
405
  continue
511
406
  latest_base_backup = os.path.join(config_settings.backup_dir, base_backups[-1].rsplit('.', 2)[0])
512
407
 
513
- if backup_type == 'DIFF':
514
- differential_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc, config_settings)
515
- elif backup_type == 'INCR':
516
- 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)
517
413
 
518
414
  logger.info("Starting verification...")
519
415
  result = verify(args, backup_file, backup_definition_path, config_settings)
520
416
  if result:
521
417
  logger.info("Verification completed successfully.")
522
418
  else:
523
- logger.error("Verification failed.")
419
+ logger.error("Verification failed.")
420
+
524
421
  if config_settings.par2_enabled:
525
- logger.info("Generate par2 redundancy files")
526
- 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)
527
424
  logger.info("par2 files completed successfully.")
528
- # we want to continue with other backup definitions, thus only logging an error
529
425
  except Exception as e:
530
- logger.exception(f"Error during {backup_type} backup process, continuing on next backup definition")
531
- logger.error("Exception details:", exc_info=True)
426
+ logger.exception(f"Error during {backup_type} backup process, continuing to next backup definition.")
427
+
428
+
532
429
 
533
- def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
430
+ def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args):
534
431
  """
535
432
  Generate PAR2 files for a given backup file in the specified backup directory.
536
433
 
537
434
  Args:
538
435
  backup_file (str): The name of the backup file.
539
- backup_dir (str): The path to the backup directory.
436
+ config_settings: The configuration settings object.
437
+ args: The command-line arguments object.
540
438
 
541
439
  Raises:
542
440
  subprocess.CalledProcessError: If the par2 command fails to execute.
@@ -544,19 +442,39 @@ def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
544
442
  Returns:
545
443
  None
546
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
+
547
451
  for filename in os.listdir(config_settings.backup_dir):
548
- if os.path.basename(backup_file) in filename:
549
- # Construct the full path to the file
550
- file_path = os.path.join(config_settings.backup_dir, filename)
551
- # Run the par2 command to generate redundancy files with error correction
552
- command = ['par2', 'create', f'-r{config_settings.error_correction_percent}', '-q', '-q', file_path]
553
- process = run_command(command, config_settings.command_timeout_secs)
554
- if process.returncode != 0:
555
- logger.error(f"Error generating par2 files for {file_path}")
556
- raise subprocess.CalledProcessError(process.returncode, command)
557
- logger.debug(f"par2 files generated for {file_path}")
452
+ match = dar_slice_pattern.match(filename)
453
+ if match:
454
+ dar_slices.append(filename)
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
558
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)
559
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
560
478
 
561
479
 
562
480
 
@@ -634,7 +552,7 @@ def requirements(type: str, config_setting: ConfigSettings):
634
552
  for key in sorted(config_setting.config[type].keys()):
635
553
  script = config_setting.config[type][key]
636
554
  try:
637
- result = subprocess.run(script, shell=True, check=True)
555
+ result = subprocess.run(script, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=True)
638
556
  logger.info(f"{type} {key}: '{script}' run, return code: {result.returncode}")
639
557
  logger.info(f"{type} stdout:\n{result.stdout}")
640
558
  if result.returncode != 0:
@@ -642,9 +560,7 @@ def requirements(type: str, config_setting: ConfigSettings):
642
560
  raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
643
561
  except subprocess.CalledProcessError as e:
644
562
  logger.error(f"Error executing {key}: '{script}': {e}")
645
- if result:
646
- logger.error(f"{type} stderr:\n{result.stderr}")
647
- raise e
563
+ raise e
648
564
 
649
565
 
650
566
  def main():
@@ -667,8 +583,9 @@ def main():
667
583
  parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
668
584
  parser.add_argument('--list-contents', help="List the contents of the specified archive.")
669
585
  parser.add_argument('--selection', help="dar file selection for listing/restoring specific files/directories.")
670
- parser.add_argument('-r', '--restore', nargs=1, type=str, help="Restore specified archive.")
671
- 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.")
672
589
  parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
673
590
  parser.add_argument('--log-level', type=str, help="`debug` or `trace`", default="info")
674
591
  parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
@@ -717,7 +634,10 @@ def main():
717
634
  args.verbose and (print(f"Alternate ref archive: {args.alternate_reference_archive}"))
718
635
  args.verbose and (print(f"Backup.d dir: {config_settings.backup_d_dir}"))
719
636
  args.verbose and (print(f"Backup dir: {config_settings.backup_dir}"))
720
- 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
+
721
641
  args.verbose and (print(f"Logfile location: {config_settings.logfile_location}"))
722
642
  args.verbose and (print(f".darrc location: {args.darrc}"))
723
643
  args.verbose and (print(f"PAR2 enabled: {config_settings.par2_enabled}"))
@@ -744,33 +664,27 @@ def main():
744
664
  elif args.incremental_backup and not args.full_backup and not args.differential_backup:
745
665
  perform_backup(args, config_settings, "INCR")
746
666
  elif args.list_contents:
747
- print(f"Listing contents of {args.list_contents}")
748
667
  list_contents(args.list_contents, config_settings.backup_dir, args.selection)
749
668
  elif args.restore:
750
- 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}")
751
670
  restore_backup(args.restore, config_settings, restore_dir, args.selection)
752
671
  else:
753
672
  parser.print_help()
754
673
 
755
674
  requirements('POSTREQ', config_settings)
756
675
 
757
-
676
+ args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
677
+ sys.exit(0)
758
678
  except Exception as e:
759
679
  logger.exception("An error occurred")
760
680
  logger.error("Exception details:", exc_info=True)
761
-
762
- end_time=int(time())
763
- logger.info(f"END TIME: {end_time}")
764
-
765
- error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
766
- if len(error_lines) > 0:
767
681
  args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
768
- for line in error_lines:
769
- args.verbose and print(line)
770
- sys.exit(1)
771
- else:
772
- args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
773
- sys.exit(0)
682
+ sys.exit(1)
683
+ finally:
684
+ end_time=int(time())
685
+ logger.info(f"END TIME: {end_time}")
686
+
687
+ # error_lines = extract_error_lines(config_settings.logfile_location, start_time, end_time)
774
688
 
775
689
 
776
690
  if __name__ == "__main__":
dar_backup/util.py CHANGED
@@ -81,9 +81,6 @@ def setup_logging(log_file: str, log_level: str, log_to_stdout: bool=False, logg
81
81
  stdout_handler.setFormatter(formatter)
82
82
  logger.addHandler(stdout_handler)
83
83
 
84
- # logging.basicConfig(filename=log_file, level=level_used,
85
- # format='%(asctime)s - %(levelname)s - %(message)s')
86
-
87
84
  except Exception as e:
88
85
  print("logging not initialized, exiting.")
89
86
  traceback.print_exc()
@@ -103,6 +100,9 @@ class CommandResult(NamedTuple):
103
100
  timeout: int
104
101
  command: list[str]
105
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}']"
105
+
106
106
 
107
107
  def run_command(command: list[str], timeout: int=30) -> typing.NamedTuple:
108
108
  """
@@ -125,23 +125,22 @@ def run_command(command: list[str], timeout: int=30) -> typing.NamedTuple:
125
125
  subprocess.TimeoutExpired: if the command execution times out (see `timeout` parameter).
126
126
  Exception: raise exceptions during command runs.
127
127
  """
128
-
128
+ stdout = None
129
+ stderr = None
129
130
  try:
130
- logger.info(f"Running command: {command}")
131
+ logger.debug(f"Running command: {command}")
131
132
  process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
132
- stdout, stderr = process.communicate(timeout=30) # Wait with timeout
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)}")
133
136
  except subprocess.TimeoutExpired:
134
137
  process.terminate()
135
138
  logger.error(f"Command: '{command}' timed out and was terminated.")
139
+ raise
136
140
  except Exception as e:
137
141
  logger.error(f"Error running command: {command}", exc_info=True)
138
142
  raise
139
- finally:
140
- if not stdout:
141
- stdout = None
142
- if not stderr:
143
- stderr = None
144
- return CommandResult(process=process, stdout=stdout, stderr=stderr, returncode=process.returncode, timeout=timeout, command=command)
143
+ return result
145
144
 
146
145
 
147
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.6.0
3
+ Version: 0.6.2
4
4
  Summary: A script to do full, differential and incremental backups using dar. Some files are restored from the backups during verification, after which par2 redundancy files are created. The script also has a cleanup feature to remove old backups and par2 files.
5
5
  Project-URL: 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=d7NGuoje3vHyudKIFR_PmfKozIOKDFvAhGx0QXiyuMw,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=lkw1ZAIdxY7WedLPKZMnHpuq_QbjkUdcG61ooiwUYpo,10197
8
+ dar_backup/util.py,sha256=6lPCFHr3MDdaLWAW9EDMZ4jdL7pt8rki-5dOXcesmP8,8955
9
+ dar_backup-0.6.2.dist-info/METADATA,sha256=PMfP8NhPwzhHRbjJt6_7Unqxbqqj13PW19QdYac6pcc,22496
10
+ dar_backup-0.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ dar_backup-0.6.2.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
12
+ dar_backup-0.6.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
+ dar_backup-0.6.2.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- dar_backup/.darrc,sha256=ggex9N6eETOS6u003_QRRJMeWbveQfkT1lDBt0XpU-I,2112
2
- dar_backup/__about__.py,sha256=DS49q_bFynltwBtgneHYeVXTHLg5bjAOFWvTkv-jYmY,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=Kz_Gwe9t6vHGk590D3pbqDe1ptyrhLNpQWCU8oDcqos,37870
7
- dar_backup/manager.py,sha256=lkw1ZAIdxY7WedLPKZMnHpuq_QbjkUdcG61ooiwUYpo,10197
8
- dar_backup/util.py,sha256=ZYalRptXvSI9laLmHOe67WOOHoHF0CS4osE9NP1MDWA,8892
9
- dar_backup-0.6.0.dist-info/METADATA,sha256=-D3-rzNmkAMpN5SpMSeoveXuGQ1x8EncsdBxs0Lfx-s,22496
10
- dar_backup-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- dar_backup-0.6.0.dist-info/entry_points.txt,sha256=x9vnW-JEl8mpDJC69f_XBcn0mBSkV1U0cyvFV-NAP1g,126
12
- dar_backup-0.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
- dar_backup-0.6.0.dist-info/RECORD,,