dar-backup 0.6.19__py3-none-any.whl → 0.6.20__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 +16 -0
- dar_backup/__about__.py +1 -1
- dar_backup/cleanup.py +22 -22
- dar_backup/config_settings.py +83 -53
- dar_backup/dar-backup.conf +4 -0
- dar_backup/dar_backup.py +38 -32
- dar_backup/demo.py +138 -0
- dar_backup/exceptions.py +3 -0
- dar_backup/installer.py +49 -129
- dar_backup/manager.py +55 -28
- dar_backup/util.py +100 -3
- {dar_backup-0.6.19.dist-info → dar_backup-0.6.20.dist-info}/METADATA +1 -1
- dar_backup-0.6.20.dist-info/RECORD +23 -0
- {dar_backup-0.6.19.dist-info → dar_backup-0.6.20.dist-info}/entry_points.txt +1 -1
- dar_backup-0.6.19.dist-info/RECORD +0 -21
- {dar_backup-0.6.19.dist-info → dar_backup-0.6.20.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.19.dist-info → dar_backup-0.6.20.dist-info}/licenses/LICENSE +0 -0
dar_backup/Changelog.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
<!-- markdownlint-disable MD024 -->
|
|
2
2
|
# dar-backup Changelog
|
|
3
3
|
|
|
4
|
+
## v2-beta-0.6.20 - 2025-05-03
|
|
5
|
+
|
|
6
|
+
Github link: [v2-beta-0.6.20](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.6.20/v2)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- show_version() moved to util and tests for dar-backup, manager and cleanup
|
|
11
|
+
- startup informational messages now works the same across the scripts
|
|
12
|
+
- Improved ConfigSettings class to handle optional configuration keys
|
|
13
|
+
|
|
14
|
+
-- test cases added
|
|
15
|
+
|
|
16
|
+
- Optional config parameter: MANAGER_DB_DIR, ideally to point to another disk for safe keeping backup catalogs
|
|
17
|
+
|
|
18
|
+
-- test cases added
|
|
19
|
+
|
|
4
20
|
## v2-beta-0.6.19 - 2025-04-21
|
|
5
21
|
|
|
6
22
|
Github link: [v2-beta-0.6.19](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.6.19/v2)
|
dar_backup/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.6.
|
|
1
|
+
__version__ = "0.6.20"
|
dar_backup/cleanup.py
CHANGED
|
@@ -20,11 +20,15 @@ import os
|
|
|
20
20
|
import re
|
|
21
21
|
import subprocess
|
|
22
22
|
import sys
|
|
23
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
|
24
|
+
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
from datetime import datetime, timedelta
|
|
25
28
|
from inputimeout import inputimeout, TimeoutOccurred
|
|
26
29
|
from time import time
|
|
27
|
-
from typing import Dict, List, NamedTuple
|
|
30
|
+
from typing import Dict, List, NamedTuple, Tuple
|
|
31
|
+
|
|
28
32
|
|
|
29
33
|
from . import __about__ as about
|
|
30
34
|
from dar_backup.config_settings import ConfigSettings
|
|
@@ -32,6 +36,8 @@ from dar_backup.util import list_backups
|
|
|
32
36
|
from dar_backup.util import setup_logging
|
|
33
37
|
from dar_backup.util import get_logger
|
|
34
38
|
from dar_backup.util import requirements
|
|
39
|
+
from dar_backup.util import show_version
|
|
40
|
+
from dar_backup.util import print_aligned_settings
|
|
35
41
|
from dar_backup.util import backup_definition_completer, list_archive_completer
|
|
36
42
|
|
|
37
43
|
from dar_backup.command_runner import CommandRunner
|
|
@@ -152,21 +158,12 @@ def delete_catalog(catalog_name: str, args: NamedTuple) -> bool:
|
|
|
152
158
|
return False
|
|
153
159
|
|
|
154
160
|
|
|
155
|
-
def show_version():
|
|
156
|
-
script_name = os.path.basename(sys.argv[0])
|
|
157
|
-
print(f"{script_name} {about.__version__}")
|
|
158
|
-
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
159
|
-
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
160
|
-
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
161
|
-
|
|
162
|
-
|
|
163
161
|
def confirm_full_archive_deletion(archive_name: str, test_mode=False) -> bool:
|
|
164
162
|
try:
|
|
165
163
|
if test_mode:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
print(f"Simulated confirmation for FULL archive: {confirmation}")
|
|
164
|
+
answer = os.getenv("CLEANUP_TEST_DELETE_FULL", "").lower()
|
|
165
|
+
print(f"Simulated confirmation for FULL archive '{archive_name}': {answer}")
|
|
166
|
+
return answer == "yes"
|
|
170
167
|
else:
|
|
171
168
|
confirmation = inputimeout(
|
|
172
169
|
prompt=f"Are you sure you want to delete the FULL archive '{archive_name}'? (yes/no): ",
|
|
@@ -220,21 +217,24 @@ def main():
|
|
|
220
217
|
command_logger = get_logger(command_output_logger = True)
|
|
221
218
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
222
219
|
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
start_msgs: List[Tuple[str, str]] = []
|
|
221
|
+
|
|
222
|
+
start_msgs.append(("cleanup.py:", about.__version__))
|
|
225
223
|
|
|
226
224
|
logger.info(f"START TIME: {start_time}")
|
|
227
225
|
logger.debug(f"`args`:\n{args}")
|
|
228
226
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
229
227
|
|
|
230
228
|
file_dir = os.path.normpath(os.path.dirname(__file__))
|
|
231
|
-
args.verbose and (
|
|
232
|
-
|
|
233
|
-
args.verbose and (
|
|
234
|
-
|
|
235
|
-
args.verbose and (
|
|
236
|
-
args.verbose and (
|
|
237
|
-
|
|
229
|
+
args.verbose and start_msgs.append(("Script directory:", file_dir))
|
|
230
|
+
start_msgs.append(("Config file:", args.config_file))
|
|
231
|
+
args.verbose and start_msgs.append(("Backup dir:", config_settings.backup_dir))
|
|
232
|
+
start_msgs.append(("Logfile:", config_settings.logfile_location))
|
|
233
|
+
args.verbose and start_msgs.append(("--alternate-archive-dir:", args.alternate_archive_dir))
|
|
234
|
+
args.verbose and start_msgs.append(("--cleanup-specific-archives:", args.cleanup_specific_archives))
|
|
235
|
+
|
|
236
|
+
dangerous_keywords = ["--cleanup", "_FULL_"] # TODO: add more dangerous keywords
|
|
237
|
+
print_aligned_settings(start_msgs, highlight_keywords=dangerous_keywords)
|
|
238
238
|
|
|
239
239
|
# run PREREQ scripts
|
|
240
240
|
requirements('PREREQ', config_settings)
|
dar_backup/config_settings.py
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
|
-
|
|
2
1
|
import configparser
|
|
3
|
-
import logging
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
2
|
from dataclasses import dataclass, field, fields
|
|
7
3
|
from os.path import expandvars, expanduser
|
|
8
4
|
from pathlib import Path
|
|
9
5
|
|
|
6
|
+
from dar_backup.exceptions import ConfigSettingsError
|
|
7
|
+
|
|
10
8
|
@dataclass
|
|
11
9
|
class ConfigSettings:
|
|
12
10
|
"""
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
Parses and holds configuration values from a dar-backup.conf file.
|
|
12
|
+
|
|
13
|
+
Required fields are defined as dataclass attributes and must be present in the config file.
|
|
14
|
+
Optional fields can be declared in the OPTIONAL_CONFIG_FIELDS list. If a key is present in the
|
|
15
|
+
config file, the field is set to its parsed value; otherwise, it defaults to None.
|
|
16
|
+
|
|
17
|
+
The __repr__ method will only include optional fields if their value is not None,
|
|
18
|
+
keeping debug output clean and focused on explicitly configured values.
|
|
19
|
+
|
|
20
|
+
OPTIONAL_CONFIG_FIELDS = [
|
|
21
|
+
{
|
|
22
|
+
"section": "DIRECTORIES",
|
|
23
|
+
"key": "MANAGER_DB_DIR",
|
|
24
|
+
"attr": "manager_db_dir",
|
|
25
|
+
"type": str,
|
|
26
|
+
"default": None,
|
|
27
|
+
}
|
|
28
|
+
]
|
|
28
29
|
"""
|
|
29
30
|
|
|
30
31
|
config_file: str
|
|
32
|
+
|
|
31
33
|
logfile_location: str = field(init=False)
|
|
32
34
|
max_size_verification_mb: int = field(init=False)
|
|
33
35
|
min_size_verification_mb: int = field(init=False)
|
|
@@ -41,17 +43,28 @@ class ConfigSettings:
|
|
|
41
43
|
error_correction_percent: int = field(init=False)
|
|
42
44
|
par2_enabled: bool = field(init=False)
|
|
43
45
|
|
|
46
|
+
|
|
47
|
+
OPTIONAL_CONFIG_FIELDS = [
|
|
48
|
+
{
|
|
49
|
+
"section": "DIRECTORIES",
|
|
50
|
+
"key": "MANAGER_DB_DIR",
|
|
51
|
+
"attr": "manager_db_dir",
|
|
52
|
+
"type": str,
|
|
53
|
+
"default": None,
|
|
54
|
+
},
|
|
55
|
+
# Add more optional fields here
|
|
56
|
+
]
|
|
57
|
+
|
|
44
58
|
def __post_init__(self):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
if self.config_file is None:
|
|
50
|
-
raise ValueError("`config_file` must be specified.")
|
|
51
|
-
|
|
52
|
-
self.config = configparser.ConfigParser()
|
|
59
|
+
if not self.config_file:
|
|
60
|
+
raise ConfigSettingsError("`config_file` must be specified.")
|
|
61
|
+
|
|
53
62
|
try:
|
|
54
|
-
self.config.
|
|
63
|
+
self.config = configparser.ConfigParser()
|
|
64
|
+
loaded_files = self.config.read(self.config_file)
|
|
65
|
+
if not loaded_files:
|
|
66
|
+
raise RuntimeError(f"Configuration file not found or unreadable: '{self.config_file}'")
|
|
67
|
+
|
|
55
68
|
self.logfile_location = self.config['MISC']['LOGFILE_LOCATION']
|
|
56
69
|
self.max_size_verification_mb = int(self.config['MISC']['MAX_SIZE_VERIFICATION_MB'])
|
|
57
70
|
self.min_size_verification_mb = int(self.config['MISC']['MIN_SIZE_VERIFICATION_MB'])
|
|
@@ -63,37 +76,54 @@ class ConfigSettings:
|
|
|
63
76
|
self.diff_age = int(self.config['AGE']['DIFF_AGE'])
|
|
64
77
|
self.incr_age = int(self.config['AGE']['INCR_AGE'])
|
|
65
78
|
self.error_correction_percent = int(self.config['PAR2']['ERROR_CORRECTION_PERCENT'])
|
|
79
|
+
|
|
66
80
|
val = self.config['PAR2']['ENABLED'].strip().lower()
|
|
67
81
|
if val in ('true', '1', 'yes'):
|
|
68
82
|
self.par2_enabled = True
|
|
69
83
|
elif val in ('false', '0', 'no'):
|
|
70
84
|
self.par2_enabled = False
|
|
71
85
|
else:
|
|
72
|
-
raise
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
raise ConfigSettingsError(f"Invalid boolean value for 'ENABLED' in [PAR2]: '{val}'")
|
|
87
|
+
|
|
88
|
+
# Load optional fields
|
|
89
|
+
for opt in self.OPTIONAL_CONFIG_FIELDS:
|
|
90
|
+
if self.config.has_option(opt['section'], opt['key']):
|
|
91
|
+
raw_value = self.config.get(opt['section'], opt['key'])
|
|
92
|
+
try:
|
|
93
|
+
value = opt['type'](raw_value.strip())
|
|
94
|
+
setattr(self, opt['attr'], value)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise ConfigSettingsError(
|
|
97
|
+
f"Failed to parse optional config '{opt['section']}::{opt['key']}': {e}"
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
setattr(self, opt['attr'], opt.get('default', None))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Expand paths in all string fields that exist
|
|
80
104
|
for field in fields(self):
|
|
81
|
-
if
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
except PermissionError as e:
|
|
89
|
-
logging.error(f"Permission error while reading config file {self.config_file}")
|
|
90
|
-
logging.error(f"Error details: {e}")
|
|
91
|
-
sys.exit("Error: Permission error while reading config file.")
|
|
105
|
+
if hasattr(self, field.name):
|
|
106
|
+
value = getattr(self, field.name)
|
|
107
|
+
if isinstance(value, str):
|
|
108
|
+
setattr(self, field.name, expanduser(expandvars(value)))
|
|
109
|
+
|
|
110
|
+
except RuntimeError as e:
|
|
111
|
+
raise ConfigSettingsError(f"RuntimeError: {e}")
|
|
92
112
|
except KeyError as e:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
raise ConfigSettingsError(f"Missing mandatory configuration key: {e}")
|
|
114
|
+
except ValueError as e:
|
|
115
|
+
raise ConfigSettingsError(f"Invalid value in config: {e}")
|
|
96
116
|
except Exception as e:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
raise ConfigSettingsError(f"Unexpected error during config initialization: {e}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
safe_fields = [
|
|
123
|
+
f"{field.name}={getattr(self, field.name)!r}"
|
|
124
|
+
for field in fields(self)
|
|
125
|
+
if hasattr(self, field.name) and getattr(self, field.name) is not None
|
|
126
|
+
]
|
|
127
|
+
return f"<ConfigSettings({', '.join(safe_fields)})>"
|
|
128
|
+
|
|
129
|
+
|
dar_backup/dar-backup.conf
CHANGED
|
@@ -16,8 +16,12 @@ COMMAND_TIMEOUT_SECS = 86400
|
|
|
16
16
|
BACKUP_DIR = ~/dar-backup/backups
|
|
17
17
|
BACKUP.D_DIR = ~/.config/dar-backup/backup.d/
|
|
18
18
|
TEST_RESTORE_DIR = ~/dar-backup/restore/
|
|
19
|
+
# Optional parameter
|
|
20
|
+
# If you want to store the catalog database away from the BACKUP_DIR, use the MANAGER_DB_DIR variable.
|
|
21
|
+
#MANAGER_DB_DIR = /some/where/else/
|
|
19
22
|
|
|
20
23
|
[AGE]
|
|
24
|
+
# DIFF and INCR backups are kept for a configured number of days, then deleted by the `cleanuo`
|
|
21
25
|
# age settings are in days
|
|
22
26
|
DIFF_AGE = 100
|
|
23
27
|
INCR_AGE = 40
|
dar_backup/dar_backup.py
CHANGED
|
@@ -37,8 +37,10 @@ from sys import stderr
|
|
|
37
37
|
from sys import argv
|
|
38
38
|
from sys import version_info
|
|
39
39
|
from time import time
|
|
40
|
+
from rich.console import Console
|
|
41
|
+
from rich.text import Text
|
|
40
42
|
from threading import Event
|
|
41
|
-
from typing import List
|
|
43
|
+
from typing import List, Tuple
|
|
42
44
|
|
|
43
45
|
from . import __about__ as about
|
|
44
46
|
from dar_backup.config_settings import ConfigSettings
|
|
@@ -48,7 +50,9 @@ from dar_backup.util import get_logger
|
|
|
48
50
|
from dar_backup.util import BackupError
|
|
49
51
|
from dar_backup.util import RestoreError
|
|
50
52
|
from dar_backup.util import requirements
|
|
53
|
+
from dar_backup.util import show_version
|
|
51
54
|
from dar_backup.util import get_binary_info
|
|
55
|
+
from dar_backup.util import print_aligned_settings
|
|
52
56
|
from dar_backup.util import backup_definition_completer, list_archive_completer
|
|
53
57
|
|
|
54
58
|
from dar_backup.command_runner import CommandRunner
|
|
@@ -647,14 +651,6 @@ def filter_darrc_file(darrc_path):
|
|
|
647
651
|
|
|
648
652
|
|
|
649
653
|
|
|
650
|
-
def show_version():
|
|
651
|
-
script_name = os.path.basename(argv[0])
|
|
652
|
-
print(f"{script_name} {about.__version__}")
|
|
653
|
-
print(f"dar-backup.py source code is here: https://github.com/per2jensen/dar-backup")
|
|
654
|
-
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
655
|
-
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
656
|
-
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
657
|
-
|
|
658
654
|
|
|
659
655
|
def show_examples():
|
|
660
656
|
examples = """
|
|
@@ -846,35 +842,40 @@ def main():
|
|
|
846
842
|
args.darrc = filter_darrc_file(args.darrc)
|
|
847
843
|
logger.debug(f"Filtered .darrc file: {args.darrc}")
|
|
848
844
|
|
|
845
|
+
start_msgs: List[Tuple[str, str]] = []
|
|
846
|
+
|
|
849
847
|
start_time=int(time())
|
|
850
|
-
|
|
851
|
-
logger.info(f"dar-backup.py started, version: {about.__version__}")
|
|
848
|
+
start_msgs.append(('dar-backup.py:', about.__version__))
|
|
852
849
|
logger.info(f"START TIME: {start_time}")
|
|
853
|
-
logger.debug(f"`
|
|
854
|
-
logger.debug(f"`
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
850
|
+
logger.debug(f"{'`Args`:\n'}{args}")
|
|
851
|
+
logger.debug(f"{'`Config_settings`:\n'}{config_settings}")
|
|
852
|
+
dar_properties = get_binary_info(command='dar')
|
|
853
|
+
start_msgs.append(('dar path:', dar_properties['path']))
|
|
854
|
+
start_msgs.append(('dar version:', dar_properties['version']))
|
|
858
855
|
|
|
859
856
|
file_dir = os.path.normpath(os.path.dirname(__file__))
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
args.verbose and args.
|
|
865
|
-
args.verbose and args.
|
|
857
|
+
start_msgs.append(('Script directory:', os.path.abspath(file_dir)))
|
|
858
|
+
start_msgs.append(('Config file:', os.path.abspath(args.config_file)))
|
|
859
|
+
start_msgs.append((".darrc location:", args.darrc))
|
|
860
|
+
|
|
861
|
+
args.verbose and args.full_backup and start_msgs.append(("Type of backup:", "FULL"))
|
|
862
|
+
args.verbose and args.differential_backup and start_msgs.append(("Type of backup:", "DIFF"))
|
|
863
|
+
args.verbose and args.incremental_backup and start_msgs.append(("Type of backup:", "INCR"))
|
|
864
|
+
args.verbose and args.backup_definition and start_msgs.append(("Backup definition:", args.backup_definition))
|
|
866
865
|
if args.alternate_reference_archive:
|
|
867
|
-
args.verbose and (
|
|
868
|
-
args.verbose and (
|
|
869
|
-
args.verbose and (
|
|
866
|
+
args.verbose and start_msgs.append(("Alternate ref archive:", args.alternate_reference_archive))
|
|
867
|
+
args.verbose and start_msgs.append(("Backup.d dir:", config_settings.backup_d_dir))
|
|
868
|
+
args.verbose and start_msgs.append(("Backup dir:", config_settings.backup_dir))
|
|
870
869
|
|
|
871
870
|
restore_dir = args.restore_dir if args.restore_dir else config_settings.test_restore_dir
|
|
872
|
-
args.verbose and (
|
|
871
|
+
args.verbose and start_msgs.append(("Restore dir:", restore_dir))
|
|
873
872
|
|
|
874
|
-
args.verbose and (
|
|
875
|
-
args.verbose and (
|
|
876
|
-
args.verbose and (
|
|
877
|
-
|
|
873
|
+
args.verbose and start_msgs.append(("Logfile location:", config_settings.logfile_location))
|
|
874
|
+
args.verbose and start_msgs.append(("PAR2 enabled:", config_settings.par2_enabled))
|
|
875
|
+
args.verbose and start_msgs.append(("--do-not-compare:", args.do_not_compare))
|
|
876
|
+
|
|
877
|
+
dangerous_keywords = ["--do-not", "alternate"] # TODO: add more dangerous keywords
|
|
878
|
+
print_aligned_settings(start_msgs)
|
|
878
879
|
|
|
879
880
|
# sanity check
|
|
880
881
|
if args.backup_definition and not os.path.exists(os.path.join(config_settings.backup_d_dir, args.backup_definition)):
|
|
@@ -936,12 +937,17 @@ def main():
|
|
|
936
937
|
else:
|
|
937
938
|
logger.error(f"not correct result type: {result}, which must be a tuple (<msg>, <exit_code>)")
|
|
938
939
|
i=i+1
|
|
940
|
+
|
|
941
|
+
console = Console()
|
|
939
942
|
if error:
|
|
940
|
-
args.verbose
|
|
943
|
+
if args.verbose:
|
|
944
|
+
console.print(Text("Errors encountered", style="bold red"))
|
|
941
945
|
exit(1)
|
|
942
946
|
else:
|
|
943
|
-
args.verbose
|
|
947
|
+
if args.verbose:
|
|
948
|
+
console.print(Text("Success: all backups completed", style="bold green"))
|
|
944
949
|
exit(0)
|
|
950
|
+
|
|
945
951
|
|
|
946
952
|
if __name__ == "__main__":
|
|
947
953
|
main()
|
dar_backup/demo.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
|
|
67
|
+
def main():
|
|
68
|
+
parser = argparse.ArgumentParser(
|
|
69
|
+
description="Set up `dar-backup` on your system.",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"-i", "--install",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Deploy a simple config file, use ~/dar-backup/ for log file, archives and restore tests."
|
|
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
|
+
if os.path.exists(DAR_BACKUP_DIR):
|
|
89
|
+
errors.append(f"Directory '{DAR_BACKUP_DIR}' already exists.")
|
|
90
|
+
|
|
91
|
+
if errors:
|
|
92
|
+
for error in errors:
|
|
93
|
+
print(f"Error: {error}")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
try:
|
|
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
|
+
|
|
108
|
+
try:
|
|
109
|
+
shutil.copy2(source_file, destination_file)
|
|
110
|
+
print(f"Config file deployed to {destination_file}")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"Error: Could not copy config file: {e}")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
backup_definition = BACKUP_DEFINITION.replace("@@HOME_DIR@@", os.path.expanduser("~"))
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with open(os.path.join(CONFIG_DIR, "backup.d", "default"), "w") as f:
|
|
120
|
+
f.write(backup_definition)
|
|
121
|
+
print(f"Default backup definition file deployed to {os.path.join(CONFIG_DIR, 'backup.d', 'default')}")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"Error: Could not write default backup definition: {e}")
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"Installation failed: {e}")
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
print("1. Now run `manager --create` to create the catalog database.")
|
|
130
|
+
print("2. Then you can run `dar-backup --full-backup` to create a backup.")
|
|
131
|
+
print("3. List backups with `dar-backup --list`")
|
|
132
|
+
print("4. List contents of a backup with `dar-backup --list-contents <backup-name>`")
|
|
133
|
+
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
dar_backup/exceptions.py
ADDED
dar_backup/installer.py
CHANGED
|
@@ -1,138 +1,58 @@
|
|
|
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
1
|
import argparse
|
|
17
2
|
import os
|
|
18
|
-
import shutil
|
|
19
|
-
import sys
|
|
20
|
-
|
|
21
|
-
from . import __about__ as about
|
|
22
3
|
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
67
|
-
def main():
|
|
68
|
-
parser = argparse.ArgumentParser(
|
|
69
|
-
description="Set up `dar-backup` on your system.",
|
|
70
|
-
)
|
|
71
|
-
parser.add_argument(
|
|
72
|
-
"-i", "--install",
|
|
73
|
-
action="store_true",
|
|
74
|
-
help="Deploy a simple config file, use ~/dar-backup/ for log file, archives and restore tests."
|
|
75
|
-
)
|
|
76
|
-
parser.add_argument(
|
|
77
|
-
"-v", "--version",
|
|
78
|
-
action="version",
|
|
79
|
-
version=f"%(prog)s version {about.__version__}, {LICENSE}"
|
|
4
|
+
from dar_backup.config_settings import ConfigSettings
|
|
5
|
+
from dar_backup.util import setup_logging, get_logger
|
|
6
|
+
from dar_backup.command_runner import CommandRunner
|
|
7
|
+
from dar_backup.manager import create_db
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_installer(config_file: str, create_db_flag: bool):
|
|
11
|
+
config_file = os.path.expanduser(os.path.expandvars(config_file))
|
|
12
|
+
config_settings = ConfigSettings(config_file)
|
|
13
|
+
|
|
14
|
+
# Set up logging based on the config's specified log file
|
|
15
|
+
command_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
16
|
+
logger = setup_logging(
|
|
17
|
+
config_settings.logfile_location,
|
|
18
|
+
command_log,
|
|
19
|
+
log_level="info",
|
|
20
|
+
log_stdout=True,
|
|
80
21
|
)
|
|
81
|
-
|
|
22
|
+
command_logger = get_logger(command_output_logger=True)
|
|
23
|
+
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
24
|
+
|
|
25
|
+
# Create directories listed in config
|
|
26
|
+
for attr in ["backup_dir", "test_restore_dir", "backup_d_dir", "manager_db_dir"]:
|
|
27
|
+
path = getattr(config_settings, attr, None)
|
|
28
|
+
if path:
|
|
29
|
+
dir_path = Path(path).expanduser()
|
|
30
|
+
if not dir_path.exists():
|
|
31
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
print(f"Created directory: {dir_path}")
|
|
33
|
+
else:
|
|
34
|
+
print(f"Directory already exists: {dir_path}")
|
|
35
|
+
|
|
36
|
+
# Optionally create databases
|
|
37
|
+
if create_db_flag:
|
|
38
|
+
for file in os.listdir(config_settings.backup_d_dir):
|
|
39
|
+
backup_def = os.path.basename(file)
|
|
40
|
+
print(f"Creating catalog for: {backup_def}")
|
|
41
|
+
result = create_db(backup_def, config_settings, logger)
|
|
42
|
+
if result == 0:
|
|
43
|
+
print(f"✔️ Catalog created (or already existed): {backup_def}")
|
|
44
|
+
else:
|
|
45
|
+
print(f"❌ Failed to create catalog: {backup_def}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def installer_main():
|
|
49
|
+
parser = argparse.ArgumentParser(description="dar-backup installer")
|
|
50
|
+
parser.add_argument("--config", required=True, help="Path to config file")
|
|
51
|
+
parser.add_argument("--create-db", action="store_true", help="Create catalog databases")
|
|
82
52
|
args = parser.parse_args()
|
|
83
53
|
|
|
84
|
-
|
|
85
|
-
errors = []
|
|
86
|
-
if os.path.exists(CONFIG_DIR):
|
|
87
|
-
errors.append(f"Config directory '{CONFIG_DIR}' already exists.")
|
|
88
|
-
if os.path.exists(DAR_BACKUP_DIR):
|
|
89
|
-
errors.append(f"Directory '{DAR_BACKUP_DIR}' already exists.")
|
|
90
|
-
|
|
91
|
-
if errors:
|
|
92
|
-
for error in errors:
|
|
93
|
-
print(f"Error: {error}")
|
|
94
|
-
sys.exit(1)
|
|
95
|
-
|
|
96
|
-
try:
|
|
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
|
-
|
|
108
|
-
try:
|
|
109
|
-
shutil.copy2(source_file, destination_file)
|
|
110
|
-
print(f"Config file deployed to {destination_file}")
|
|
111
|
-
except Exception as e:
|
|
112
|
-
print(f"Error: Could not copy config file: {e}")
|
|
113
|
-
sys.exit(1)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
backup_definition = BACKUP_DEFINITION.replace("@@HOME_DIR@@", os.path.expanduser("~"))
|
|
117
|
-
|
|
118
|
-
try:
|
|
119
|
-
with open(os.path.join(CONFIG_DIR, "backup.d", "default"), "w") as f:
|
|
120
|
-
f.write(backup_definition)
|
|
121
|
-
print(f"Default backup definition file deployed to {os.path.join(CONFIG_DIR, 'backup.d', 'default')}")
|
|
122
|
-
except Exception as e:
|
|
123
|
-
print(f"Error: Could not write default backup definition: {e}")
|
|
124
|
-
sys.exit(1)
|
|
125
|
-
except Exception as e:
|
|
126
|
-
print(f"Installation failed: {e}")
|
|
127
|
-
sys.exit(1)
|
|
128
|
-
|
|
129
|
-
print("1. Now run `manager --create` to create the catalog database.")
|
|
130
|
-
print("2. Then you can run `dar-backup --full-backup` to create a backup.")
|
|
131
|
-
print("3. List backups with `dar-backup --list`")
|
|
132
|
-
print("4. List contents of a backup with `dar-backup --list-contents <backup-name>`")
|
|
133
|
-
|
|
134
|
-
sys.exit(0)
|
|
54
|
+
run_installer(args.config, args.create_db)
|
|
135
55
|
|
|
136
56
|
|
|
137
57
|
if __name__ == "__main__":
|
|
138
|
-
|
|
58
|
+
installer_main()
|
dar_backup/manager.py
CHANGED
|
@@ -36,6 +36,8 @@ from dar_backup.util import setup_logging
|
|
|
36
36
|
from dar_backup.util import CommandResult
|
|
37
37
|
from dar_backup.util import get_logger
|
|
38
38
|
from dar_backup.util import get_binary_info
|
|
39
|
+
from dar_backup.util import show_version
|
|
40
|
+
from dar_backup.util import print_aligned_settings
|
|
39
41
|
|
|
40
42
|
from dar_backup.command_runner import CommandRunner
|
|
41
43
|
from dar_backup.command_runner import CommandResult
|
|
@@ -43,7 +45,7 @@ from dar_backup.util import backup_definition_completer, list_archive_completer,
|
|
|
43
45
|
|
|
44
46
|
from datetime import datetime
|
|
45
47
|
from time import time
|
|
46
|
-
from typing import Dict, List, NamedTuple
|
|
48
|
+
from typing import Dict, List, NamedTuple, Tuple
|
|
47
49
|
|
|
48
50
|
# Constants
|
|
49
51
|
SCRIPTNAME = os.path.basename(__file__)
|
|
@@ -54,6 +56,15 @@ DB_SUFFIX = ".db"
|
|
|
54
56
|
logger = None
|
|
55
57
|
runner = None
|
|
56
58
|
|
|
59
|
+
|
|
60
|
+
def get_db_dir(config_settings: ConfigSettings) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Return the correct directory for storing catalog databases.
|
|
63
|
+
Uses manager_db_dir if set, otherwise falls back to backup_dir.
|
|
64
|
+
"""
|
|
65
|
+
return getattr(config_settings, "manager_db_dir", None) or config_settings.backup_dir
|
|
66
|
+
|
|
67
|
+
|
|
57
68
|
def show_more_help():
|
|
58
69
|
help_text = f"""
|
|
59
70
|
NAME
|
|
@@ -62,31 +73,43 @@ NAME
|
|
|
62
73
|
print(help_text)
|
|
63
74
|
|
|
64
75
|
|
|
65
|
-
def create_db(backup_def: str, config_settings: ConfigSettings):
|
|
76
|
+
def create_db(backup_def: str, config_settings: ConfigSettings, logger, runner) -> int:
|
|
77
|
+
db_dir = get_db_dir(config_settings)
|
|
78
|
+
|
|
79
|
+
if not os.path.exists(db_dir):
|
|
80
|
+
logger.error(f"DB dir does not exist: {db_dir}")
|
|
81
|
+
return 1
|
|
82
|
+
if not os.path.isdir(db_dir):
|
|
83
|
+
logger.error(f"DB path exists but is not a directory: {db_dir}")
|
|
84
|
+
return 1
|
|
85
|
+
if not os.access(db_dir, os.W_OK):
|
|
86
|
+
logger.error(f"DB dir is not writable: {db_dir}")
|
|
87
|
+
return 1
|
|
88
|
+
|
|
66
89
|
database = f"{backup_def}{DB_SUFFIX}"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
logger.debug(f"BACKUP_DIR: {config_settings.backup_dir}")
|
|
90
|
+
database_path = os.path.join(db_dir, database)
|
|
91
|
+
|
|
92
|
+
logger.debug(f"DB directory: {db_dir}")
|
|
71
93
|
|
|
72
94
|
if os.path.exists(database_path):
|
|
73
|
-
logger.
|
|
95
|
+
logger.info(f'"{database_path}" already exists, skipping creation')
|
|
74
96
|
return 0
|
|
75
97
|
else:
|
|
76
98
|
logger.info(f'Create catalog database: "{database_path}"')
|
|
77
|
-
command = ['dar_manager', '--create'
|
|
99
|
+
command = ['dar_manager', '--create', database_path]
|
|
78
100
|
process = runner.run(command)
|
|
79
101
|
logger.debug(f"return code from 'db created': {process.returncode}")
|
|
80
102
|
if process.returncode == 0:
|
|
81
103
|
logger.info(f'Database created: "{database_path}"')
|
|
82
104
|
else:
|
|
83
105
|
logger.error(f'Something went wrong creating the database: "{database_path}"')
|
|
84
|
-
stdout, stderr = process.stdout, process.stderr
|
|
106
|
+
stdout, stderr = process.stdout, process.stderr
|
|
85
107
|
logger.error(f"stderr: {stderr}")
|
|
86
108
|
logger.error(f"stdout: {stdout}")
|
|
87
109
|
|
|
88
110
|
return process.returncode
|
|
89
111
|
|
|
112
|
+
|
|
90
113
|
def list_catalogs(backup_def: str, config_settings: ConfigSettings, suppress_output=False) -> CommandResult:
|
|
91
114
|
"""
|
|
92
115
|
List catalogs from the database for the given backup definition.
|
|
@@ -95,7 +118,7 @@ def list_catalogs(backup_def: str, config_settings: ConfigSettings, suppress_out
|
|
|
95
118
|
A CommandResult containing the raw stdout/stderr and return code.
|
|
96
119
|
"""
|
|
97
120
|
database = f"{backup_def}{DB_SUFFIX}"
|
|
98
|
-
database_path = os.path.join(config_settings
|
|
121
|
+
database_path = os.path.join(get_db_dir(config_settings), database)
|
|
99
122
|
|
|
100
123
|
if not os.path.exists(database_path):
|
|
101
124
|
error_msg = f'Database not found: "{database_path}"'
|
|
@@ -174,7 +197,7 @@ def list_archive_contents(archive: str, config_settings: ConfigSettings) -> int:
|
|
|
174
197
|
"""
|
|
175
198
|
backup_def = backup_def_from_archive(archive)
|
|
176
199
|
database = f"{backup_def}{DB_SUFFIX}"
|
|
177
|
-
database_path = os.path.join(config_settings
|
|
200
|
+
database_path = os.path.join(get_db_dir(config_settings), database)
|
|
178
201
|
|
|
179
202
|
if not os.path.exists(database_path):
|
|
180
203
|
logger.error(f'Database not found: "{database_path}"')
|
|
@@ -217,8 +240,9 @@ def list_catalog_contents(catalog_number: int, backup_def: str, config_settings:
|
|
|
217
240
|
"""
|
|
218
241
|
List the contents of catalog # in catalog database for given backup definition
|
|
219
242
|
"""
|
|
243
|
+
logger = get_logger()
|
|
220
244
|
database = f"{backup_def}{DB_SUFFIX}"
|
|
221
|
-
database_path = os.path.join(config_settings
|
|
245
|
+
database_path = os.path.join(get_db_dir(config_settings), database)
|
|
222
246
|
if not os.path.exists(database_path):
|
|
223
247
|
logger.error(f'Catalog database not found: "{database_path}"')
|
|
224
248
|
return 1
|
|
@@ -239,7 +263,7 @@ def find_file(file, backup_def, config_settings):
|
|
|
239
263
|
Find a specific file
|
|
240
264
|
"""
|
|
241
265
|
database = f"{backup_def}{DB_SUFFIX}"
|
|
242
|
-
database_path = os.path.join(config_settings
|
|
266
|
+
database_path = os.path.join(get_db_dir(config_settings), database)
|
|
243
267
|
if not os.path.exists(database_path):
|
|
244
268
|
logger.error(f'Database not found: "{database_path}"')
|
|
245
269
|
return 1
|
|
@@ -283,7 +307,7 @@ def add_specific_archive(archive: str, config_settings: ConfigSettings, director
|
|
|
283
307
|
|
|
284
308
|
# Determine catalog DB path
|
|
285
309
|
database = f"{backup_definition}{DB_SUFFIX}"
|
|
286
|
-
database_path = os.path.realpath(os.path.join(config_settings
|
|
310
|
+
database_path = os.path.realpath(os.path.join(get_db_dir(config_settings), database))
|
|
287
311
|
|
|
288
312
|
# Safety check: is archive older than latest in catalog?
|
|
289
313
|
try:
|
|
@@ -444,7 +468,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
|
|
|
444
468
|
|
|
445
469
|
"""
|
|
446
470
|
backup_def = backup_def_from_archive(archive)
|
|
447
|
-
database_path = os.path.join(config_settings
|
|
471
|
+
database_path = os.path.join(get_db_dir(config_settings), f"{backup_def}{DB_SUFFIX}")
|
|
448
472
|
cat_no:int = cat_no_for_name(archive, config_settings)
|
|
449
473
|
if cat_no >= 0:
|
|
450
474
|
command = ['dar_manager', '--base', database_path, "--delete", str(cat_no)]
|
|
@@ -484,8 +508,6 @@ def build_arg_parser():
|
|
|
484
508
|
return parser
|
|
485
509
|
|
|
486
510
|
|
|
487
|
-
|
|
488
|
-
|
|
489
511
|
def main():
|
|
490
512
|
global logger, runner
|
|
491
513
|
|
|
@@ -508,16 +530,13 @@ def main():
|
|
|
508
530
|
return
|
|
509
531
|
|
|
510
532
|
if args.version:
|
|
511
|
-
|
|
512
|
-
print(f"Source code is here: https://github.com/per2jensen/dar-backup")
|
|
513
|
-
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
514
|
-
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
515
|
-
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
533
|
+
show_version()
|
|
516
534
|
sys.exit(0)
|
|
517
|
-
return
|
|
518
535
|
|
|
519
536
|
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
520
537
|
config_settings = ConfigSettings(args.config_file)
|
|
538
|
+
print(f"Config settings: {config_settings}")
|
|
539
|
+
|
|
521
540
|
if not os.path.dirname(config_settings.logfile_location):
|
|
522
541
|
print(f"Directory for log file '{config_settings.logfile_location}' does not exist, exiting")
|
|
523
542
|
sys.exit(1)
|
|
@@ -528,15 +547,23 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
528
547
|
command_logger = get_logger(command_output_logger=True)
|
|
529
548
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
530
549
|
|
|
550
|
+
start_msgs: List[Tuple[str, str]] = []
|
|
551
|
+
|
|
531
552
|
start_time = int(time())
|
|
532
|
-
|
|
553
|
+
start_msgs.append((f"{SCRIPTNAME}:", about.__version__))
|
|
533
554
|
logger.info(f"START TIME: {start_time}")
|
|
534
555
|
logger.debug(f"`args`:\n{args}")
|
|
535
556
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
557
|
+
start_msgs.append(("Config file:", args.config_file))
|
|
558
|
+
args.verbose and start_msgs.append(("Backup dir:", config_settings.backup_dir))
|
|
559
|
+
start_msgs.append(("Logfile:", config_settings.logfile_location))
|
|
560
|
+
args.verbose and start_msgs.append(("--alternate-archive-dir:", args.alternate_archive_dir))
|
|
561
|
+
args.verbose and start_msgs.append(("--cleanup-specific-archives:", args.cleanup_specific_archives))
|
|
536
562
|
dar_manager_properties = get_binary_info(command='dar_manager')
|
|
537
|
-
|
|
538
|
-
|
|
563
|
+
start_msgs.append(("dar_manager:", dar_manager_properties['path']))
|
|
564
|
+
start_msgs.append(("dar_manager v.:", dar_manager_properties['version']))
|
|
539
565
|
|
|
566
|
+
print_aligned_settings(start_msgs)
|
|
540
567
|
|
|
541
568
|
# --- Sanity checks ---
|
|
542
569
|
if args.add_dir and not args.add_dir.strip():
|
|
@@ -597,14 +624,14 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
597
624
|
# --- Functional logic ---
|
|
598
625
|
if args.create_db:
|
|
599
626
|
if args.backup_def:
|
|
600
|
-
sys.exit(create_db(args.backup_def, config_settings))
|
|
627
|
+
sys.exit(create_db(args.backup_def, config_settings, logger, runner))
|
|
601
628
|
return
|
|
602
629
|
else:
|
|
603
630
|
for root, dirs, files in os.walk(config_settings.backup_d_dir):
|
|
604
631
|
for file in files:
|
|
605
632
|
current_backupdef = os.path.basename(file)
|
|
606
633
|
logger.debug(f"Create catalog db for backup definition: '{current_backupdef}'")
|
|
607
|
-
result = create_db(current_backupdef, config_settings)
|
|
634
|
+
result = create_db(current_backupdef, config_settings, logger, runner)
|
|
608
635
|
if result != 0:
|
|
609
636
|
sys.exit(result)
|
|
610
637
|
return
|
dar_backup/util.py
CHANGED
|
@@ -24,8 +24,11 @@ from datetime import datetime
|
|
|
24
24
|
from dar_backup.config_settings import ConfigSettings
|
|
25
25
|
import dar_backup.__about__ as about
|
|
26
26
|
|
|
27
|
-
from
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
from rich.text import Text
|
|
28
29
|
|
|
30
|
+
from typing import NamedTuple, List
|
|
31
|
+
from typing import Tuple
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
logger=None
|
|
@@ -99,7 +102,13 @@ def get_logger(command_output_logger: bool = False) -> logging.Logger:
|
|
|
99
102
|
|
|
100
103
|
return secondary_logger if command_output_logger else logger
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
def show_version():
|
|
106
|
+
script_name = os.path.basename(sys.argv[0])
|
|
107
|
+
print(f"{script_name} {about.__version__}")
|
|
108
|
+
print(f"{script_name} source code is here: https://github.com/per2jensen/dar-backup")
|
|
109
|
+
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
110
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
111
|
+
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
103
112
|
|
|
104
113
|
def extract_version(output):
|
|
105
114
|
match = re.search(r'(\d+\.\d+(\.\d+)?)', output)
|
|
@@ -218,7 +227,6 @@ class RestoreError(Exception):
|
|
|
218
227
|
pass
|
|
219
228
|
|
|
220
229
|
|
|
221
|
-
|
|
222
230
|
class CommandResult(NamedTuple):
|
|
223
231
|
"""
|
|
224
232
|
The reult of the run_command() function.
|
|
@@ -484,3 +492,92 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
|
|
|
484
492
|
candidates = sorted(archive for archive in all_archives if archive not in existing)
|
|
485
493
|
return candidates or ["[no new archives]"]
|
|
486
494
|
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def patch_config_file(path: str, replacements: dict) -> None:
|
|
499
|
+
"""
|
|
500
|
+
Replace specific key values in a config file in-place.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
path: Path to the config file.
|
|
504
|
+
replacements: Dictionary of keys to new values, e.g., {"LOGFILE_LOCATION": "/tmp/logfile.log"}.
|
|
505
|
+
"""
|
|
506
|
+
with open(path, 'r') as f:
|
|
507
|
+
lines = f.readlines()
|
|
508
|
+
|
|
509
|
+
with open(path, 'w') as f:
|
|
510
|
+
for line in lines:
|
|
511
|
+
key = line.split('=')[0].strip()
|
|
512
|
+
if key in replacements:
|
|
513
|
+
f.write(f"{key} = {replacements[key]}\n")
|
|
514
|
+
else:
|
|
515
|
+
f.write(line)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
console = Console()
|
|
521
|
+
|
|
522
|
+
def print_aligned_settings(
|
|
523
|
+
settings: List[Tuple[str, str]],
|
|
524
|
+
log: bool = True,
|
|
525
|
+
header: str = "Startup Settings",
|
|
526
|
+
highlight_keywords: List[str] = None
|
|
527
|
+
) -> None:
|
|
528
|
+
"""
|
|
529
|
+
Print and optionally log settings nicely, using rich for color.
|
|
530
|
+
Highlights settings if dangerous keywords are found inside label or text,
|
|
531
|
+
but only if text is not None or empty.
|
|
532
|
+
"""
|
|
533
|
+
if not settings:
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
settings = [(str(label), "" if text is None else str(text)) for label, text in settings]
|
|
537
|
+
logger = get_logger()
|
|
538
|
+
|
|
539
|
+
max_label_length = max(len(label) for label, _ in settings)
|
|
540
|
+
|
|
541
|
+
header_line = f"========== {header} =========="
|
|
542
|
+
footer_line = "=" * len(header_line)
|
|
543
|
+
|
|
544
|
+
console.print(f"[bold cyan]{header_line}[/bold cyan]")
|
|
545
|
+
if log and logger:
|
|
546
|
+
logger.info(header_line)
|
|
547
|
+
|
|
548
|
+
for label, text in settings:
|
|
549
|
+
padded_label = f"{label:<{max_label_length}}"
|
|
550
|
+
|
|
551
|
+
label_clean = label.rstrip(":").lower()
|
|
552
|
+
text_clean = text.lower()
|
|
553
|
+
|
|
554
|
+
# Skip highlighting if text is empty
|
|
555
|
+
if not text_clean.strip():
|
|
556
|
+
danger = False
|
|
557
|
+
else:
|
|
558
|
+
danger = False
|
|
559
|
+
if highlight_keywords:
|
|
560
|
+
combined_text = f"{label_clean} {text_clean}"
|
|
561
|
+
danger = any(keyword.lower() in combined_text for keyword in highlight_keywords)
|
|
562
|
+
|
|
563
|
+
# Build the line
|
|
564
|
+
line_text = Text()
|
|
565
|
+
line_text.append(padded_label, style="bold")
|
|
566
|
+
line_text.append(" ", style="none")
|
|
567
|
+
|
|
568
|
+
if danger:
|
|
569
|
+
line_text.append("[!]", style="bold red")
|
|
570
|
+
line_text.append(" ", style="none")
|
|
571
|
+
|
|
572
|
+
line_text.append(text, style="white")
|
|
573
|
+
|
|
574
|
+
console.print(line_text)
|
|
575
|
+
|
|
576
|
+
# Always log clean text (no [!] in log)
|
|
577
|
+
final_line_for_log = f"{padded_label} {text}"
|
|
578
|
+
if log and logger:
|
|
579
|
+
logger.info(final_line_for_log)
|
|
580
|
+
|
|
581
|
+
console.print(f"[bold cyan]{footer_line}[/bold cyan]")
|
|
582
|
+
if log and logger:
|
|
583
|
+
logger.info(footer_line)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dar-backup
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.20
|
|
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: GPG Public Key, https://keys.openpgp.org/search?q=dar-backup@pm.me
|
|
6
6
|
Project-URL: Homepage, https://github.com/per2jensen/dar-backup/tree/main/v2
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
+
dar_backup/Changelog.md,sha256=hFz5NoZyezxgKza9i4VBv57aPnCuzhIQM9yK_2cxAZ8,9107
|
|
3
|
+
dar_backup/README.md,sha256=PcZQRaBw9i_GbRoA786OFKI0PwY87E7D30UzovWqEQQ,44902
|
|
4
|
+
dar_backup/__about__.py,sha256=bhh1JgaMOqVsHTPoLk0PdjZxyDSXmYzOzEaRGXYuakc,22
|
|
5
|
+
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dar_backup/clean_log.py,sha256=uQ_9HomK7RRkskXrgMPbAC84RgNlCFJ8RHL9KHeAMvc,5447
|
|
7
|
+
dar_backup/cleanup.py,sha256=4IG736gdmvjurcQQ_DC-B5CoA3Ix9u5C3WfwmMmFIpc,12500
|
|
8
|
+
dar_backup/command_runner.py,sha256=PQA968EXssSGjSs_16psFkdxRZi9-YK4TrBKFz0ss3k,4455
|
|
9
|
+
dar_backup/config_settings.py,sha256=8HhIDVtFk7D3VqJlYcveOlAaSEJwIaO3MICz-rN3tVY,5260
|
|
10
|
+
dar_backup/dar-backup.conf,sha256=WWNrysjQ1ii2jpab8jxgWCw3IkNxLBYVOW8fxWbO_9g,1155
|
|
11
|
+
dar_backup/dar_backup.py,sha256=x0at94NeQzu8tv2ohg3YKAt6j76gQUS2387wh5Z4B78,41856
|
|
12
|
+
dar_backup/dar_backup_systemd.py,sha256=oehD_t9CFu0CsMgDWRH-Gt74Ynl1m29yqQEh5Kxv7aw,3807
|
|
13
|
+
dar_backup/demo.py,sha256=fl2LHHWxuXRT814M_zuZh_YqqLk6nuNg9BI9HpLzdUU,4841
|
|
14
|
+
dar_backup/exceptions.py,sha256=6fpHpnhkbtFcFZVDMqCsdESg-kQNCjF40EROGKeq8yU,113
|
|
15
|
+
dar_backup/installer.py,sha256=gISJ3inz1uTjM5235gNtwjDUG-Fq6b3OZzMQuJl5eRg,2252
|
|
16
|
+
dar_backup/manager.py,sha256=UyEcm9ydkok47slgSlpPEKP_mAdkvcwCghLCXdNlGpc,26622
|
|
17
|
+
dar_backup/rich_progress.py,sha256=jTwM-4VlqHHzKqIfyXjL1pWEriobSJwCdN3YXzXzRdo,3105
|
|
18
|
+
dar_backup/util.py,sha256=2Uwd1bf3N-AEzIvc69gvwEnGXNYNzZ-RqxUfbvmOUAc,20079
|
|
19
|
+
dar_backup-0.6.20.dist-info/METADATA,sha256=dO6-9MvI9M_munyvCVFGNrq33NBbDDxn6UAIRLwVb5U,86624
|
|
20
|
+
dar_backup-0.6.20.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
dar_backup-0.6.20.dist-info/entry_points.txt,sha256=jFMqGdvGO8NeHrmcB0pxY7__PCSQAWRCFzsschMXsec,248
|
|
22
|
+
dar_backup-0.6.20.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
23
|
+
dar_backup-0.6.20.dist-info/RECORD,,
|
|
@@ -3,5 +3,5 @@ clean-log = dar_backup.clean_log:main
|
|
|
3
3
|
cleanup = dar_backup.cleanup:main
|
|
4
4
|
dar-backup = dar_backup.dar_backup:main
|
|
5
5
|
dar-backup-systemd = dar_backup.dar_backup_systemd:main
|
|
6
|
-
|
|
6
|
+
demo = dar_backup.demo:main
|
|
7
7
|
manager = dar_backup.manager:main
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
dar_backup/.darrc,sha256=-aerqivZmOsW_XBCh9IfbYTUvw0GkzDSr3Vx4GcNB1g,2113
|
|
2
|
-
dar_backup/Changelog.md,sha256=PbR2JupEHiWdRNP8ApuWEYIrRQEeJQUnlkVR7N4JJHg,8591
|
|
3
|
-
dar_backup/README.md,sha256=PcZQRaBw9i_GbRoA786OFKI0PwY87E7D30UzovWqEQQ,44902
|
|
4
|
-
dar_backup/__about__.py,sha256=1xdEgfhhUWpssnr_9DTiORat5DDv6NUXRtydGkm-Aog,22
|
|
5
|
-
dar_backup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
dar_backup/clean_log.py,sha256=uQ_9HomK7RRkskXrgMPbAC84RgNlCFJ8RHL9KHeAMvc,5447
|
|
7
|
-
dar_backup/cleanup.py,sha256=mrpu9xAy6AKcEbjMetscEh-IzJp0xVFObdrwaXncFtA,12712
|
|
8
|
-
dar_backup/command_runner.py,sha256=PQA968EXssSGjSs_16psFkdxRZi9-YK4TrBKFz0ss3k,4455
|
|
9
|
-
dar_backup/config_settings.py,sha256=Rh4T35-w_5tpRAViMfv3YP3GBpG4mQy7Do8cNBzYAR0,4912
|
|
10
|
-
dar_backup/dar-backup.conf,sha256=64O3bGlzqupneT2gVeaETJ1qS6-3Exet9Zto27jgwPQ,897
|
|
11
|
-
dar_backup/dar_backup.py,sha256=Uy0VAFRbOAmZCyZyyEfWuIM4he2WivfQXhKzVGhWbHc,41939
|
|
12
|
-
dar_backup/dar_backup_systemd.py,sha256=oehD_t9CFu0CsMgDWRH-Gt74Ynl1m29yqQEh5Kxv7aw,3807
|
|
13
|
-
dar_backup/installer.py,sha256=fl2LHHWxuXRT814M_zuZh_YqqLk6nuNg9BI9HpLzdUU,4841
|
|
14
|
-
dar_backup/manager.py,sha256=t-CWxvxzhMHCOCU99px6heL3dRmil2JDOyw9PHL96xY,25712
|
|
15
|
-
dar_backup/rich_progress.py,sha256=jTwM-4VlqHHzKqIfyXjL1pWEriobSJwCdN3YXzXzRdo,3105
|
|
16
|
-
dar_backup/util.py,sha256=REZANvqLqczx3MvGszpVaesrpW03NuH_GPGjDllpT4Q,16877
|
|
17
|
-
dar_backup-0.6.19.dist-info/METADATA,sha256=jQwLE815tTEXjKh7l0rUd5SprMF_czZ3QPRXn8TNMnM,86624
|
|
18
|
-
dar_backup-0.6.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
19
|
-
dar_backup-0.6.19.dist-info/entry_points.txt,sha256=NSCYoG5Dvh1UhvKWOQPgcHdFv4--R4Sre3d9FwJra3E,258
|
|
20
|
-
dar_backup-0.6.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
21
|
-
dar_backup-0.6.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|