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