dar-backup 0.5.17__py3-none-any.whl → 0.6.0__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 +37 -28
- dar_backup/manager.py +3 -3
- dar_backup/util.py +47 -12
- {dar_backup-0.5.17.dist-info → dar_backup-0.6.0.dist-info}/METADATA +49 -11
- dar_backup-0.6.0.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.0.dist-info}/WHEEL +0 -0
- {dar_backup-0.5.17.dist-info → dar_backup-0.6.0.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.5.17.dist-info → dar_backup-0.6.0.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.6.0"
|
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,8 +63,8 @@ def backup(backup_file: str, backup_definition: str, darrc: str):
|
|
|
63
63
|
command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-Q', "compress-exclusion", "verbose"]
|
|
64
64
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
65
65
|
try:
|
|
66
|
-
process = run_command(command)
|
|
67
|
-
stdout, stderr = process.
|
|
66
|
+
process = run_command(command, config_settings.command_timeout_secs)
|
|
67
|
+
stdout, stderr = process.stdout, process.stderr
|
|
68
68
|
if process.returncode == 0:
|
|
69
69
|
logger.info("FULL backup completed successfully.")
|
|
70
70
|
elif process.returncode == 5:
|
|
@@ -87,7 +87,7 @@ def backup(backup_file: str, backup_definition: str, darrc: str):
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def differential_backup(backup_file: str, backup_definition: str, base_backup_file: str, darrc: str):
|
|
90
|
+
def differential_backup(backup_file: str, backup_definition: str, base_backup_file: str, darrc: str, config_settings: ConfigSettings):
|
|
91
91
|
"""
|
|
92
92
|
Creates a differential backup based on a specified base backup.
|
|
93
93
|
|
|
@@ -120,7 +120,7 @@ def differential_backup(backup_file: str, backup_definition: str, base_backup_fi
|
|
|
120
120
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
121
121
|
try:
|
|
122
122
|
process = run_command(command)
|
|
123
|
-
stdout, stderr = process.
|
|
123
|
+
stdout, stderr = process.stdout, process.stderr
|
|
124
124
|
if process.returncode == 0:
|
|
125
125
|
logger.info("DIFF backup completed successfully.")
|
|
126
126
|
elif process.returncode == 5:
|
|
@@ -139,7 +139,7 @@ def differential_backup(backup_file: str, backup_definition: str, base_backup_fi
|
|
|
139
139
|
raise DifferentialBackupError(f"Unexpected error during differential backup: {e}") from e
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
def incremental_backup(backup_file: str, backup_definition: str, last_backup_file: str, darrc: str):
|
|
142
|
+
def incremental_backup(backup_file: str, backup_definition: str, last_backup_file: str, darrc: str, config_settings: ConfigSettings):
|
|
143
143
|
"""
|
|
144
144
|
Creates an incremental backup based on the last backup file.
|
|
145
145
|
|
|
@@ -173,8 +173,8 @@ def incremental_backup(backup_file: str, backup_definition: str, last_backup_fil
|
|
|
173
173
|
command = ['dar', '-c', backup_file, "-N", '-B', darrc, '-B', backup_definition, '-A', last_backup_file, '-Q', "compress-exclusion", "verbose"]
|
|
174
174
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
175
175
|
try:
|
|
176
|
-
process = run_command(command)
|
|
177
|
-
stdout, stderr = process.
|
|
176
|
+
process = run_command(command, config_settings.command_timeout_secs)
|
|
177
|
+
stdout, stderr = process.stdout, process.stderr
|
|
178
178
|
if process.returncode == 0:
|
|
179
179
|
logger.info("INCR backup completed successfully.")
|
|
180
180
|
elif process.returncode == 5:
|
|
@@ -285,8 +285,8 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
285
285
|
result = True
|
|
286
286
|
command = ['dar', '-t', backup_file, '-Q']
|
|
287
287
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
288
|
-
process = run_command(command)
|
|
289
|
-
stdout, stderr = process.
|
|
288
|
+
process = run_command(command, config_settings.command_timeout_secs)
|
|
289
|
+
stdout, stderr = process.stdout, process.stderr
|
|
290
290
|
if process.returncode == 0:
|
|
291
291
|
logger.info("Archive integrity test passed.")
|
|
292
292
|
else:
|
|
@@ -324,8 +324,8 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
324
324
|
logger.info(f"Restoring file: '{restored_file_path}' from backup to: '{config_settings.test_restore_dir}' for file comparing")
|
|
325
325
|
command = ['dar', '-x', backup_file, '-g', restored_file_path.lstrip("/"), '-R', config_settings.test_restore_dir, '-O', '-Q']
|
|
326
326
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
327
|
-
process = run_command(command)
|
|
328
|
-
stdout, stderr = process.
|
|
327
|
+
process = run_command(command, config_settings.command_timeout_secs)
|
|
328
|
+
stdout, stderr = process.stdout, process.stderr
|
|
329
329
|
if process.returncode != 0:
|
|
330
330
|
logger.error(f"Restore failed, dar return code: {process.returncode}.")
|
|
331
331
|
raise Exception(stderr)
|
|
@@ -343,7 +343,7 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
343
343
|
|
|
344
344
|
|
|
345
345
|
|
|
346
|
-
def restore_backup(backup_name: str,
|
|
346
|
+
def restore_backup(backup_name: str, config_settings: ConfigSettings, restore_dir: str, selection: str =None):
|
|
347
347
|
"""
|
|
348
348
|
Restores a backup file to a specified directory.
|
|
349
349
|
|
|
@@ -353,7 +353,7 @@ def restore_backup(backup_name: str, backup_dir: str, restore_dir: str, selectio
|
|
|
353
353
|
restore_dir (str): The directory where the backup should be restored to.
|
|
354
354
|
selection (str, optional): A selection criteria to restore specific files or directories. Defaults to None.
|
|
355
355
|
"""
|
|
356
|
-
backup_file = os.path.join(backup_dir, backup_name)
|
|
356
|
+
backup_file = os.path.join(config_settings.backup_dir, backup_name)
|
|
357
357
|
command = ['dar', '-x', backup_file, '-O', '-Q', '-D']
|
|
358
358
|
if restore_dir:
|
|
359
359
|
if not os.path.exists(restore_dir):
|
|
@@ -364,13 +364,18 @@ def restore_backup(backup_name: str, backup_dir: str, restore_dir: str, selectio
|
|
|
364
364
|
command.extend(selection_criteria)
|
|
365
365
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
366
366
|
try:
|
|
367
|
-
run_command(command)
|
|
367
|
+
process = run_command(command, config_settings.command_timeout_secs)
|
|
368
|
+
stdout, stderr = process.stdout, process.stderr
|
|
368
369
|
except subprocess.CalledProcessError as e:
|
|
369
370
|
logger.error("Exception details:", exc_info=True)
|
|
371
|
+
logger.error(f"stdout: {stdout}")
|
|
372
|
+
logger.error(f"stderr: {stderr}")
|
|
370
373
|
raise RestoreError(f"Restore command failed: {e}") from e
|
|
371
374
|
except Exception as e:
|
|
372
375
|
logger.exception(f"Unexpected error during restore")
|
|
373
376
|
logger.error("Exception details:", exc_info=True)
|
|
377
|
+
logger.error(f"stdout: {stdout}")
|
|
378
|
+
logger.error(f"stderr: {stderr}")
|
|
374
379
|
raise RestoreError(f"Unexpected error during restore: {e}") from e
|
|
375
380
|
|
|
376
381
|
|
|
@@ -391,7 +396,7 @@ def get_backed_up_files(backup_name: str, backup_dir: str):
|
|
|
391
396
|
command = ['dar', '-l', backup_path, '-am', '-as', "-Txml" , '-Q']
|
|
392
397
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
393
398
|
process = run_command(command)
|
|
394
|
-
stdout,
|
|
399
|
+
stdout,stderr = process.stdout, process.stderr
|
|
395
400
|
# Parse the XML data
|
|
396
401
|
root = ET.fromstring(stdout)
|
|
397
402
|
output = None # help gc
|
|
@@ -422,7 +427,8 @@ def list_contents(backup_name, backup_dir, selection=None):
|
|
|
422
427
|
command.extend(selection_criteria)
|
|
423
428
|
logger.info(f"Running command: {' '.join(map(shlex.quote, command))}")
|
|
424
429
|
process = run_command(command)
|
|
425
|
-
stdout,
|
|
430
|
+
stdout,stderr = process.stdout, process.stderr
|
|
431
|
+
|
|
426
432
|
for line in stdout.splitlines():
|
|
427
433
|
if "[--- REMOVED ENTRY ----]" in line or "[Saved]" in line:
|
|
428
434
|
print(line)
|
|
@@ -484,7 +490,7 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
484
490
|
continue
|
|
485
491
|
|
|
486
492
|
if backup_type == 'FULL':
|
|
487
|
-
backup(backup_file, backup_definition_path, args.darrc)
|
|
493
|
+
backup(backup_file, backup_definition_path, args.darrc, config_settings)
|
|
488
494
|
else:
|
|
489
495
|
base_backup_type = 'FULL' if backup_type == 'DIFF' else 'DIFF'
|
|
490
496
|
|
|
@@ -505,9 +511,9 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
505
511
|
latest_base_backup = os.path.join(config_settings.backup_dir, base_backups[-1].rsplit('.', 2)[0])
|
|
506
512
|
|
|
507
513
|
if backup_type == 'DIFF':
|
|
508
|
-
differential_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc)
|
|
514
|
+
differential_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc, config_settings)
|
|
509
515
|
elif backup_type == 'INCR':
|
|
510
|
-
incremental_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc)
|
|
516
|
+
incremental_backup(backup_file, backup_definition_path, latest_base_backup, args.darrc, config_settings)
|
|
511
517
|
|
|
512
518
|
logger.info("Starting verification...")
|
|
513
519
|
result = verify(args, backup_file, backup_definition_path, config_settings)
|
|
@@ -524,7 +530,7 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
524
530
|
logger.exception(f"Error during {backup_type} backup process, continuing on next backup definition")
|
|
525
531
|
logger.error("Exception details:", exc_info=True)
|
|
526
532
|
|
|
527
|
-
def generate_par2_files(backup_file: str,
|
|
533
|
+
def generate_par2_files(backup_file: str, config_settings: ConfigSettings):
|
|
528
534
|
"""
|
|
529
535
|
Generate PAR2 files for a given backup file in the specified backup directory.
|
|
530
536
|
|
|
@@ -538,13 +544,13 @@ def generate_par2_files(backup_file: str, configSettings: ConfigSettings):
|
|
|
538
544
|
Returns:
|
|
539
545
|
None
|
|
540
546
|
"""
|
|
541
|
-
for filename in os.listdir(
|
|
547
|
+
for filename in os.listdir(config_settings.backup_dir):
|
|
542
548
|
if os.path.basename(backup_file) in filename:
|
|
543
549
|
# Construct the full path to the file
|
|
544
|
-
file_path = os.path.join(
|
|
550
|
+
file_path = os.path.join(config_settings.backup_dir, filename)
|
|
545
551
|
# Run the par2 command to generate redundancy files with error correction
|
|
546
|
-
command = ['par2', 'create', f'-r{
|
|
547
|
-
process = run_command(command)
|
|
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)
|
|
548
554
|
if process.returncode != 0:
|
|
549
555
|
logger.error(f"Error generating par2 files for {file_path}")
|
|
550
556
|
raise subprocess.CalledProcessError(process.returncode, command)
|
|
@@ -631,6 +637,9 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
631
637
|
result = subprocess.run(script, shell=True, check=True)
|
|
632
638
|
logger.info(f"{type} {key}: '{script}' run, return code: {result.returncode}")
|
|
633
639
|
logger.info(f"{type} stdout:\n{result.stdout}")
|
|
640
|
+
if result.returncode != 0:
|
|
641
|
+
logger.error(f"{type} stderr:\n{result.stderr}")
|
|
642
|
+
raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
|
|
634
643
|
except subprocess.CalledProcessError as e:
|
|
635
644
|
logger.error(f"Error executing {key}: '{script}': {e}")
|
|
636
645
|
if result:
|
|
@@ -658,8 +667,8 @@ def main():
|
|
|
658
667
|
parser.add_argument('-l', '--list', action='store_true', help="List available archives.")
|
|
659
668
|
parser.add_argument('--list-contents', help="List the contents of the specified archive.")
|
|
660
669
|
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.")
|
|
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.")
|
|
663
672
|
parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
|
|
664
673
|
parser.add_argument('--log-level', type=str, help="`debug` or `trace`", default="info")
|
|
665
674
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
@@ -739,7 +748,7 @@ def main():
|
|
|
739
748
|
list_contents(args.list_contents, config_settings.backup_dir, args.selection)
|
|
740
749
|
elif args.restore:
|
|
741
750
|
restore_dir = args.restore_dir if args.restore_dir else config_settings.test_restore_dir
|
|
742
|
-
restore_backup(args.restore, config_settings
|
|
751
|
+
restore_backup(args.restore, config_settings, restore_dir, args.selection)
|
|
743
752
|
else:
|
|
744
753
|
parser.print_help()
|
|
745
754
|
|
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,16 +68,21 @@ 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')
|
|
84
|
+
# logging.basicConfig(filename=log_file, level=level_used,
|
|
85
|
+
# format='%(asctime)s - %(levelname)s - %(message)s')
|
|
77
86
|
|
|
78
87
|
except Exception as e:
|
|
79
88
|
print("logging not initialized, exiting.")
|
|
@@ -83,30 +92,56 @@ def setup_logging(log_file: str, log_level: str, log_to_stdout=False) -> logging
|
|
|
83
92
|
return logger
|
|
84
93
|
|
|
85
94
|
|
|
95
|
+
class CommandResult(NamedTuple):
|
|
96
|
+
"""
|
|
97
|
+
The reult of the run_command() function.
|
|
98
|
+
"""
|
|
99
|
+
process: subprocess.CompletedProcess
|
|
100
|
+
stdout: str
|
|
101
|
+
stderr: str
|
|
102
|
+
returncode: int
|
|
103
|
+
timeout: int
|
|
104
|
+
command: list[str]
|
|
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
|
+
|
|
101
129
|
try:
|
|
102
|
-
logger.
|
|
130
|
+
logger.info(f"Running command: {command}")
|
|
103
131
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
104
|
-
process.
|
|
132
|
+
stdout, stderr = process.communicate(timeout=30) # Wait with timeout
|
|
133
|
+
except subprocess.TimeoutExpired:
|
|
134
|
+
process.terminate()
|
|
135
|
+
logger.error(f"Command: '{command}' timed out and was terminated.")
|
|
105
136
|
except Exception as e:
|
|
106
137
|
logger.error(f"Error running command: {command}", exc_info=True)
|
|
107
138
|
raise
|
|
108
|
-
|
|
109
|
-
|
|
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)
|
|
110
145
|
|
|
111
146
|
|
|
112
147
|
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.0
|
|
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=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,,
|
|
@@ -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
|