dar-backup 0.6.19__py3-none-any.whl → 0.6.20.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/Changelog.md +25 -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 +54 -28
- dar_backup/util.py +153 -14
- {dar_backup-0.6.19.dist-info → dar_backup-0.6.20.1.dist-info}/METADATA +1 -1
- dar_backup-0.6.20.1.dist-info/RECORD +23 -0
- {dar_backup-0.6.19.dist-info → dar_backup-0.6.20.1.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.1.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.19.dist-info → dar_backup-0.6.20.1.dist-info}/licenses/LICENSE +0 -0
dar_backup/Changelog.md
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
<!-- markdownlint-disable MD024 -->
|
|
2
2
|
# dar-backup Changelog
|
|
3
3
|
|
|
4
|
+
## v2-beta-0.6.20.1 - 2025-05-04
|
|
5
|
+
|
|
6
|
+
Github link: [v2-beta-0.6.20.1](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.6.20.1/v2)
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- FIX: bash/zsh completers fixed to support MANAGER_DB_DIR config if set
|
|
11
|
+
- `cleanup` and `manager` completer now sorts archives by \<backup-definition> and \<archive date> (so not using \<type>)
|
|
12
|
+
|
|
13
|
+
## v2-beta-0.6.20 - 2025-05-03
|
|
14
|
+
|
|
15
|
+
Github link: [v2-beta-0.6.20](https://github.com/per2jensen/dar-backup/tree/v2-beta-0.6.20/v2)
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- show_version() moved to util and tests for dar-backup, manager and cleanup
|
|
20
|
+
- startup informational messages now works the same across the scripts
|
|
21
|
+
- Improved ConfigSettings class to handle optional configuration keys
|
|
22
|
+
|
|
23
|
+
-- test cases added
|
|
24
|
+
|
|
25
|
+
- Optional config parameter: MANAGER_DB_DIR, ideally to point to another disk for safe keeping backup catalogs
|
|
26
|
+
|
|
27
|
+
-- test cases added
|
|
28
|
+
|
|
4
29
|
## v2-beta-0.6.19 - 2025-04-21
|
|
5
30
|
|
|
6
31
|
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.1"
|
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,12 @@ 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
|
+
|
|
521
539
|
if not os.path.dirname(config_settings.logfile_location):
|
|
522
540
|
print(f"Directory for log file '{config_settings.logfile_location}' does not exist, exiting")
|
|
523
541
|
sys.exit(1)
|
|
@@ -528,15 +546,23 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
528
546
|
command_logger = get_logger(command_output_logger=True)
|
|
529
547
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
530
548
|
|
|
549
|
+
start_msgs: List[Tuple[str, str]] = []
|
|
550
|
+
|
|
531
551
|
start_time = int(time())
|
|
532
|
-
|
|
552
|
+
start_msgs.append((f"{SCRIPTNAME}:", about.__version__))
|
|
533
553
|
logger.info(f"START TIME: {start_time}")
|
|
534
554
|
logger.debug(f"`args`:\n{args}")
|
|
535
555
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
556
|
+
start_msgs.append(("Config file:", args.config_file))
|
|
557
|
+
args.verbose and start_msgs.append(("Backup dir:", config_settings.backup_dir))
|
|
558
|
+
start_msgs.append(("Logfile:", config_settings.logfile_location))
|
|
559
|
+
args.verbose and start_msgs.append(("--alternate-archive-dir:", args.alternate_archive_dir))
|
|
560
|
+
args.verbose and start_msgs.append(("--cleanup-specific-archives:", args.cleanup_specific_archives))
|
|
536
561
|
dar_manager_properties = get_binary_info(command='dar_manager')
|
|
537
|
-
|
|
538
|
-
|
|
562
|
+
start_msgs.append(("dar_manager:", dar_manager_properties['path']))
|
|
563
|
+
start_msgs.append(("dar_manager v.:", dar_manager_properties['version']))
|
|
539
564
|
|
|
565
|
+
print_aligned_settings(start_msgs)
|
|
540
566
|
|
|
541
567
|
# --- Sanity checks ---
|
|
542
568
|
if args.add_dir and not args.add_dir.strip():
|
|
@@ -597,14 +623,14 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
597
623
|
# --- Functional logic ---
|
|
598
624
|
if args.create_db:
|
|
599
625
|
if args.backup_def:
|
|
600
|
-
sys.exit(create_db(args.backup_def, config_settings))
|
|
626
|
+
sys.exit(create_db(args.backup_def, config_settings, logger, runner))
|
|
601
627
|
return
|
|
602
628
|
else:
|
|
603
629
|
for root, dirs, files in os.walk(config_settings.backup_d_dir):
|
|
604
630
|
for file in files:
|
|
605
631
|
current_backupdef = os.path.basename(file)
|
|
606
632
|
logger.debug(f"Create catalog db for backup definition: '{current_backupdef}'")
|
|
607
|
-
result = create_db(current_backupdef, config_settings)
|
|
633
|
+
result = create_db(current_backupdef, config_settings, logger, runner)
|
|
608
634
|
if result != 0:
|
|
609
635
|
sys.exit(result)
|
|
610
636
|
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
|
|
@@ -100,6 +103,30 @@ def get_logger(command_output_logger: bool = False) -> logging.Logger:
|
|
|
100
103
|
return secondary_logger if command_output_logger else logger
|
|
101
104
|
|
|
102
105
|
|
|
106
|
+
# Setup completer logger only once
|
|
107
|
+
def _setup_completer_logger(logfile="/tmp/dar_backup_completer.log"):
|
|
108
|
+
logger = logging.getLogger("completer")
|
|
109
|
+
if not logger.handlers:
|
|
110
|
+
handler = logging.FileHandler(logfile)
|
|
111
|
+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
112
|
+
handler.setFormatter(formatter)
|
|
113
|
+
logger.addHandler(handler)
|
|
114
|
+
logger.setLevel(logging.DEBUG)
|
|
115
|
+
return logger
|
|
116
|
+
|
|
117
|
+
# Singleton logger for completer debugging
|
|
118
|
+
completer_logger = _setup_completer_logger()
|
|
119
|
+
completer_logger.debug("Completer logger initialized.")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def show_version():
|
|
124
|
+
script_name = os.path.basename(sys.argv[0])
|
|
125
|
+
print(f"{script_name} {about.__version__}")
|
|
126
|
+
print(f"{script_name} source code is here: https://github.com/per2jensen/dar-backup")
|
|
127
|
+
print('''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
128
|
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
129
|
+
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
103
130
|
|
|
104
131
|
def extract_version(output):
|
|
105
132
|
match = re.search(r'(\d+\.\d+(\.\d+)?)', output)
|
|
@@ -218,7 +245,6 @@ class RestoreError(Exception):
|
|
|
218
245
|
pass
|
|
219
246
|
|
|
220
247
|
|
|
221
|
-
|
|
222
248
|
class CommandResult(NamedTuple):
|
|
223
249
|
"""
|
|
224
250
|
The reult of the run_command() function.
|
|
@@ -338,6 +364,7 @@ def extract_backup_definition_fallback() -> str:
|
|
|
338
364
|
|
|
339
365
|
|
|
340
366
|
|
|
367
|
+
|
|
341
368
|
def list_archive_completer(prefix, parsed_args, **kwargs):
|
|
342
369
|
import os
|
|
343
370
|
import configparser
|
|
@@ -361,12 +388,39 @@ def list_archive_completer(prefix, parsed_args, **kwargs):
|
|
|
361
388
|
files = os.listdir(backup_dir)
|
|
362
389
|
archive_re = re.compile(rf"^{re.escape(backup_def)}_.+_\d{{4}}-\d{{2}}-\d{{2}}\.1\.dar$") if backup_def else re.compile(r".+_\d{4}-\d{2}-\d{2}\.1\.dar$")
|
|
363
390
|
|
|
364
|
-
|
|
391
|
+
completions = [
|
|
365
392
|
f.rsplit(".1.dar", 1)[0]
|
|
366
393
|
for f in files
|
|
367
394
|
if archive_re.match(f)
|
|
368
395
|
]
|
|
369
396
|
|
|
397
|
+
completions = sorted(set(completions), key=sort_key)
|
|
398
|
+
return completions or ["[no matching archives]"]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def sort_key(archive_name: str):
|
|
403
|
+
"""
|
|
404
|
+
Sort by backup definition and then by date extracted from the archive name.
|
|
405
|
+
Handles formats like: <def>_<TYPE>_<YYYY-MM-DD>.<N>.dar
|
|
406
|
+
"""
|
|
407
|
+
try:
|
|
408
|
+
base = archive_name.split('.')[0] # remove .1.dar
|
|
409
|
+
parts = base.split('_')
|
|
410
|
+
if len(parts) < 3:
|
|
411
|
+
return (archive_name, datetime.min) # fallback for non-matching formats
|
|
412
|
+
|
|
413
|
+
# Correct assumption: last two parts are TYPE and DATE
|
|
414
|
+
def_name = '_'.join(parts[:-2]) # everything before _TYPE_DATE
|
|
415
|
+
date_str = parts[-1]
|
|
416
|
+
date = datetime.strptime(date_str, "%Y-%m-%d")
|
|
417
|
+
completer_logger.debug(f"Archive: {archive_name}, Def: {def_name}, Date: {date}")
|
|
418
|
+
return (def_name, date)
|
|
419
|
+
except Exception:
|
|
420
|
+
return (archive_name, datetime.min)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
370
424
|
|
|
371
425
|
def archive_content_completer(prefix, parsed_args, **kwargs):
|
|
372
426
|
"""
|
|
@@ -384,14 +438,15 @@ def archive_content_completer(prefix, parsed_args, **kwargs):
|
|
|
384
438
|
# Expand config path
|
|
385
439
|
config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
|
|
386
440
|
config = ConfigSettings(config_file=config_file)
|
|
387
|
-
|
|
441
|
+
#db_dir = expand_path((getattr(config, 'manager_db_dir', config.backup_dir))) # use manager_db_dir if set, else backup_dir
|
|
442
|
+
db_dir = expand_path(getattr(config, 'manager_db_dir', None) or config.backup_dir)
|
|
388
443
|
|
|
389
444
|
# Which db files to inspect?
|
|
390
445
|
backup_def = getattr(parsed_args, "backup_def", None)
|
|
391
446
|
db_files = (
|
|
392
|
-
[os.path.join(
|
|
447
|
+
[os.path.join( db_dir, f"{backup_def}.db")]
|
|
393
448
|
if backup_def
|
|
394
|
-
else [os.path.join(
|
|
449
|
+
else [os.path.join( db_dir, f) for f in os.listdir( db_dir) if f.endswith(".db")]
|
|
395
450
|
)
|
|
396
451
|
|
|
397
452
|
completions = []
|
|
@@ -418,13 +473,6 @@ def archive_content_completer(prefix, parsed_args, **kwargs):
|
|
|
418
473
|
if archive.startswith(prefix):
|
|
419
474
|
completions.append(archive)
|
|
420
475
|
|
|
421
|
-
# Sort: first by name (before first '_'), then by date (YYYY-MM-DD)
|
|
422
|
-
def sort_key(archive):
|
|
423
|
-
name_part = archive.split("_")[0]
|
|
424
|
-
date_match = re.search(r"\d{4}-\d{2}-\d{2}", archive)
|
|
425
|
-
date = datetime.strptime(date_match.group(0), "%Y-%m-%d") if date_match else datetime.min
|
|
426
|
-
return (name_part, date)
|
|
427
|
-
|
|
428
476
|
completions = sorted(set(completions), key=sort_key)
|
|
429
477
|
return completions or ["[no matching archives]"]
|
|
430
478
|
|
|
@@ -444,6 +492,8 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
|
|
|
444
492
|
|
|
445
493
|
config_file = expand_path(getattr(parsed_args, "config_file", "~/.config/dar-backup/dar-backup.conf"))
|
|
446
494
|
config = ConfigSettings(config_file=config_file)
|
|
495
|
+
#db_dir = expand_path((getattr(config, 'manager_db_dir', config.backup_dir))) # use manager_db_dir if set, else backup_dir
|
|
496
|
+
db_dir = expand_path(getattr(config, 'manager_db_dir') or config.backup_dir)
|
|
447
497
|
backup_dir = config.backup_dir
|
|
448
498
|
backup_def = getattr(parsed_args, "backup_def", None)
|
|
449
499
|
|
|
@@ -461,7 +511,7 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
|
|
|
461
511
|
all_archives.add(base)
|
|
462
512
|
|
|
463
513
|
# Step 2: exclude ones already present in the .db
|
|
464
|
-
db_path = os.path.join(
|
|
514
|
+
db_path = os.path.join(db_dir, f"{backup_def}.db") if backup_def else None
|
|
465
515
|
existing = set()
|
|
466
516
|
|
|
467
517
|
if db_path and os.path.exists(db_path):
|
|
@@ -484,3 +534,92 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
|
|
|
484
534
|
candidates = sorted(archive for archive in all_archives if archive not in existing)
|
|
485
535
|
return candidates or ["[no new archives]"]
|
|
486
536
|
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def patch_config_file(path: str, replacements: dict) -> None:
|
|
541
|
+
"""
|
|
542
|
+
Replace specific key values in a config file in-place.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
path: Path to the config file.
|
|
546
|
+
replacements: Dictionary of keys to new values, e.g., {"LOGFILE_LOCATION": "/tmp/logfile.log"}.
|
|
547
|
+
"""
|
|
548
|
+
with open(path, 'r') as f:
|
|
549
|
+
lines = f.readlines()
|
|
550
|
+
|
|
551
|
+
with open(path, 'w') as f:
|
|
552
|
+
for line in lines:
|
|
553
|
+
key = line.split('=')[0].strip()
|
|
554
|
+
if key in replacements:
|
|
555
|
+
f.write(f"{key} = {replacements[key]}\n")
|
|
556
|
+
else:
|
|
557
|
+
f.write(line)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
console = Console()
|
|
563
|
+
|
|
564
|
+
def print_aligned_settings(
|
|
565
|
+
settings: List[Tuple[str, str]],
|
|
566
|
+
log: bool = True,
|
|
567
|
+
header: str = "Startup Settings",
|
|
568
|
+
highlight_keywords: List[str] = None
|
|
569
|
+
) -> None:
|
|
570
|
+
"""
|
|
571
|
+
Print and optionally log settings nicely, using rich for color.
|
|
572
|
+
Highlights settings if dangerous keywords are found inside label or text,
|
|
573
|
+
but only if text is not None or empty.
|
|
574
|
+
"""
|
|
575
|
+
if not settings:
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
settings = [(str(label), "" if text is None else str(text)) for label, text in settings]
|
|
579
|
+
logger = get_logger()
|
|
580
|
+
|
|
581
|
+
max_label_length = max(len(label) for label, _ in settings)
|
|
582
|
+
|
|
583
|
+
header_line = f"========== {header} =========="
|
|
584
|
+
footer_line = "=" * len(header_line)
|
|
585
|
+
|
|
586
|
+
console.print(f"[bold cyan]{header_line}[/bold cyan]")
|
|
587
|
+
if log and logger:
|
|
588
|
+
logger.info(header_line)
|
|
589
|
+
|
|
590
|
+
for label, text in settings:
|
|
591
|
+
padded_label = f"{label:<{max_label_length}}"
|
|
592
|
+
|
|
593
|
+
label_clean = label.rstrip(":").lower()
|
|
594
|
+
text_clean = text.lower()
|
|
595
|
+
|
|
596
|
+
# Skip highlighting if text is empty
|
|
597
|
+
if not text_clean.strip():
|
|
598
|
+
danger = False
|
|
599
|
+
else:
|
|
600
|
+
danger = False
|
|
601
|
+
if highlight_keywords:
|
|
602
|
+
combined_text = f"{label_clean} {text_clean}"
|
|
603
|
+
danger = any(keyword.lower() in combined_text for keyword in highlight_keywords)
|
|
604
|
+
|
|
605
|
+
# Build the line
|
|
606
|
+
line_text = Text()
|
|
607
|
+
line_text.append(padded_label, style="bold")
|
|
608
|
+
line_text.append(" ", style="none")
|
|
609
|
+
|
|
610
|
+
if danger:
|
|
611
|
+
line_text.append("[!]", style="bold red")
|
|
612
|
+
line_text.append(" ", style="none")
|
|
613
|
+
|
|
614
|
+
line_text.append(text, style="white")
|
|
615
|
+
|
|
616
|
+
console.print(line_text)
|
|
617
|
+
|
|
618
|
+
# Always log clean text (no [!] in log)
|
|
619
|
+
final_line_for_log = f"{padded_label} {text}"
|
|
620
|
+
if log and logger:
|
|
621
|
+
logger.info(final_line_for_log)
|
|
622
|
+
|
|
623
|
+
console.print(f"[bold cyan]{footer_line}[/bold cyan]")
|
|
624
|
+
if log and logger:
|
|
625
|
+
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.1
|
|
4
4
|
Summary: A script to do full, differential and incremental backups using dar. Some files are restored from the backups during verification, after which par2 redundancy files are created. The script also has a cleanup feature to remove old backups and par2 files.
|
|
5
5
|
Project-URL: 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=D993XIep6ECnK42XdH6ayE8RlXDJVCbpr-SeCqEFRH8,9448
|
|
3
|
+
dar_backup/README.md,sha256=PcZQRaBw9i_GbRoA786OFKI0PwY87E7D30UzovWqEQQ,44902
|
|
4
|
+
dar_backup/__about__.py,sha256=rQMac36Uw_nIjFumz9vuaDITRsJHLv1bbxAZYx-Ka4E,24
|
|
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=JL-JX2C6RiDIj64Gw9pxNLUpxFqij1OBPp1BaUvV5sM,26573
|
|
17
|
+
dar_backup/rich_progress.py,sha256=jTwM-4VlqHHzKqIfyXjL1pWEriobSJwCdN3YXzXzRdo,3105
|
|
18
|
+
dar_backup/util.py,sha256=u_l6Hti6ZAHxaOqTZlSWBDspoKmkdoyzfZjhQgJqyOQ,21643
|
|
19
|
+
dar_backup-0.6.20.1.dist-info/METADATA,sha256=bD9ou-yK2zva5xz8vPVd57xqkEsjN9OaNAWZeoxDajE,86626
|
|
20
|
+
dar_backup-0.6.20.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
dar_backup-0.6.20.1.dist-info/entry_points.txt,sha256=jFMqGdvGO8NeHrmcB0pxY7__PCSQAWRCFzsschMXsec,248
|
|
22
|
+
dar_backup-0.6.20.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
23
|
+
dar_backup-0.6.20.1.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
|