dar-backup 0.6.11__py3-none-any.whl → 0.6.13__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/clean_log.py +1 -1
- dar_backup/cleanup.py +6 -2
- dar_backup/config_settings.py +34 -11
- dar_backup/dar-backup.conf +33 -0
- dar_backup/dar_backup.py +107 -44
- dar_backup/installer.py +122 -0
- dar_backup/manager.py +7 -3
- dar_backup/util.py +98 -187
- {dar_backup-0.6.11.dist-info → dar_backup-0.6.13.dist-info}/METADATA +200 -228
- dar_backup-0.6.13.dist-info/RECORD +16 -0
- {dar_backup-0.6.11.dist-info → dar_backup-0.6.13.dist-info}/entry_points.txt +1 -0
- dar_backup-0.6.11.dist-info/RECORD +0 -14
- {dar_backup-0.6.11.dist-info → dar_backup-0.6.13.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.11.dist-info → dar_backup-0.6.13.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.6.
|
|
1
|
+
__version__ = "0.6.13"
|
dar_backup/clean_log.py
CHANGED
|
@@ -122,7 +122,7 @@ def main():
|
|
|
122
122
|
|
|
123
123
|
args = parser.parse_args()
|
|
124
124
|
|
|
125
|
-
config_settings = ConfigSettings(os.path.expanduser(args.config_file))
|
|
125
|
+
config_settings = ConfigSettings(os.path.expanduser(os.path.expandvars(args.config_file)))
|
|
126
126
|
|
|
127
127
|
if not args.file:
|
|
128
128
|
args.file = [config_settings.logfile_location]
|
dar_backup/cleanup.py
CHANGED
|
@@ -171,7 +171,7 @@ def main():
|
|
|
171
171
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
172
172
|
args = parser.parse_args()
|
|
173
173
|
|
|
174
|
-
args.config_file = os.path.expanduser(args.config_file)
|
|
174
|
+
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
if args.version:
|
|
@@ -181,7 +181,11 @@ def main():
|
|
|
181
181
|
config_settings = ConfigSettings(args.config_file)
|
|
182
182
|
|
|
183
183
|
start_time=int(time())
|
|
184
|
-
|
|
184
|
+
|
|
185
|
+
# command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
|
|
186
|
+
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
187
|
+
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
188
|
+
|
|
185
189
|
|
|
186
190
|
logger.info(f"=====================================")
|
|
187
191
|
logger.info(f"cleanup.py started, version: {about.__version__}")
|
dar_backup/config_settings.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import configparser
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
import sys
|
|
5
3
|
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field, fields
|
|
7
|
+
from os.path import expandvars, expanduser
|
|
8
|
+
from pathlib import Path
|
|
6
9
|
|
|
7
10
|
@dataclass
|
|
8
11
|
class ConfigSettings:
|
|
@@ -20,21 +23,35 @@ class ConfigSettings:
|
|
|
20
23
|
backup_d_dir (str): The directory for backup.d.
|
|
21
24
|
diff_age (int): The age for differential backups before deletion.
|
|
22
25
|
incr_age (int): The age for incremental backups before deletion.
|
|
26
|
+
error_correction_percent (int): The error correction percentage for PAR2.
|
|
27
|
+
par2_enabled (bool): Whether PAR2 is enabled.
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
config_file: str
|
|
31
|
+
logfile_location: str = field(init=False)
|
|
32
|
+
max_size_verification_mb: int = field(init=False)
|
|
33
|
+
min_size_verification_mb: int = field(init=False)
|
|
34
|
+
no_files_verification: int = field(init=False)
|
|
35
|
+
command_timeout_secs: int = field(init=False)
|
|
36
|
+
backup_dir: str = field(init=False)
|
|
37
|
+
test_restore_dir: str = field(init=False)
|
|
38
|
+
backup_d_dir: str = field(init=False)
|
|
39
|
+
diff_age: int = field(init=False)
|
|
40
|
+
incr_age: int = field(init=False)
|
|
41
|
+
error_correction_percent: int = field(init=False)
|
|
42
|
+
par2_enabled: bool = field(init=False)
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
26
45
|
"""
|
|
27
|
-
Initializes the ConfigSettings instance by reading the specified configuration file
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
config_file (str): The path to the configuration file.
|
|
46
|
+
Initializes the ConfigSettings instance by reading the specified configuration file
|
|
47
|
+
and expands environment variables for all string fields.
|
|
31
48
|
"""
|
|
32
|
-
if config_file is None:
|
|
49
|
+
if self.config_file is None:
|
|
33
50
|
raise ValueError("`config_file` must be specified.")
|
|
34
51
|
|
|
35
52
|
self.config = configparser.ConfigParser()
|
|
36
53
|
try:
|
|
37
|
-
self.config.read(config_file)
|
|
54
|
+
self.config.read(self.config_file)
|
|
38
55
|
self.logfile_location = self.config['MISC']['LOGFILE_LOCATION']
|
|
39
56
|
self.max_size_verification_mb = int(self.config['MISC']['MAX_SIZE_VERIFICATION_MB'])
|
|
40
57
|
self.min_size_verification_mb = int(self.config['MISC']['MIN_SIZE_VERIFICATION_MB'])
|
|
@@ -46,12 +63,18 @@ class ConfigSettings:
|
|
|
46
63
|
self.diff_age = int(self.config['AGE']['DIFF_AGE'])
|
|
47
64
|
self.incr_age = int(self.config['AGE']['INCR_AGE'])
|
|
48
65
|
self.error_correction_percent = int(self.config['PAR2']['ERROR_CORRECTION_PERCENT'])
|
|
49
|
-
self.par2_enabled =
|
|
66
|
+
self.par2_enabled = self.config['PAR2']['ENABLED'].lower() in ('true', '1', 'yes')
|
|
67
|
+
|
|
50
68
|
# Ensure the directories exist
|
|
51
69
|
Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
|
|
52
70
|
Path(self.test_restore_dir).mkdir(parents=True, exist_ok=True)
|
|
53
71
|
Path(self.backup_d_dir).mkdir(parents=True, exist_ok=True)
|
|
54
72
|
|
|
73
|
+
# Expand environment variables for all string fields
|
|
74
|
+
for field in fields(self):
|
|
75
|
+
if isinstance(getattr(self, field.name), str):
|
|
76
|
+
setattr(self, field.name, expanduser(expandvars(getattr(self, field.name))))
|
|
77
|
+
|
|
55
78
|
except FileNotFoundError as e:
|
|
56
79
|
logging.error(f"Configuration file not found: {self.config_file}")
|
|
57
80
|
logging.error(f"Error details: {e}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This config file is intended to demo `dar-backup`.
|
|
2
|
+
#
|
|
3
|
+
# The `installer` puts it in ~/.config/dar-backup/dar-backup.conf
|
|
4
|
+
|
|
5
|
+
[MISC]
|
|
6
|
+
LOGFILE_LOCATION = ~/dar-backup/dar-backup.log
|
|
7
|
+
MAX_SIZE_VERIFICATION_MB = 20
|
|
8
|
+
MIN_SIZE_VERIFICATION_MB = 1
|
|
9
|
+
NO_FILES_VERIFICATION = 5
|
|
10
|
+
# timeout in seconds for backup, test, restore and par2 operations
|
|
11
|
+
# The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
|
|
12
|
+
# If a timeout is not specified when using the util.run_command(), a default timeout of 30 secs is used.
|
|
13
|
+
COMMAND_TIMEOUT_SECS = 86400
|
|
14
|
+
|
|
15
|
+
[DIRECTORIES]
|
|
16
|
+
BACKUP_DIR = ~/dar-backup/backups
|
|
17
|
+
BACKUP.D_DIR = ~/.config/dar-backup/backup.d/
|
|
18
|
+
TEST_RESTORE_DIR = ~/dar-backup/restore/
|
|
19
|
+
|
|
20
|
+
[AGE]
|
|
21
|
+
# age settings are in days
|
|
22
|
+
DIFF_AGE = 100
|
|
23
|
+
INCR_AGE = 40
|
|
24
|
+
|
|
25
|
+
[PAR2]
|
|
26
|
+
ERROR_CORRECTION_PERCENT = 5
|
|
27
|
+
ENABLED = True
|
|
28
|
+
|
|
29
|
+
[PREREQ]
|
|
30
|
+
#SCRIPT_1 = <pre-script 1>
|
|
31
|
+
|
|
32
|
+
[POSTREQ]
|
|
33
|
+
#SCRIPT_1 = <post-script 1>
|
dar_backup/dar_backup.py
CHANGED
|
@@ -7,14 +7,18 @@ import os
|
|
|
7
7
|
import random
|
|
8
8
|
import re
|
|
9
9
|
import shlex
|
|
10
|
+
import shutil
|
|
10
11
|
import subprocess
|
|
11
|
-
import sys
|
|
12
12
|
import xml.etree.ElementTree as ET
|
|
13
|
-
|
|
13
|
+
import tempfile
|
|
14
14
|
|
|
15
15
|
from argparse import ArgumentParser
|
|
16
16
|
from datetime import datetime
|
|
17
17
|
from pathlib import Path
|
|
18
|
+
from sys import exit
|
|
19
|
+
from sys import stderr
|
|
20
|
+
from sys import argv
|
|
21
|
+
from sys import version_info
|
|
18
22
|
from time import time
|
|
19
23
|
from typing import List
|
|
20
24
|
|
|
@@ -27,6 +31,7 @@ from dar_backup.util import BackupError
|
|
|
27
31
|
from dar_backup.util import RestoreError
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
RESULT = True
|
|
30
35
|
logger = None
|
|
31
36
|
|
|
32
37
|
def generic_backup(type: str, command: List[str], backup_file: str, backup_definition: str, darrc: str, config_settings: ConfigSettings, args: argparse.Namespace):
|
|
@@ -96,12 +101,12 @@ def find_files_with_paths(xml_root: ET.Element):
|
|
|
96
101
|
current_path = []
|
|
97
102
|
|
|
98
103
|
for elem in xml_root.iter():
|
|
99
|
-
if elem.tag == "
|
|
104
|
+
if elem.tag == "Directory":
|
|
100
105
|
current_path.append(elem.get('name'))
|
|
101
|
-
elif elem.tag == "
|
|
106
|
+
elif elem.tag == "File":
|
|
102
107
|
file_path = ("/".join(current_path + [elem.get('name')]), elem.get('size'))
|
|
103
108
|
files.append(file_path)
|
|
104
|
-
elif elem.tag == "
|
|
109
|
+
elif elem.tag == "Directory" and elem.get('Name') in current_path:
|
|
105
110
|
current_path.pop()
|
|
106
111
|
|
|
107
112
|
return files
|
|
@@ -189,10 +194,10 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
189
194
|
logger.info(f"No files between {config_settings.min_size_verification_mb}MB and {config_settings.max_size_verification_mb}MB for verification, skipping")
|
|
190
195
|
return result
|
|
191
196
|
|
|
197
|
+
# find Root path in backup definition
|
|
192
198
|
with open(backup_definition, 'r') as f:
|
|
193
199
|
backup_definition_content = f.readlines()
|
|
194
200
|
logger.debug(f"Backup definition: '{backup_definition}', content:\n{backup_definition_content}")
|
|
195
|
-
# Initialize a variable to hold the path after "-R"
|
|
196
201
|
root_path = None
|
|
197
202
|
for line in backup_definition_content:
|
|
198
203
|
line = line.strip()
|
|
@@ -200,7 +205,10 @@ def verify(args: argparse.Namespace, backup_file: str, backup_definition: str, c
|
|
|
200
205
|
root_path = line.split("-R", 1)[1].strip()
|
|
201
206
|
break
|
|
202
207
|
if root_path is None:
|
|
203
|
-
|
|
208
|
+
msg = f"No Root (-R) path found in the backup definition file: '{backup_definition}', restore verification skipped"
|
|
209
|
+
raise BackupError(msg)
|
|
210
|
+
|
|
211
|
+
|
|
204
212
|
|
|
205
213
|
no_files_verification = config_settings.no_files_verification
|
|
206
214
|
if len(files) < config_settings.no_files_verification:
|
|
@@ -407,7 +415,7 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
407
415
|
logger.info(f"Using alternate reference archive: {latest_base_backup}")
|
|
408
416
|
if not os.path.exists(latest_base_backup + '.1.dar'):
|
|
409
417
|
logger.error(f"Alternate reference archive: \"{latest_base_backup}.1.dar\" does not exist, exiting.")
|
|
410
|
-
|
|
418
|
+
exit(1)
|
|
411
419
|
else:
|
|
412
420
|
base_backups = sorted(
|
|
413
421
|
[f for f in os.listdir(config_settings.backup_dir) if f.startswith(f"{backup_definition}_{base_backup_type}_") and f.endswith('.1.dar')],
|
|
@@ -435,9 +443,10 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
435
443
|
logger.info("Generate par2 redundancy files.")
|
|
436
444
|
generate_par2_files(backup_file, config_settings, args)
|
|
437
445
|
logger.info("par2 files completed successfully.")
|
|
438
|
-
|
|
439
446
|
|
|
440
447
|
except Exception as e:
|
|
448
|
+
global RESULT
|
|
449
|
+
RESULT = False
|
|
441
450
|
logger.exception(f"Error during {backup_type} backup process, continuing to next backup definition.")
|
|
442
451
|
|
|
443
452
|
|
|
@@ -491,8 +500,46 @@ def generate_par2_files(backup_file: str, config_settings: ConfigSettings, args)
|
|
|
491
500
|
|
|
492
501
|
|
|
493
502
|
|
|
503
|
+
def filter_darrc_file(darrc_path):
|
|
504
|
+
"""
|
|
505
|
+
Filters the .darrc file to remove lines containing the options: -vt, -vs, -vd, -vf, and -va.
|
|
506
|
+
The filtered version is stored in a uniquely named file in the home directory of the user running the script.
|
|
507
|
+
The file permissions are set to 440.
|
|
508
|
+
|
|
509
|
+
:param darrc_path: Path to the original .darrc file.
|
|
510
|
+
:return: Path to the filtered .darrc file.
|
|
511
|
+
"""
|
|
512
|
+
# Define options to filter out
|
|
513
|
+
options_to_remove = {"-vt", "-vs", "-vd", "-vf", "-va"}
|
|
514
|
+
|
|
515
|
+
# Get the user's home directory
|
|
516
|
+
home_dir = os.path.expanduser("~")
|
|
517
|
+
|
|
518
|
+
# Create a unique file name in the home directory
|
|
519
|
+
filtered_darrc_path = os.path.join(home_dir, f"filtered_darrc_{next(tempfile._get_candidate_names())}.darrc")
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
with open(darrc_path, "r") as infile, open(filtered_darrc_path, "w") as outfile:
|
|
523
|
+
for line in infile:
|
|
524
|
+
# Check if any unwanted option is in the line
|
|
525
|
+
if not any(option in line for option in options_to_remove):
|
|
526
|
+
outfile.write(line)
|
|
527
|
+
|
|
528
|
+
# Set file permissions to 440 (read-only for owner and group, no permissions for others)
|
|
529
|
+
os.chmod(filtered_darrc_path, 0o440)
|
|
530
|
+
|
|
531
|
+
return filtered_darrc_path
|
|
532
|
+
|
|
533
|
+
except Exception as e:
|
|
534
|
+
# If anything goes wrong, clean up the temp file if it was created
|
|
535
|
+
if os.path.exists(filtered_darrc_path):
|
|
536
|
+
os.remove(filtered_darrc_path)
|
|
537
|
+
raise RuntimeError(f"Error filtering .darrc file: {e}")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
|
|
494
541
|
def show_version():
|
|
495
|
-
script_name = os.path.basename(
|
|
542
|
+
script_name = os.path.basename(argv[0])
|
|
496
543
|
print(f"{script_name} {about.__version__}")
|
|
497
544
|
print(f"dar-backup.py source code is here: https://github.com/per2jensen/dar-backup")
|
|
498
545
|
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
@@ -577,14 +624,14 @@ def requirements(type: str, config_setting: ConfigSettings):
|
|
|
577
624
|
|
|
578
625
|
|
|
579
626
|
def main():
|
|
580
|
-
global logger
|
|
627
|
+
global logger, RESULT
|
|
581
628
|
|
|
582
629
|
MIN_PYTHON_VERSION = (3, 9)
|
|
583
|
-
if
|
|
584
|
-
|
|
585
|
-
|
|
630
|
+
if version_info < MIN_PYTHON_VERSION:
|
|
631
|
+
stderr.write(f"Error: This script requires Python {'.'.join(map(str, MIN_PYTHON_VERSION))} or higher.\n")
|
|
632
|
+
exit(1)
|
|
586
633
|
|
|
587
|
-
parser = argparse.ArgumentParser(description="Backup
|
|
634
|
+
parser = argparse.ArgumentParser(description="Backup, verify & redundancy using dar and par2.")
|
|
588
635
|
parser.add_argument('-F', '--full-backup', action='store_true', help="Perform a full backup.")
|
|
589
636
|
parser.add_argument('-D', '--differential-backup', action='store_true', help="Perform differential backup.")
|
|
590
637
|
parser.add_argument('-I', '--incremental-backup', action='store_true', help="Perform incremental backup.")
|
|
@@ -600,43 +647,55 @@ def main():
|
|
|
600
647
|
parser.add_argument('-r', '--restore', type=str, help="Restore specified archive.")
|
|
601
648
|
parser.add_argument('--restore-dir', type=str, help="Directory to restore files to.")
|
|
602
649
|
parser.add_argument('--verbose', action='store_true', help="Print various status messages to screen")
|
|
650
|
+
parser.add_argument('--suppress-dar-msg', action='store_true', help="cancel dar options in .darrc: -vt, -vs, -vd, -vf and -va")
|
|
603
651
|
parser.add_argument('--log-level', type=str, help="`debug` or `trace`", default="info")
|
|
604
652
|
parser.add_argument('--log-stdout', action='store_true', help='also print log messages to stdout')
|
|
605
653
|
parser.add_argument('--do-not-compare', action='store_true', help="do not compare restores to file system")
|
|
606
654
|
parser.add_argument('-v', '--version', action='store_true', help="Show version and license information.")
|
|
607
655
|
args = parser.parse_args()
|
|
608
656
|
|
|
609
|
-
args.config_file = os.path.expanduser(args.config_file)
|
|
610
|
-
config_settings = ConfigSettings(args.config_file)
|
|
611
|
-
|
|
612
657
|
if args.version:
|
|
613
658
|
show_version()
|
|
614
|
-
|
|
659
|
+
exit(0)
|
|
615
660
|
elif args.examples:
|
|
616
661
|
show_examples()
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
logger = setup_logging(config_settings.logfile_location, args.log_level, args.log_stdout)
|
|
620
|
-
|
|
621
|
-
if not args.darrc:
|
|
622
|
-
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
623
|
-
args.darrc = os.path.join(current_script_dir, ".darrc")
|
|
624
|
-
|
|
625
|
-
if os.path.exists(args.darrc) and os.path.isfile(args.darrc):
|
|
626
|
-
logger.debug(f"Using .darrc: {args.darrc}")
|
|
627
|
-
else:
|
|
628
|
-
logger.error(f"Supplied .darrc: '{args.darrc}' does not exist or is not a file")
|
|
662
|
+
exit(0)
|
|
629
663
|
|
|
630
664
|
if not args.config_file:
|
|
631
|
-
|
|
632
|
-
|
|
665
|
+
print(f"Config file not specified, exiting", file=stderr)
|
|
666
|
+
exit(1)
|
|
633
667
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
668
|
+
config_settings_path = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
669
|
+
if not os.path.exists(config_settings_path):
|
|
670
|
+
print(f"Config file {args.config_file} does not exist.", file=stderr)
|
|
671
|
+
exit(127)
|
|
672
|
+
|
|
673
|
+
args.config_file = config_settings_path
|
|
674
|
+
config_settings = ConfigSettings(args.config_file)
|
|
637
675
|
|
|
676
|
+
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
677
|
+
if command_output_log == config_settings.logfile_location:
|
|
678
|
+
print(f"Error: logfile_location in {args.config_file} does not end at 'dar-backup.log', exiting", file=stderr)
|
|
679
|
+
|
|
680
|
+
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
638
681
|
|
|
639
682
|
try:
|
|
683
|
+
if not args.darrc:
|
|
684
|
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
685
|
+
args.darrc = os.path.join(current_script_dir, ".darrc")
|
|
686
|
+
|
|
687
|
+
darrc_file = os.path.expanduser(os.path.expandvars(args.darrc))
|
|
688
|
+
if os.path.exists(darrc_file) and os.path.isfile(darrc_file):
|
|
689
|
+
logger.debug(f"Using .darrc: {args.darrc}")
|
|
690
|
+
else:
|
|
691
|
+
logger.error(f"Supplied .darrc: '{args.darrc}' does not exist or is not a file, exiting", file=stderr)
|
|
692
|
+
exit(127)
|
|
693
|
+
|
|
694
|
+
if args.suppress_dar_msg:
|
|
695
|
+
logger.info("Suppressing dar messages: -vt, -vs, -vd, -vf and -va")
|
|
696
|
+
args.darrc = filter_darrc_file(args.darrc)
|
|
697
|
+
logger.debug(f"Filtered .darrc file saved at: {args.darrc}")
|
|
698
|
+
|
|
640
699
|
start_time=int(time())
|
|
641
700
|
logger.info(f"=====================================")
|
|
642
701
|
logger.info(f"dar-backup.py started, version: {about.__version__}")
|
|
@@ -664,14 +723,13 @@ def main():
|
|
|
664
723
|
args.verbose and (print(f"PAR2 enabled: {config_settings.par2_enabled}"))
|
|
665
724
|
args.verbose and (print(f"--do-not-compare: {args.do_not_compare}"))
|
|
666
725
|
|
|
667
|
-
|
|
668
726
|
# sanity check
|
|
669
727
|
if args.backup_definition and not os.path.exists(os.path.join(config_settings.backup_d_dir, args.backup_definition)):
|
|
670
728
|
logger.error(f"Backup definition: '{args.backup_definition}' does not exist, exiting")
|
|
671
|
-
|
|
729
|
+
exit(127)
|
|
672
730
|
if args.backup_definition and '_' in args.backup_definition:
|
|
673
731
|
logger.error(f"Backup definition: '{args.backup_definition}' contains '_', exiting")
|
|
674
|
-
|
|
732
|
+
exit(1)
|
|
675
733
|
|
|
676
734
|
|
|
677
735
|
requirements('PREREQ', config_settings)
|
|
@@ -694,19 +752,24 @@ def main():
|
|
|
694
752
|
|
|
695
753
|
requirements('POSTREQ', config_settings)
|
|
696
754
|
|
|
697
|
-
args.verbose and print("\033[1m\033[32mSUCCESS\033[0m No errors encountered")
|
|
698
|
-
sys.exit(0)
|
|
699
755
|
except Exception as e:
|
|
700
756
|
logger.exception("An error occurred")
|
|
701
757
|
logger.error("Exception details:", exc_info=True)
|
|
702
758
|
args.verbose and print("\033[1m\033[31mErrors\033[0m encountered")
|
|
703
|
-
|
|
759
|
+
RESULT = False
|
|
704
760
|
finally:
|
|
705
761
|
end_time=int(time())
|
|
706
762
|
logger.info(f"END TIME: {end_time}")
|
|
707
|
-
|
|
708
|
-
|
|
763
|
+
# Clean up
|
|
764
|
+
if args.suppress_dar_msg and os.path.exists(args.darrc) and os.path.dirname(args.darrc) == os.path.expanduser("~"):
|
|
765
|
+
if args.darrc.startswith("filtered_darrc_"):
|
|
766
|
+
os.remove(args.darrc)
|
|
767
|
+
logger.info(f"Removed filtered .darrc: {args.darrc}")
|
|
709
768
|
|
|
710
769
|
|
|
770
|
+
if RESULT:
|
|
771
|
+
exit(0)
|
|
772
|
+
else:
|
|
773
|
+
exit(1)
|
|
711
774
|
if __name__ == "__main__":
|
|
712
775
|
main()
|
dar_backup/installer.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
installer.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/installer.py
|
|
4
|
+
This script is part of dar-backup, a backup solution for Linux using dar and systemd.
|
|
5
|
+
|
|
6
|
+
Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
7
|
+
|
|
8
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW,
|
|
9
|
+
not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See section 15 and section 16 in the supplied "LICENSE" file
|
|
11
|
+
|
|
12
|
+
This script can be used to configure dar-backup on your system.
|
|
13
|
+
It is non-destructive and will not overwrite any existing files or directories.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from . import __about__ as about
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
LICENSE = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
25
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
26
|
+
See section 15 and section 16 in the supplied "LICENSE" file.'''
|
|
27
|
+
|
|
28
|
+
CONFIG_DIR = os.path.expanduser("~/.config/dar-backup")
|
|
29
|
+
DAR_BACKUP_DIR = os.path.expanduser("~/dar-backup/")
|
|
30
|
+
|
|
31
|
+
BACKUP_DEFINITION = '''
|
|
32
|
+
# Demo of a `dar-backup` definition file
|
|
33
|
+
# This back definition file configures a backup of ~/.config/dar-backup
|
|
34
|
+
# `dar-backup` puts the backups in ~/dar-backup/backups
|
|
35
|
+
# ------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# Switch to ordered selection mode, which means that the following options
|
|
38
|
+
# will be considered top to bottom
|
|
39
|
+
-am
|
|
40
|
+
|
|
41
|
+
# Backup Root dir
|
|
42
|
+
-R @@HOME_DIR@@
|
|
43
|
+
|
|
44
|
+
# Directories to backup below the Root dir
|
|
45
|
+
-g .config/dar-backup
|
|
46
|
+
|
|
47
|
+
# Examples of directories to exclude below the Root dir
|
|
48
|
+
-P mnt
|
|
49
|
+
-P .private
|
|
50
|
+
-P .cache
|
|
51
|
+
|
|
52
|
+
# compression level
|
|
53
|
+
-z5
|
|
54
|
+
|
|
55
|
+
# no overwrite, if you rerun a backup, 'dar' halts and asks what to do
|
|
56
|
+
-n
|
|
57
|
+
|
|
58
|
+
# size of each slice in the archive
|
|
59
|
+
--slice 10G
|
|
60
|
+
|
|
61
|
+
# bypass directores marked as cache directories
|
|
62
|
+
# http://dar.linux.free.fr/doc/Features.html
|
|
63
|
+
--cache-directory-tagging
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
description="Set up `dar-backup` on your system.",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"-i", "--install",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Deploy a simple config file, use ~/dar-backup/ for log file, archives and restore tests."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"-v", "--version",
|
|
78
|
+
action="version",
|
|
79
|
+
version=f"%(prog)s version {about.__version__}, {LICENSE}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
args = parser.parse_args()
|
|
83
|
+
|
|
84
|
+
if args.install:
|
|
85
|
+
errors = []
|
|
86
|
+
if os.path.exists(CONFIG_DIR):
|
|
87
|
+
errors.append(f"Config directory '{CONFIG_DIR}' already exists.")
|
|
88
|
+
|
|
89
|
+
if os.path.exists(DAR_BACKUP_DIR):
|
|
90
|
+
errors.append(f"Directory '{DAR_BACKUP_DIR}' already exists.")
|
|
91
|
+
|
|
92
|
+
if len(errors) > 0:
|
|
93
|
+
for error in errors:
|
|
94
|
+
print(f"Error: {error}")
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
os.makedirs(DAR_BACKUP_DIR, exist_ok=False)
|
|
98
|
+
os.makedirs(os.path.join(DAR_BACKUP_DIR, "backups"), exist_ok=False)
|
|
99
|
+
os.makedirs(os.path.join(DAR_BACKUP_DIR, "restore"), exist_ok=False)
|
|
100
|
+
os.makedirs(CONFIG_DIR, exist_ok=False)
|
|
101
|
+
os.makedirs(os.path.join(CONFIG_DIR, "backup.d"), exist_ok=False)
|
|
102
|
+
print(f"Directories created: `{DAR_BACKUP_DIR}` and `{CONFIG_DIR}`")
|
|
103
|
+
|
|
104
|
+
script_dir = Path(__file__).parent
|
|
105
|
+
source_file = script_dir / "dar-backup.conf"
|
|
106
|
+
destination_file = Path(CONFIG_DIR) / "dar-backup.conf"
|
|
107
|
+
shutil.copy(source_file, destination_file)
|
|
108
|
+
print(f"Config file deployed to {destination_file}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
backup_definition = BACKUP_DEFINITION.replace("@@HOME_DIR@@", os.path.expanduser("~"))
|
|
112
|
+
with open(os.path.join(CONFIG_DIR, "backup.d", "default"), "w") as f:
|
|
113
|
+
f.write(backup_definition)
|
|
114
|
+
print(f"Default backup definition file deployed to {os.path.join(CONFIG_DIR, 'backup.d', 'default')}")
|
|
115
|
+
print("1. Now run `manager --create` to create the catalog database.")
|
|
116
|
+
print("2. Then you can run `dar-backup --full-backup` to create a backup.")
|
|
117
|
+
print("3. List backups with `dar-backup --list`")
|
|
118
|
+
print("4. List contents of a backup with `dar-backup --list-contents <backup-name>`")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
dar_backup/manager.py
CHANGED
|
@@ -335,7 +335,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
|
|
|
335
335
|
"""
|
|
336
336
|
backup_def = backup_def_from_archive(archive)
|
|
337
337
|
database_path = os.path.join(config_settings.backup_dir, f"{backup_def}{DB_SUFFIX}")
|
|
338
|
-
cat_no = cat_no_for_name(archive, config_settings)
|
|
338
|
+
cat_no:int = cat_no_for_name(archive, config_settings)
|
|
339
339
|
if cat_no >= 0:
|
|
340
340
|
command = ['dar_manager', '--base', database_path, "--delete", str(cat_no)]
|
|
341
341
|
process: CommandResult = run_command(command)
|
|
@@ -395,12 +395,16 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
395
395
|
sys.exit(0)
|
|
396
396
|
|
|
397
397
|
# setup logging
|
|
398
|
-
args.config_file = os.path.expanduser(args.config_file)
|
|
398
|
+
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
399
399
|
config_settings = ConfigSettings(args.config_file)
|
|
400
400
|
if not os.path.dirname(config_settings.logfile_location):
|
|
401
401
|
print(f"Directory for log file '{config_settings.logfile_location}' does not exist, exiting")
|
|
402
402
|
sys.exit(1)
|
|
403
|
-
|
|
403
|
+
|
|
404
|
+
# command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
|
|
405
|
+
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
406
|
+
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
407
|
+
|
|
404
408
|
|
|
405
409
|
start_time=int(time())
|
|
406
410
|
logger.info(f"=====================================")
|