dar-backup 0.6.21__py3-none-any.whl → 0.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/Changelog.md +35 -2
- dar_backup/README.md +433 -97
- dar_backup/__about__.py +1 -1
- dar_backup/clean_log.py +3 -0
- dar_backup/cleanup.py +13 -7
- dar_backup/command_runner.py +2 -0
- dar_backup/config_settings.py +18 -0
- dar_backup/dar-backup.conf +4 -2
- dar_backup/dar-backup.conf.j2 +64 -0
- dar_backup/dar_backup.py +19 -13
- dar_backup/dar_backup_systemd.py +3 -0
- dar_backup/demo.py +153 -77
- dar_backup/demo_backup_def.j2 +62 -0
- dar_backup/exceptions.py +2 -0
- dar_backup/installer.py +119 -4
- dar_backup/manager.py +9 -4
- dar_backup/rich_progress.py +4 -0
- dar_backup/util.py +107 -13
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/METADATA +454 -98
- dar_backup-0.7.2.dist-info/RECORD +25 -0
- dar_backup-0.6.21.dist-info/RECORD +0 -23
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.7.2"
|
|
2
2
|
|
|
3
3
|
__license__ = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
4
4
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
dar_backup/clean_log.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
2
4
|
"""
|
|
3
5
|
clean-log.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/clean-log.py
|
|
4
6
|
This script is part of dar-backup, a backup solution for Linux using dar and systemd.
|
|
@@ -135,6 +137,7 @@ def main():
|
|
|
135
137
|
args.file = [config_settings.logfile_location]
|
|
136
138
|
|
|
137
139
|
for file_path in args.file:
|
|
140
|
+
|
|
138
141
|
if not isinstance(file_path, (str, bytes, os.PathLike)):
|
|
139
142
|
print(f"Error: Invalid file path type: {file_path}")
|
|
140
143
|
sys.exit(1)
|
dar_backup/cleanup.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
3
|
|
|
3
4
|
"""
|
|
4
5
|
cleanup.py source code is here: https://github.com/per2jensen/dar-backup
|
|
@@ -40,6 +41,8 @@ from dar_backup.util import show_version
|
|
|
40
41
|
from dar_backup.util import get_invocation_command_line
|
|
41
42
|
from dar_backup.util import print_aligned_settings
|
|
42
43
|
from dar_backup.util import backup_definition_completer, list_archive_completer
|
|
44
|
+
from dar_backup.util import is_safe_filename
|
|
45
|
+
from dar_backup.util import show_scriptname
|
|
43
46
|
|
|
44
47
|
from dar_backup.command_runner import CommandRunner
|
|
45
48
|
from dar_backup.command_runner import CommandResult
|
|
@@ -79,7 +82,7 @@ def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=Non
|
|
|
79
82
|
if file_date < cutoff_date:
|
|
80
83
|
file_path = os.path.join(backup_dir, filename)
|
|
81
84
|
try:
|
|
82
|
-
os.remove(file_path)
|
|
85
|
+
is_safe_filename(file_path) and os.remove(file_path)
|
|
83
86
|
logger.info(f"Deleted {backup_type} backup: {file_path}")
|
|
84
87
|
archive_name = filename.split('.')[0]
|
|
85
88
|
if not archive_name in archives_deleted:
|
|
@@ -108,7 +111,7 @@ def delete_archive(backup_dir, archive_name, args):
|
|
|
108
111
|
if archive_regex.match(filename):
|
|
109
112
|
file_path = os.path.join(backup_dir, filename)
|
|
110
113
|
try:
|
|
111
|
-
os.remove(file_path)
|
|
114
|
+
is_safe_filename(file_path) and os.remove(file_path)
|
|
112
115
|
logger.info(f"Deleted archive slice: {file_path}")
|
|
113
116
|
files_deleted = True
|
|
114
117
|
except Exception as e:
|
|
@@ -126,7 +129,7 @@ def delete_archive(backup_dir, archive_name, args):
|
|
|
126
129
|
if par2_regex.match(filename):
|
|
127
130
|
file_path = os.path.join(backup_dir, filename)
|
|
128
131
|
try:
|
|
129
|
-
os.remove(file_path)
|
|
132
|
+
is_safe_filename(file_path) and os.remove(file_path)
|
|
130
133
|
logger.info(f"Deleted PAR2 file: {file_path}")
|
|
131
134
|
files_deleted = True
|
|
132
135
|
except Exception as e:
|
|
@@ -214,13 +217,13 @@ def main():
|
|
|
214
217
|
|
|
215
218
|
# command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
|
|
216
219
|
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
217
|
-
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
220
|
+
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout, logfile_max_bytes=config_settings.logfile_max_bytes, logfile_backup_count=config_settings.logfile_backup_count)
|
|
218
221
|
command_logger = get_logger(command_output_logger = True)
|
|
219
222
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
220
223
|
|
|
221
224
|
start_msgs: List[Tuple[str, str]] = []
|
|
222
225
|
|
|
223
|
-
start_msgs.append(("
|
|
226
|
+
start_msgs.append((f"{show_scriptname()}:", about.__version__))
|
|
224
227
|
|
|
225
228
|
logger.info(f"START TIME: {start_time}")
|
|
226
229
|
logger.debug(f"Command line: {get_invocation_command_line()}")
|
|
@@ -232,11 +235,13 @@ def main():
|
|
|
232
235
|
start_msgs.append(("Config file:", args.config_file))
|
|
233
236
|
args.verbose and start_msgs.append(("Backup dir:", config_settings.backup_dir))
|
|
234
237
|
start_msgs.append(("Logfile:", config_settings.logfile_location))
|
|
238
|
+
args.verbose and start_msgs.append(("Logfile max size (bytes):", config_settings.logfile_max_bytes))
|
|
239
|
+
args.verbose and start_msgs.append(("Logfile backup count:", config_settings.logfile_backup_count))
|
|
235
240
|
args.verbose and start_msgs.append(("--alternate-archive-dir:", args.alternate_archive_dir))
|
|
236
241
|
args.verbose and start_msgs.append(("--cleanup-specific-archives:", args.cleanup_specific_archives))
|
|
237
242
|
|
|
238
243
|
dangerous_keywords = ["--cleanup", "_FULL_"] # TODO: add more dangerous keywords
|
|
239
|
-
print_aligned_settings(start_msgs, highlight_keywords=dangerous_keywords)
|
|
244
|
+
print_aligned_settings(start_msgs, highlight_keywords=dangerous_keywords, quiet=not args.verbose)
|
|
240
245
|
|
|
241
246
|
# run PREREQ scripts
|
|
242
247
|
requirements('PREREQ', config_settings)
|
|
@@ -260,7 +265,8 @@ def main():
|
|
|
260
265
|
if "_FULL_" in archive_name:
|
|
261
266
|
if not confirm_full_archive_deletion(archive_name, args.test_mode):
|
|
262
267
|
continue
|
|
263
|
-
|
|
268
|
+
archive_path = os.path.join(config_settings.backup_dir, archive_name.strip())
|
|
269
|
+
logger.info(f"Deleting archive: {archive_path}")
|
|
264
270
|
delete_archive(config_settings.backup_dir, archive_name.strip(), args)
|
|
265
271
|
elif args.list:
|
|
266
272
|
list_backups(config_settings.backup_dir, args.backup_definition)
|
dar_backup/command_runner.py
CHANGED
dar_backup/config_settings.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
|
|
1
3
|
import configparser
|
|
2
4
|
from dataclasses import dataclass, field, fields
|
|
3
5
|
from os.path import expandvars, expanduser
|
|
@@ -42,6 +44,8 @@ class ConfigSettings:
|
|
|
42
44
|
incr_age: int = field(init=False)
|
|
43
45
|
error_correction_percent: int = field(init=False)
|
|
44
46
|
par2_enabled: bool = field(init=False)
|
|
47
|
+
logfile_max_bytes: int = field(init=False)
|
|
48
|
+
logfile_no_count: int = field(init=False)
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
OPTIONAL_CONFIG_FIELDS = [
|
|
@@ -52,6 +56,20 @@ class ConfigSettings:
|
|
|
52
56
|
"type": str,
|
|
53
57
|
"default": None,
|
|
54
58
|
},
|
|
59
|
+
{
|
|
60
|
+
"section": "MISC",
|
|
61
|
+
"key": "LOGFILE_MAX_BYTES",
|
|
62
|
+
"attr": "logfile_max_bytes",
|
|
63
|
+
"type": int,
|
|
64
|
+
"default": 26214400 , # 25 MB
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"section": "MISC",
|
|
68
|
+
"key": "LOGFILE_BACKUP_COUNT",
|
|
69
|
+
"attr": "logfile_backup_count",
|
|
70
|
+
"type": int,
|
|
71
|
+
"default": 5,
|
|
72
|
+
},
|
|
55
73
|
# Add more optional fields here
|
|
56
74
|
]
|
|
57
75
|
|
dar_backup/dar-backup.conf
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
|
|
1
3
|
# This config file is intended to demo `dar-backup`.
|
|
2
4
|
#
|
|
3
|
-
# The `
|
|
5
|
+
# The `demo` application puts this file in ~/.config/dar-backup/dar-backup.conf
|
|
4
6
|
|
|
5
7
|
[MISC]
|
|
6
8
|
LOGFILE_LOCATION = ~/dar-backup/dar-backup.log
|
|
@@ -13,7 +15,7 @@ NO_FILES_VERIFICATION = 5
|
|
|
13
15
|
COMMAND_TIMEOUT_SECS = 86400
|
|
14
16
|
|
|
15
17
|
[DIRECTORIES]
|
|
16
|
-
BACKUP_DIR =
|
|
18
|
+
BACKUP_DIR = @@BACKUP_DIR@@
|
|
17
19
|
BACKUP.D_DIR = ~/.config/dar-backup/backup.d/
|
|
18
20
|
TEST_RESTORE_DIR = ~/dar-backup/restore/
|
|
19
21
|
# Optional parameter
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
# ------------------------------------------------------------------------
|
|
5
|
+
# Demo of a `dar-backup` configuration file
|
|
6
|
+
# This file was generated by dar-backup's `demo` program.
|
|
7
|
+
#
|
|
8
|
+
{%- if opts_dict | length > 0 %}
|
|
9
|
+
# Options given to the `demo` program:
|
|
10
|
+
{% endif %}
|
|
11
|
+
{%- if opts_dict.ROOT_DIR -%}
|
|
12
|
+
# --root-dir : {{ opts_dict.ROOT_DIR }}
|
|
13
|
+
{% endif %}
|
|
14
|
+
{%- if opts_dict.DIR_TO_BACKUP -%}
|
|
15
|
+
# --dir-to-backup : {{ opts_dict.DIR_TO_BACKUP }}
|
|
16
|
+
{% endif -%}
|
|
17
|
+
{%- if opts_dict.BACKUP_DIR -%}
|
|
18
|
+
# --backup-dir : {{ opts_dict.BACKUP_DIR }}
|
|
19
|
+
{% endif %}
|
|
20
|
+
#
|
|
21
|
+
# Variables used to generate this file:
|
|
22
|
+
# =====================================
|
|
23
|
+
{% for k,v in vars_map|dictsort %}# {{ k }} : {{ v }}
|
|
24
|
+
{% endfor -%}
|
|
25
|
+
# ------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
[MISC]
|
|
28
|
+
LOGFILE_LOCATION = {{ vars_map.DAR_BACKUP_DIR -}}/dar-backup.log
|
|
29
|
+
# optional parameters
|
|
30
|
+
# LOGFILE_MAX_BYTES = 26214400 # 25 MB default, change as neeeded
|
|
31
|
+
# LOGFILE_BACKUP_COUNT = 5 # default, change as needed
|
|
32
|
+
|
|
33
|
+
MAX_SIZE_VERIFICATION_MB = 2
|
|
34
|
+
MIN_SIZE_VERIFICATION_MB = 0
|
|
35
|
+
NO_FILES_VERIFICATION = 1
|
|
36
|
+
# timeout in seconds for backup, test, restore and par2 operations
|
|
37
|
+
# The author has such `dar` tasks running for 10-15 hours on the yearly backups, so a value of 24 hours is used.
|
|
38
|
+
# If a timeout is not specified when using the CommandRunner, a default timeout of 30 secs is used.
|
|
39
|
+
COMMAND_TIMEOUT_SECS = 86400
|
|
40
|
+
|
|
41
|
+
[DIRECTORIES]
|
|
42
|
+
BACKUP_DIR = {{ vars_map.BACKUP_DIR }}
|
|
43
|
+
BACKUP.D_DIR = {{ vars_map.BACKUP_D_DIR }}
|
|
44
|
+
TEST_RESTORE_DIR = {{ vars_map.TEST_RESTORE_DIR }}
|
|
45
|
+
# Optional parameter
|
|
46
|
+
# If you want to store the catalog database away from the BACKUP_DIR, use the MANAGER_DB_DIR variable.
|
|
47
|
+
#MANAGER_DB_DIR = /some/where/else/
|
|
48
|
+
|
|
49
|
+
[AGE]
|
|
50
|
+
# DIFF and INCR backups are kept for a configured number of days, then deleted by the `cleanuo`
|
|
51
|
+
# age settings are in days
|
|
52
|
+
DIFF_AGE = 100
|
|
53
|
+
INCR_AGE = 40
|
|
54
|
+
|
|
55
|
+
[PAR2]
|
|
56
|
+
ERROR_CORRECTION_PERCENT = 5
|
|
57
|
+
ENABLED = True
|
|
58
|
+
|
|
59
|
+
[PREREQ]
|
|
60
|
+
#SCRIPT_1 = <pre-script 1>
|
|
61
|
+
|
|
62
|
+
[POSTREQ]
|
|
63
|
+
#SCRIPT_1 = <post-script 1>
|
|
64
|
+
|
dar_backup/dar_backup.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
3
|
"""
|
|
4
4
|
installer.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/installer.py
|
|
5
5
|
This script is part of dar-backup, a backup solution for Linux using dar and systemd.
|
|
@@ -55,6 +55,8 @@ from dar_backup.util import get_invocation_command_line
|
|
|
55
55
|
from dar_backup.util import get_binary_info
|
|
56
56
|
from dar_backup.util import print_aligned_settings
|
|
57
57
|
from dar_backup.util import backup_definition_completer, list_archive_completer
|
|
58
|
+
from dar_backup.util import show_scriptname
|
|
59
|
+
from dar_backup.util import print_debug
|
|
58
60
|
|
|
59
61
|
from dar_backup.command_runner import CommandRunner
|
|
60
62
|
from dar_backup.command_runner import CommandResult
|
|
@@ -92,6 +94,7 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
|
|
|
92
94
|
Returns:
|
|
93
95
|
List of tuples (<msg>, <exit_code>) of errors not considered critical enough for raising an exception
|
|
94
96
|
"""
|
|
97
|
+
|
|
95
98
|
result: List[tuple] = []
|
|
96
99
|
|
|
97
100
|
logger.info(f"===> Starting {type} backup for {backup_definition}")
|
|
@@ -105,7 +108,6 @@ def generic_backup(type: str, command: List[str], backup_file: str, backup_defin
|
|
|
105
108
|
stop_event = Event()
|
|
106
109
|
session_marker = f"=== START BACKUP SESSION: {int(time())} ==="
|
|
107
110
|
get_logger(command_output_logger=True).info(session_marker)
|
|
108
|
-
|
|
109
111
|
progress_thread = threading.Thread(
|
|
110
112
|
target=show_log_driven_bar,
|
|
111
113
|
args=(log_path, stop_event, session_marker),
|
|
@@ -534,12 +536,12 @@ def perform_backup(args: argparse.Namespace, config_settings: ConfigSettings, ba
|
|
|
534
536
|
|
|
535
537
|
# Perform backup
|
|
536
538
|
backup_result = generic_backup(backup_type, command, backup_file, backup_definition_path, args.darrc, config_settings, args)
|
|
539
|
+
|
|
537
540
|
if not isinstance(backup_result, list) or not all(isinstance(i, tuple) and len(i) == 2 for i in backup_result):
|
|
538
541
|
logger.error("Unexpected return format from generic_backup")
|
|
539
542
|
backup_result = [("Unexpected return format from generic_backup", 1)]
|
|
540
543
|
|
|
541
544
|
results.extend(backup_result)
|
|
542
|
-
|
|
543
545
|
logger.info("Starting verification...")
|
|
544
546
|
verify_result = verify(args, backup_file, backup_definition_path, config_settings)
|
|
545
547
|
if verify_result:
|
|
@@ -838,7 +840,7 @@ def main():
|
|
|
838
840
|
if command_output_log == config_settings.logfile_location:
|
|
839
841
|
print(f"Error: logfile_location in {args.config_file} does not end at 'dar-backup.log', exiting", file=stderr)
|
|
840
842
|
|
|
841
|
-
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
843
|
+
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout, logfile_max_bytes=config_settings.logfile_max_bytes, logfile_backup_count=config_settings.logfile_backup_count)
|
|
842
844
|
command_logger = get_logger(command_output_logger = True)
|
|
843
845
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
844
846
|
|
|
@@ -863,11 +865,11 @@ def main():
|
|
|
863
865
|
start_msgs: List[Tuple[str, str]] = []
|
|
864
866
|
|
|
865
867
|
start_time=int(time())
|
|
866
|
-
start_msgs.append((
|
|
868
|
+
start_msgs.append((f"{show_scriptname()}:", about.__version__))
|
|
867
869
|
logger.info(f"START TIME: {start_time}")
|
|
868
|
-
logger.debug(f"Command line
|
|
869
|
-
logger.debug(f"
|
|
870
|
-
logger.debug(f"
|
|
870
|
+
logger.debug(f"Command line:\n{get_invocation_command_line()}")
|
|
871
|
+
logger.debug(f"`Args`:\n{args}")
|
|
872
|
+
logger.debug(f"`Config_settings`:\n{config_settings}")
|
|
871
873
|
dar_properties = get_binary_info(command='dar')
|
|
872
874
|
start_msgs.append(('dar path:', dar_properties['path']))
|
|
873
875
|
start_msgs.append(('dar version:', dar_properties['version']))
|
|
@@ -877,9 +879,9 @@ def main():
|
|
|
877
879
|
start_msgs.append(('Config file:', os.path.abspath(args.config_file)))
|
|
878
880
|
start_msgs.append((".darrc location:", args.darrc))
|
|
879
881
|
|
|
880
|
-
args.
|
|
881
|
-
args.
|
|
882
|
-
args.
|
|
882
|
+
args.full_backup and start_msgs.append(("Type of backup:", "FULL"))
|
|
883
|
+
args.differential_backup and start_msgs.append(("Type of backup:", "DIFF"))
|
|
884
|
+
args.incremental_backup and start_msgs.append(("Type of backup:", "INCR"))
|
|
883
885
|
args.verbose and args.backup_definition and start_msgs.append(("Backup definition:", args.backup_definition))
|
|
884
886
|
if args.alternate_reference_archive:
|
|
885
887
|
args.verbose and start_msgs.append(("Alternate ref archive:", args.alternate_reference_archive))
|
|
@@ -890,11 +892,14 @@ def main():
|
|
|
890
892
|
args.verbose and start_msgs.append(("Restore dir:", restore_dir))
|
|
891
893
|
|
|
892
894
|
args.verbose and start_msgs.append(("Logfile location:", config_settings.logfile_location))
|
|
895
|
+
args.verbose and start_msgs.append(("Logfile max size (bytes):", config_settings.logfile_max_bytes))
|
|
896
|
+
args.verbose and start_msgs.append(("Logfile backup count:", config_settings.logfile_backup_count))
|
|
897
|
+
|
|
893
898
|
args.verbose and start_msgs.append(("PAR2 enabled:", config_settings.par2_enabled))
|
|
894
899
|
args.verbose and start_msgs.append(("--do-not-compare:", args.do_not_compare))
|
|
895
900
|
|
|
896
|
-
|
|
897
|
-
print_aligned_settings(start_msgs)
|
|
901
|
+
highlight_keywords = ["--do-not", "alternate"] # TODO: add more dangerous keywords
|
|
902
|
+
print_aligned_settings(start_msgs, quiet=not args.verbose, highlight_keywords=highlight_keywords)
|
|
898
903
|
|
|
899
904
|
# sanity check
|
|
900
905
|
if args.backup_definition and not os.path.exists(os.path.join(config_settings.backup_d_dir, args.backup_definition)):
|
|
@@ -928,6 +933,7 @@ def main():
|
|
|
928
933
|
|
|
929
934
|
requirements('POSTREQ', config_settings)
|
|
930
935
|
|
|
936
|
+
|
|
931
937
|
except Exception as e:
|
|
932
938
|
logger.error("Exception details:", exc_info=True)
|
|
933
939
|
results.append((repr(e), 1))
|
dar_backup/dar_backup_systemd.py
CHANGED
dar_backup/demo.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
2
4
|
"""
|
|
3
|
-
|
|
5
|
+
demo.py source code is here: https://github.com/per2jensen/dar-backup/tree/main/v2/src/dar_backup/demo.py
|
|
4
6
|
This script is part of dar-backup, a backup solution for Linux using dar and systemd.
|
|
5
7
|
|
|
6
8
|
Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
@@ -10,7 +12,9 @@ not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
10
12
|
See section 15 and section 16 in the supplied "LICENSE" file
|
|
11
13
|
|
|
12
14
|
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.
|
|
15
|
+
It is non-destructive and will not overwrite any existing files or directories under --override is used.
|
|
16
|
+
|
|
17
|
+
User can set ROOT_DIR, DIR_TO_BACKUP and BACKUP_DIR (destination for backups) via optins to override defaults.
|
|
14
18
|
"""
|
|
15
19
|
|
|
16
20
|
import argparse
|
|
@@ -19,117 +23,189 @@ import shutil
|
|
|
19
23
|
import sys
|
|
20
24
|
|
|
21
25
|
from . import __about__ as about
|
|
26
|
+
from . import util
|
|
22
27
|
|
|
28
|
+
from jinja2 import Environment, FileSystemLoader
|
|
23
29
|
from pathlib import Path
|
|
30
|
+
from typing import Dict, Tuple
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
CONFIG_DIR = util.normalize_dir(util.expand_path("~/.config/dar-backup"))
|
|
34
|
+
DAR_BACKUP_DIR = util.normalize_dir(util.expand_path("~/dar-backup"))
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
def check_directories(args, vars_map: Dict[str,str]) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Check if the directories exist and create them if they don't.
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
Returns:
|
|
43
|
+
bool: True if the directories were created successfully, False otherwise.
|
|
44
|
+
"""
|
|
45
|
+
result = True
|
|
46
|
+
for key in ("DAR_BACKUP_DIR","BACKUP_DIR","TEST_RESTORE_DIR","CONFIG_DIR","BACKUP_D_DIR"):
|
|
47
|
+
path = Path(vars_map[key])
|
|
48
|
+
if path.exists() and not args.override:
|
|
49
|
+
print(f"Directory '{path}' already exists")
|
|
50
|
+
result = False
|
|
51
|
+
return result
|
|
33
52
|
|
|
34
|
-
# Switch to ordered selection mode, which means that the following options
|
|
35
|
-
# will be considered top to bottom
|
|
36
|
-
-am
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
def generate_file(args, template: str, file_path: Path, vars_map: Dict[str, str], opts_dict: Dict[str, str]) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Generate a file using a Jinja2 template.
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
Args:
|
|
59
|
+
args: Command line arguments.
|
|
60
|
+
template (str): The name of the template file.
|
|
61
|
+
file_path (Path): The path where the generated file will be saved.
|
|
62
|
+
vars_map (Dict[str, str]): A dictionary containing variables for the template.
|
|
63
|
+
opts_dict (Dict[str, str]): A dictionary containing options given by user.
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
Returns:
|
|
66
|
+
bool: True if the file was generated successfully, False otherwise.
|
|
67
|
+
"""
|
|
68
|
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
69
|
+
env = Environment(loader=FileSystemLoader(current_script_dir))
|
|
70
|
+
tpl = env.get_template(template)
|
|
71
|
+
rendered = tpl.render(vars_map = vars_map, opts_dict = opts_dict)
|
|
72
|
+
if rendered is None:
|
|
73
|
+
print(f"Error: Template '{template}' could not be rendered.")
|
|
74
|
+
return False
|
|
75
|
+
if os.path.exists(file_path) and not args.override:
|
|
76
|
+
print(f"Error: File '{file_path}' already exists. Use --override to overwrite.")
|
|
77
|
+
return False
|
|
78
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
file_path.write_text(rendered)
|
|
80
|
+
print(f"File generated at '{file_path}'")
|
|
48
81
|
|
|
49
|
-
# compression level
|
|
50
|
-
-z5
|
|
51
82
|
|
|
52
|
-
# no overwrite, if you rerun a backup, 'dar' halts and asks what to do
|
|
53
|
-
-n
|
|
54
|
-
|
|
55
|
-
# size of each slice in the archive
|
|
56
|
-
--slice 10G
|
|
57
83
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
def setup_dicts(args, vars_map: Dict[str, str]) -> Tuple[Dict[str, str], Dict[str, str]]:
|
|
85
|
+
"""
|
|
86
|
+
Override various entries in the dictionaries for jinja templating with user input.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tuple[Dict[str, str], Dict[str, str]]: A tuple containing the vars_map and opts_dict dictionaries.
|
|
90
|
+
"""
|
|
91
|
+
opts_dict = {}
|
|
92
|
+
if args.root_dir:
|
|
93
|
+
opts_dict["ROOT_DIR"] = args.root_dir
|
|
94
|
+
if args.dir_to_backup:
|
|
95
|
+
opts_dict["DIR_TO_BACKUP"] = args.dir_to_backup
|
|
96
|
+
if args.backup_dir:
|
|
97
|
+
opts_dict["BACKUP_DIR"] = args.backup_dir
|
|
98
|
+
|
|
99
|
+
for key, value in opts_dict.items():
|
|
100
|
+
vars_map[key] = value
|
|
101
|
+
|
|
102
|
+
return vars_map, opts_dict
|
|
62
103
|
|
|
63
104
|
|
|
64
105
|
def main():
|
|
65
106
|
parser = argparse.ArgumentParser(
|
|
66
|
-
description="Set up `dar-backup` on your system.",
|
|
107
|
+
description="Set up demo configuration for `dar-backup` on your system.",
|
|
67
108
|
)
|
|
68
109
|
parser.add_argument(
|
|
69
110
|
"-i", "--install",
|
|
70
111
|
action="store_true",
|
|
71
|
-
help="Deploy
|
|
112
|
+
help="Deploy demo config files and directories. Will not overwrite existing files or directories unless --override is used."
|
|
113
|
+
)
|
|
114
|
+
req = parser.add_argument_group(
|
|
115
|
+
'These options must be used together'
|
|
116
|
+
)
|
|
117
|
+
req.add_argument(
|
|
118
|
+
"--root-dir",
|
|
119
|
+
type=str,
|
|
120
|
+
help="Specify the root directory for the backup."
|
|
121
|
+
)
|
|
122
|
+
req.add_argument(
|
|
123
|
+
"--dir-to-backup",
|
|
124
|
+
type=str,
|
|
125
|
+
help="Directory to backup, relative to the root directory."
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--backup-dir",
|
|
129
|
+
type=str,
|
|
130
|
+
help="Directory where backups and redundancy files are put"
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--override",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="By default, the script will not overwrite existing files or directories. Use this option to override this behavior."
|
|
72
136
|
)
|
|
73
137
|
parser.add_argument(
|
|
74
138
|
"-v", "--version",
|
|
75
139
|
action="version",
|
|
76
140
|
version=f"%(prog)s version {about.__version__}, {about.__license__}"
|
|
77
141
|
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"-g", "--generate",
|
|
144
|
+
action="store_true",
|
|
145
|
+
help="Generate config files and put them in /tmp/."
|
|
146
|
+
)
|
|
78
147
|
|
|
79
148
|
args = parser.parse_args()
|
|
80
149
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
150
|
+
group = [args.root_dir, args.dir_to_backup]
|
|
151
|
+
if any(group) and not all(group):
|
|
152
|
+
parser.error(
|
|
153
|
+
"Options --root-dir, --dir-to-backup must all be specified together."
|
|
154
|
+
)
|
|
155
|
+
exit(1)
|
|
156
|
+
|
|
157
|
+
args.root_dir = util.normalize_dir(util.expand_path(args.root_dir)) if args.root_dir else None
|
|
158
|
+
args.backup_dir = util.normalize_dir(util.expand_path(args.backup_dir)) if args.backup_dir else None
|
|
159
|
+
args.dir_to_backup = util.normalize_dir(util.expand_path(args.dir_to_backup)) if args.dir_to_backup else None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
vars_map = {
|
|
164
|
+
# dar-backup.conf variables
|
|
165
|
+
"CONFIG_DIR" : CONFIG_DIR,
|
|
166
|
+
"DAR_BACKUP_DIR" : DAR_BACKUP_DIR,
|
|
167
|
+
"BACKUP_DIR" : os.path.join(DAR_BACKUP_DIR, "backups"),
|
|
168
|
+
"BACKUP_D_DIR" : os.path.join(CONFIG_DIR, "backup.d"),
|
|
169
|
+
"TEST_RESTORE_DIR" : os.path.join(DAR_BACKUP_DIR, "restore"),
|
|
170
|
+
# backup definition variables
|
|
171
|
+
"ROOT_DIR" : util.normalize_dir(util.expand_path("$HOME")),
|
|
172
|
+
"DIR_TO_BACKUP" : ".config/dar-backup",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
vars_map, opts_dict = setup_dicts(args, vars_map)
|
|
177
|
+
|
|
178
|
+
if args.generate:
|
|
179
|
+
print("Generating backup definition file...")
|
|
180
|
+
vars_map["DAR_BACKUP_DIR"] = "/tmp"
|
|
181
|
+
args.override = True
|
|
182
|
+
generate_file(args, "demo_backup_def.j2", Path("/tmp/dar-backup/backup.d/demo"), vars_map, opts_dict)
|
|
183
|
+
vars_map["CONFIG_DIR"] = "/tmp"
|
|
184
|
+
generate_file(args, "dar-backup.conf.j2", Path("/tmp/dar-backup.conf"), vars_map, opts_dict)
|
|
185
|
+
elif args.install:
|
|
186
|
+
if not check_directories(args, vars_map):
|
|
187
|
+
print("Error: One or more directories already exist.\nSpecify non-existent directories or use --override to overwrite.")
|
|
91
188
|
sys.exit(1)
|
|
92
189
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
source_file = script_dir / "dar-backup.conf"
|
|
103
|
-
destination_file = Path(CONFIG_DIR) / "dar-backup.conf"
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
shutil.copy2(source_file, destination_file)
|
|
107
|
-
print(f"Config file deployed to {destination_file}")
|
|
108
|
-
except Exception as e:
|
|
109
|
-
print(f"Error: Could not copy config file: {e}")
|
|
110
|
-
sys.exit(1)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
backup_definition = BACKUP_DEFINITION.replace("@@HOME_DIR@@", os.path.expanduser("~"))
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
with open(os.path.join(CONFIG_DIR, "backup.d", "default"), "w") as f:
|
|
117
|
-
f.write(backup_definition)
|
|
118
|
-
print(f"Default backup definition file deployed to {os.path.join(CONFIG_DIR, 'backup.d', 'default')}")
|
|
119
|
-
except Exception as e:
|
|
120
|
-
print(f"Error: Could not write default backup definition: {e}")
|
|
121
|
-
sys.exit(1)
|
|
122
|
-
except Exception as e:
|
|
123
|
-
print(f"Installation failed: {e}")
|
|
124
|
-
sys.exit(1)
|
|
190
|
+
Path(vars_map["DAR_BACKUP_DIR"]).mkdir(parents=True, exist_ok=True)
|
|
191
|
+
Path(vars_map["BACKUP_DIR"]).mkdir(parents=True, exist_ok=True)
|
|
192
|
+
Path(vars_map["TEST_RESTORE_DIR"]).mkdir(parents=True, exist_ok=True)
|
|
193
|
+
Path(vars_map["CONFIG_DIR"]).mkdir(parents=True, exist_ok=True)
|
|
194
|
+
Path(vars_map["BACKUP_D_DIR"]).mkdir(parents=True, exist_ok=True)
|
|
195
|
+
print(f"Directories created.")
|
|
196
|
+
|
|
197
|
+
generate_file(args, "demo_backup_def.j2", Path(vars_map["BACKUP_D_DIR"]).joinpath("demo"), vars_map, opts_dict)
|
|
198
|
+
generate_file(args, "dar-backup.conf.j2", Path(vars_map["CONFIG_DIR"]).joinpath("dar-backup.conf"), vars_map, opts_dict)
|
|
125
199
|
|
|
126
|
-
print("1. Now run `manager --create` to create the catalog database.")
|
|
200
|
+
print("1. Now run `manager --create-db` to create the catalog database.")
|
|
127
201
|
print("2. Then you can run `dar-backup --full-backup` to create a backup.")
|
|
128
202
|
print("3. List backups with `dar-backup --list`")
|
|
129
203
|
print("4. List contents of a backup with `dar-backup --list-contents <backup-name>`")
|
|
204
|
+
else:
|
|
205
|
+
parser.print_help()
|
|
206
|
+
sys.exit(1)
|
|
130
207
|
|
|
131
208
|
sys.exit(0)
|
|
132
209
|
|
|
133
|
-
|
|
134
210
|
if __name__ == "__main__":
|
|
135
211
|
main()
|