dar-backup 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/__about__.py +1 -2
- dar_backup/clean_log.py +102 -63
- dar_backup/cleanup.py +139 -107
- dar_backup/command_runner.py +123 -15
- dar_backup/config_settings.py +25 -12
- dar_backup/dar-backup.conf +7 -0
- dar_backup/dar-backup.conf.j2 +3 -1
- dar_backup/dar_backup.py +529 -102
- dar_backup/dar_backup_systemd.py +1 -1
- dar_backup/demo.py +19 -11
- dar_backup/installer.py +18 -1
- dar_backup/manager.py +1085 -96
- dar_backup/util.py +128 -19
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/METADATA +320 -42
- dar_backup-1.1.0.dist-info/RECORD +23 -0
- dar_backup/Changelog.md +0 -401
- dar_backup/README.md +0 -2045
- dar_backup-1.0.1.dist-info/RECORD +0 -25
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/WHEEL +0 -0
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/entry_points.txt +0 -0
- {dar_backup-1.0.1.dist-info → dar_backup-1.1.0.dist-info}/licenses/LICENSE +0 -0
dar_backup/__about__.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
__version__ = "1.0
|
|
1
|
+
__version__ = "1.1.0"
|
|
2
2
|
|
|
3
3
|
__author__ = "Per Jensen"
|
|
4
4
|
|
|
5
5
|
__license__ = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
6
6
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
7
7
|
See section 15 and section 16 in the supplied "LICENSE" file.'''
|
|
8
|
-
|
dar_backup/clean_log.py
CHANGED
|
@@ -23,13 +23,54 @@ import re
|
|
|
23
23
|
import os
|
|
24
24
|
import sys
|
|
25
25
|
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
26
28
|
from dar_backup import __about__ as about
|
|
27
29
|
from dar_backup.config_settings import ConfigSettings
|
|
30
|
+
from dar_backup.util import send_discord_message, get_logger
|
|
28
31
|
|
|
29
32
|
LICENSE = '''Licensed under GNU GENERAL PUBLIC LICENSE v3, see the supplied file "LICENSE" for details.
|
|
30
33
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
31
34
|
See section 15 and section 16 in the supplied "LICENSE" file.'''
|
|
32
35
|
|
|
36
|
+
TIMESTAMP_RE = re.compile(r"^\d{4}-\d{2}-\d{2}\b")
|
|
37
|
+
CLEAN_MESSAGE_PREFIXES = (
|
|
38
|
+
"Inspecting directory",
|
|
39
|
+
"Finished Inspecting",
|
|
40
|
+
"<File",
|
|
41
|
+
"</File",
|
|
42
|
+
"<Attributes",
|
|
43
|
+
"</Attributes",
|
|
44
|
+
"<Directory",
|
|
45
|
+
"</Directory",
|
|
46
|
+
"<Catalog",
|
|
47
|
+
"</Catalog",
|
|
48
|
+
"<Symlink",
|
|
49
|
+
"</Symlink",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _split_level_and_message(line):
|
|
53
|
+
line = line.rstrip("\n")
|
|
54
|
+
if " - " not in line:
|
|
55
|
+
return None, None
|
|
56
|
+
|
|
57
|
+
parts = line.split(" - ")
|
|
58
|
+
if len(parts) >= 3 and TIMESTAMP_RE.match(parts[0].strip()):
|
|
59
|
+
level = parts[1]
|
|
60
|
+
message = " - ".join(parts[2:])
|
|
61
|
+
else:
|
|
62
|
+
level = parts[0]
|
|
63
|
+
message = " - ".join(parts[1:])
|
|
64
|
+
|
|
65
|
+
return level.strip(), message
|
|
66
|
+
|
|
67
|
+
def _should_remove_line(line):
|
|
68
|
+
level, message = _split_level_and_message(line)
|
|
69
|
+
if level != "INFO" or message is None:
|
|
70
|
+
return False
|
|
71
|
+
message = message.lstrip()
|
|
72
|
+
return any(message.startswith(prefix) for prefix in CLEAN_MESSAGE_PREFIXES)
|
|
73
|
+
|
|
33
74
|
def clean_log_file(log_file_path, dry_run=False):
|
|
34
75
|
"""Removes specific log lines from the given file using a memory-efficient streaming approach."""
|
|
35
76
|
|
|
@@ -42,7 +83,7 @@ def clean_log_file(log_file_path, dry_run=False):
|
|
|
42
83
|
print(f"No read permission for '{log_file_path}'")
|
|
43
84
|
sys.exit(1)
|
|
44
85
|
|
|
45
|
-
if not os.access(log_file_path, os.W_OK):
|
|
86
|
+
if not dry_run and not os.access(log_file_path, os.W_OK):
|
|
46
87
|
print(f"Error: No write permission for '{log_file_path}'")
|
|
47
88
|
sys.exit(1)
|
|
48
89
|
|
|
@@ -51,47 +92,20 @@ def clean_log_file(log_file_path, dry_run=False):
|
|
|
51
92
|
print(f"Performing a dry run on: {log_file_path}")
|
|
52
93
|
|
|
53
94
|
temp_file_path = log_file_path + ".tmp"
|
|
54
|
-
|
|
55
|
-
patterns = [
|
|
56
|
-
r"INFO\s*-\s*Inspecting\s*directory",
|
|
57
|
-
r"INFO\s*-\s*Finished\s*Inspecting",
|
|
58
|
-
r"INFO\s*-\s*<File",
|
|
59
|
-
r"INFO\s*-\s*</File",
|
|
60
|
-
r"INFO\s*-\s*<Attributes",
|
|
61
|
-
r"INFO\s*-\s*</Attributes",
|
|
62
|
-
r"INFO\s*-\s*</Directory",
|
|
63
|
-
r"INFO\s*-\s*<Directory",
|
|
64
|
-
r"INFO\s*-\s*<Catalog",
|
|
65
|
-
r"INFO\s*-\s*</Catalog",
|
|
66
|
-
r"INFO\s*-\s*<Symlink",
|
|
67
|
-
r"INFO\s*-\s*</Symlink",
|
|
68
|
-
]
|
|
69
95
|
|
|
70
96
|
try:
|
|
71
|
-
|
|
97
|
+
if dry_run:
|
|
98
|
+
with open(log_file_path, "r", errors="ignore") as infile:
|
|
99
|
+
for line in infile:
|
|
100
|
+
if _should_remove_line(line):
|
|
101
|
+
print(f"Would remove: {line.strip()}")
|
|
102
|
+
return
|
|
72
103
|
|
|
104
|
+
with open(log_file_path, "r", errors="ignore") as infile, open(temp_file_path, "w") as outfile:
|
|
73
105
|
for line in infile:
|
|
74
|
-
|
|
75
|
-
matched = False # Track if a pattern is matched
|
|
76
|
-
|
|
77
|
-
for pattern in patterns:
|
|
78
|
-
if re.search(pattern, line): # Check if the pattern matches
|
|
79
|
-
if dry_run:
|
|
80
|
-
print(f"Would remove: {original_line.strip()}") # Print full line for dry-run
|
|
81
|
-
matched = True # Mark that a pattern matched
|
|
82
|
-
break # No need to check other patterns if one matches
|
|
83
|
-
|
|
84
|
-
if not dry_run and not matched: # In normal mode, only write non-empty lines
|
|
106
|
+
if not _should_remove_line(line):
|
|
85
107
|
outfile.write(line.rstrip() + "\n")
|
|
86
108
|
|
|
87
|
-
if dry_run and matched:
|
|
88
|
-
continue # In dry-run mode, skip writing (since we’re just showing)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# Ensure the temp file exists before renaming
|
|
92
|
-
if not os.path.exists(temp_file_path):
|
|
93
|
-
open(temp_file_path, "w").close() # Create an empty file if nothing was written
|
|
94
|
-
|
|
95
109
|
os.replace(temp_file_path, log_file_path)
|
|
96
110
|
print(f"Successfully cleaned log file: {log_file_path}")
|
|
97
111
|
|
|
@@ -131,43 +145,68 @@ def main():
|
|
|
131
145
|
|
|
132
146
|
args = parser.parse_args()
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
try:
|
|
149
|
+
config_settings = ConfigSettings(os.path.expanduser(os.path.expandvars(args.config_file)))
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
msg = f"Config error: {exc}"
|
|
152
|
+
print(msg, file=sys.stderr)
|
|
153
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
154
|
+
send_discord_message(f"{ts} - clean-log: FAILURE - {msg}")
|
|
155
|
+
sys.exit(127)
|
|
135
156
|
|
|
136
|
-
|
|
137
|
-
args.file
|
|
157
|
+
try:
|
|
158
|
+
files_to_clean = args.file if args.file else [config_settings.logfile_location]
|
|
159
|
+
logfile_dir = os.path.dirname(os.path.realpath(config_settings.logfile_location))
|
|
160
|
+
validated_files = []
|
|
138
161
|
|
|
139
|
-
|
|
162
|
+
for file_path in files_to_clean:
|
|
163
|
+
if not isinstance(file_path, (str, bytes, os.PathLike)):
|
|
164
|
+
print(f"Error: Invalid file path type: {file_path}")
|
|
165
|
+
sys.exit(1)
|
|
140
166
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
file_path = os.fspath(file_path)
|
|
168
|
+
if isinstance(file_path, bytes):
|
|
169
|
+
file_path = os.fsdecode(file_path)
|
|
144
170
|
|
|
145
|
-
|
|
146
|
-
|
|
171
|
+
if file_path.strip() == "":
|
|
172
|
+
print(f"Error: Invalid empty filename '{file_path}'.")
|
|
173
|
+
sys.exit(1)
|
|
147
174
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
if ".." in os.path.normpath(file_path).split(os.sep):
|
|
176
|
+
print(f"Error: Path traversal is not allowed: '{file_path}'")
|
|
177
|
+
sys.exit(1)
|
|
151
178
|
|
|
152
|
-
|
|
153
|
-
if not isinstance(file_path, (str, bytes, os.PathLike)):
|
|
154
|
-
print(f"Error: Invalid file path type: {file_path}")
|
|
155
|
-
sys.exit(1)
|
|
179
|
+
resolved_path = os.path.realpath(file_path)
|
|
156
180
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
if not resolved_path.startswith(logfile_dir + os.sep):
|
|
182
|
+
print(f"Error: File is outside allowed directory: '{file_path}'")
|
|
183
|
+
sys.exit(1)
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
if not os.path.exists(file_path):
|
|
186
|
+
print(f"Error: Log file '{file_path}' does not exist.")
|
|
187
|
+
sys.exit(1)
|
|
164
188
|
|
|
165
|
-
|
|
166
|
-
# Run the log file cleaning function
|
|
167
|
-
for log_file in args.file:
|
|
168
|
-
clean_log_file(log_file, dry_run=args.dry_run)
|
|
169
|
-
print(f"Log file '{args.file}' has been cleaned successfully.")
|
|
189
|
+
validated_files.append(file_path)
|
|
170
190
|
|
|
171
191
|
|
|
192
|
+
# Run the log file cleaning function
|
|
193
|
+
for log_file in validated_files:
|
|
194
|
+
clean_log_file(log_file, dry_run=args.dry_run)
|
|
195
|
+
file_list = ", ".join(validated_files)
|
|
196
|
+
if args.dry_run:
|
|
197
|
+
print(f"Dry run complete for: {file_list}")
|
|
198
|
+
else:
|
|
199
|
+
print(f"Log file '{file_list}' has been cleaned successfully.")
|
|
200
|
+
except Exception as e:
|
|
201
|
+
msg = f"Unexpected error during clean-log: {e}"
|
|
202
|
+
logger = get_logger()
|
|
203
|
+
if logger:
|
|
204
|
+
logger.error(msg, exc_info=True)
|
|
205
|
+
else:
|
|
206
|
+
print(msg, file=sys.stderr)
|
|
207
|
+
|
|
208
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
209
|
+
send_discord_message(f"{ts} - clean-log: FAILURE - {msg}", config_settings=config_settings)
|
|
210
|
+
sys.exit(1)
|
|
172
211
|
if __name__ == "__main__":
|
|
173
212
|
main()
|
dar_backup/cleanup.py
CHANGED
|
@@ -16,10 +16,8 @@ This script removes old DIFF and INCR archives + accompanying .par2 files accord
|
|
|
16
16
|
|
|
17
17
|
import argcomplete
|
|
18
18
|
import argparse
|
|
19
|
-
import logging
|
|
20
19
|
import os
|
|
21
20
|
import re
|
|
22
|
-
import subprocess
|
|
23
21
|
import sys
|
|
24
22
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
|
25
23
|
|
|
@@ -29,7 +27,7 @@ from inputimeout import inputimeout, TimeoutOccurred
|
|
|
29
27
|
from pathlib import Path
|
|
30
28
|
from sys import stderr
|
|
31
29
|
from time import time
|
|
32
|
-
from typing import
|
|
30
|
+
from typing import List, NamedTuple, Tuple
|
|
33
31
|
import glob
|
|
34
32
|
|
|
35
33
|
|
|
@@ -48,6 +46,7 @@ from dar_backup.util import is_archive_name_allowed
|
|
|
48
46
|
from dar_backup.util import is_safe_filename
|
|
49
47
|
from dar_backup.util import safe_remove_file
|
|
50
48
|
from dar_backup.util import show_scriptname
|
|
49
|
+
from dar_backup.util import send_discord_message
|
|
51
50
|
|
|
52
51
|
from dar_backup.command_runner import CommandRunner
|
|
53
52
|
from dar_backup.command_runner import CommandResult
|
|
@@ -67,7 +66,6 @@ def _delete_par2_files(
|
|
|
67
66
|
else:
|
|
68
67
|
par2_config = {
|
|
69
68
|
"par2_dir": None,
|
|
70
|
-
"par2_mode": None,
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
par2_dir = par2_config.get("par2_dir") or backup_dir
|
|
@@ -76,49 +74,33 @@ def _delete_par2_files(
|
|
|
76
74
|
logger.warning(f"PAR2 directory not found, skipping cleanup: {par2_dir}")
|
|
77
75
|
return
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
targets
|
|
84
|
-
manifest_path = os.path.join(par2_dir, f"{archive_name}.par2.manifest.ini")
|
|
85
|
-
if os.path.exists(manifest_path):
|
|
86
|
-
targets.append(manifest_path)
|
|
87
|
-
if not targets:
|
|
88
|
-
logger.info("No par2 files matched the per-archive cleanup pattern.")
|
|
89
|
-
return
|
|
90
|
-
for file_path in sorted(set(targets)):
|
|
91
|
-
try:
|
|
92
|
-
if dry_run:
|
|
93
|
-
logger.info(f"Dry run: would delete PAR2 file: {file_path}")
|
|
94
|
-
else:
|
|
95
|
-
safe_remove_file(file_path, base_dir=Path(par2_dir))
|
|
96
|
-
logger.info(f"Deleted PAR2 file: {file_path}")
|
|
97
|
-
except Exception as e:
|
|
98
|
-
logger.error(f"Error deleting PAR2 file {file_path}: {e}")
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
if par2_mode != "per-slice":
|
|
102
|
-
logger.error(f"Unsupported PAR2_MODE during cleanup: {par2_mode}")
|
|
103
|
-
return
|
|
77
|
+
par2_glob = os.path.join(par2_dir, f"{archive_name}*.par2")
|
|
78
|
+
targets = set(glob.glob(par2_glob))
|
|
79
|
+
manifest_path = os.path.join(par2_dir, f"{archive_name}.par2.manifest.ini")
|
|
80
|
+
if os.path.exists(manifest_path):
|
|
81
|
+
targets.add(manifest_path)
|
|
104
82
|
|
|
105
83
|
par2_regex = re.compile(rf"^{re.escape(archive_name)}\.[0-9]+\.dar.*\.par2$")
|
|
106
|
-
|
|
107
|
-
|
|
84
|
+
for entry in os.scandir(par2_dir):
|
|
85
|
+
if not entry.is_file():
|
|
86
|
+
continue
|
|
87
|
+
filename = entry.name
|
|
108
88
|
if par2_regex.match(filename):
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
if dry_run:
|
|
112
|
-
logger.info(f"Dry run: would delete PAR2 file: {file_path}")
|
|
113
|
-
else:
|
|
114
|
-
safe_remove_file(file_path, base_dir=Path(par2_dir))
|
|
115
|
-
logger.info(f"Deleted PAR2 file: {file_path}")
|
|
116
|
-
files_deleted = True
|
|
117
|
-
except Exception as e:
|
|
118
|
-
logger.error(f"Error deleting PAR2 file {file_path}: {e}")
|
|
89
|
+
targets.add(entry.path)
|
|
119
90
|
|
|
120
|
-
if not
|
|
121
|
-
logger.info("No
|
|
91
|
+
if not targets:
|
|
92
|
+
logger.info("No par2 files matched the cleanup patterns.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
for file_path in sorted(targets):
|
|
96
|
+
try:
|
|
97
|
+
if dry_run:
|
|
98
|
+
logger.info(f"Dry run: would delete PAR2 file: {file_path}")
|
|
99
|
+
else:
|
|
100
|
+
safe_remove_file(file_path, base_dir=Path(par2_dir))
|
|
101
|
+
logger.info(f"Deleted PAR2 file: {file_path}")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Error deleting PAR2 file {file_path}: {e}")
|
|
122
104
|
|
|
123
105
|
|
|
124
106
|
def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=None, config_settings: ConfigSettings = None):
|
|
@@ -138,7 +120,10 @@ def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=Non
|
|
|
138
120
|
archives_deleted = {}
|
|
139
121
|
|
|
140
122
|
dry_run = getattr(args, "dry_run", False) is True
|
|
141
|
-
for
|
|
123
|
+
for entry in os.scandir(backup_dir):
|
|
124
|
+
if not entry.is_file():
|
|
125
|
+
continue
|
|
126
|
+
filename = entry.name
|
|
142
127
|
if not filename.endswith('.dar'):
|
|
143
128
|
continue
|
|
144
129
|
if backup_definition and not filename.startswith(backup_definition):
|
|
@@ -152,7 +137,7 @@ def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=Non
|
|
|
152
137
|
raise
|
|
153
138
|
|
|
154
139
|
if file_date < cutoff_date:
|
|
155
|
-
file_path =
|
|
140
|
+
file_path = entry.path
|
|
156
141
|
try:
|
|
157
142
|
if dry_run:
|
|
158
143
|
logger.info(f"Dry run: would delete {backup_type} backup: {file_path}")
|
|
@@ -160,7 +145,7 @@ def delete_old_backups(backup_dir, age, backup_type, args, backup_definition=Non
|
|
|
160
145
|
safe_remove_file(file_path, base_dir=Path(backup_dir))
|
|
161
146
|
logger.info(f"Deleted {backup_type} backup: {file_path}")
|
|
162
147
|
archive_name = filename.split('.')[0]
|
|
163
|
-
if not
|
|
148
|
+
if archive_name not in archives_deleted:
|
|
164
149
|
logger.debug(f"Archive name: '{archive_name}' added to catalog deletion list")
|
|
165
150
|
archives_deleted[archive_name] = True
|
|
166
151
|
except Exception as e:
|
|
@@ -190,9 +175,12 @@ def delete_archive(backup_dir, archive_name, args, config_settings: ConfigSettin
|
|
|
190
175
|
# Delete the specified .dar files according to the naming convention
|
|
191
176
|
files_deleted = False
|
|
192
177
|
dry_run = getattr(args, "dry_run", False) is True
|
|
193
|
-
for
|
|
178
|
+
for entry in os.scandir(backup_dir):
|
|
179
|
+
if not entry.is_file():
|
|
180
|
+
continue
|
|
181
|
+
filename = entry.name
|
|
194
182
|
if archive_regex.match(filename):
|
|
195
|
-
file_path =
|
|
183
|
+
file_path = entry.path
|
|
196
184
|
try:
|
|
197
185
|
if dry_run:
|
|
198
186
|
logger.info(f"Dry run: would delete archive slice: {file_path}")
|
|
@@ -219,7 +207,7 @@ def delete_catalog(catalog_name: str, args: NamedTuple) -> bool:
|
|
|
219
207
|
"""
|
|
220
208
|
Call `manager.py` to delete the specified catalog in it's database
|
|
221
209
|
"""
|
|
222
|
-
command = [
|
|
210
|
+
command = ["manager", "--remove-specific-archive", catalog_name, "--config-file", args.config_file, '--log-level', 'debug', '--log-stdout']
|
|
223
211
|
logger.info(f"Deleting catalog '{catalog_name}' using config file: '{args.config_file}'")
|
|
224
212
|
try:
|
|
225
213
|
result:CommandResult = runner.run(command)
|
|
@@ -308,15 +296,35 @@ def main():
|
|
|
308
296
|
raise SystemExit(127)
|
|
309
297
|
args.config_file = config_settings_path
|
|
310
298
|
|
|
311
|
-
|
|
299
|
+
try:
|
|
300
|
+
config_settings = ConfigSettings(args.config_file)
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
msg = f"Config error: {exc}"
|
|
303
|
+
print(msg, file=stderr)
|
|
304
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
305
|
+
send_discord_message(f"{ts} - cleanup: FAILURE - {msg}")
|
|
306
|
+
sys.exit(127)
|
|
312
307
|
|
|
313
308
|
start_time=int(time())
|
|
314
309
|
|
|
315
310
|
# command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
|
|
316
311
|
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
317
|
-
logger = setup_logging(
|
|
312
|
+
logger = setup_logging(
|
|
313
|
+
config_settings.logfile_location,
|
|
314
|
+
command_output_log,
|
|
315
|
+
args.log_level,
|
|
316
|
+
args.log_stdout,
|
|
317
|
+
logfile_max_bytes=config_settings.logfile_max_bytes,
|
|
318
|
+
logfile_backup_count=config_settings.logfile_backup_count,
|
|
319
|
+
trace_log_max_bytes=getattr(config_settings, "trace_log_max_bytes", 10485760),
|
|
320
|
+
trace_log_backup_count=getattr(config_settings, "trace_log_backup_count", 1)
|
|
321
|
+
)
|
|
318
322
|
command_logger = get_logger(command_output_logger = True)
|
|
319
|
-
runner = CommandRunner(
|
|
323
|
+
runner = CommandRunner(
|
|
324
|
+
logger=logger,
|
|
325
|
+
command_logger=command_logger,
|
|
326
|
+
default_capture_limit_bytes=getattr(config_settings, "command_capture_max_bytes", None)
|
|
327
|
+
)
|
|
320
328
|
|
|
321
329
|
start_msgs: List[Tuple[str, str]] = []
|
|
322
330
|
|
|
@@ -341,65 +349,89 @@ def main():
|
|
|
341
349
|
print_aligned_settings(start_msgs, highlight_keywords=dangerous_keywords, quiet=not args.verbose)
|
|
342
350
|
|
|
343
351
|
# run PREREQ scripts
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
352
|
+
try:
|
|
353
|
+
requirements('PREREQ', config_settings)
|
|
354
|
+
except Exception as exc:
|
|
355
|
+
msg = f"PREREQ failed: {exc}"
|
|
356
|
+
logger.error(msg)
|
|
357
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
358
|
+
send_discord_message(f"{ts} - cleanup: FAILURE - {msg}", config_settings=config_settings)
|
|
359
|
+
sys.exit(1)
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
if args.alternate_archive_dir:
|
|
363
|
+
if not os.path.exists(args.alternate_archive_dir):
|
|
364
|
+
logger.error(f"Alternate archive directory does not exist: {args.alternate_archive_dir}, exiting")
|
|
365
|
+
sys.exit(1)
|
|
366
|
+
if not os.path.isdir(args.alternate_archive_dir):
|
|
367
|
+
logger.error("Alternate archive directory is not a directory, exiting")
|
|
368
|
+
sys.exit(1)
|
|
369
|
+
config_settings.backup_dir = args.alternate_archive_dir
|
|
370
|
+
|
|
371
|
+
if args.cleanup_specific_archives is None and args.test_mode:
|
|
372
|
+
logger.info("No --cleanup-specific-archives provided; skipping specific archive deletion in test mode.")
|
|
373
|
+
|
|
374
|
+
if args.cleanup_specific_archives or args.cleanup_specific_archives_list:
|
|
375
|
+
combined = []
|
|
376
|
+
if args.cleanup_specific_archives:
|
|
377
|
+
combined.extend(args.cleanup_specific_archives.split(','))
|
|
378
|
+
combined.extend(args.cleanup_specific_archives_list or [])
|
|
379
|
+
archive_names = [name.strip() for name in combined if name.strip()]
|
|
380
|
+
logger.info(f"Cleaning up specific archives: {', '.join(archive_names)}")
|
|
381
|
+
for archive_name in archive_names:
|
|
382
|
+
if not is_archive_name_allowed(archive_name):
|
|
383
|
+
logger.error(f"Refusing unsafe archive name: {archive_name}")
|
|
368
384
|
continue
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
backup_definitions.append(args.backup_definition)
|
|
385
|
+
if "_FULL_" in archive_name:
|
|
386
|
+
if not confirm_full_archive_deletion(archive_name, args.test_mode):
|
|
387
|
+
continue
|
|
388
|
+
archive_path = os.path.join(config_settings.backup_dir, archive_name.strip())
|
|
389
|
+
logger.info(f"Deleting archive: {archive_path}")
|
|
390
|
+
delete_archive(config_settings.backup_dir, archive_name.strip(), args, config_settings)
|
|
391
|
+
elif args.list:
|
|
392
|
+
list_backups(config_settings.backup_dir, args.backup_definition)
|
|
378
393
|
else:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
394
|
+
backup_definitions = []
|
|
395
|
+
if args.backup_definition:
|
|
396
|
+
backup_definitions.append(args.backup_definition)
|
|
397
|
+
else:
|
|
398
|
+
for root, _, files in os.walk(config_settings.backup_d_dir):
|
|
399
|
+
for file in files:
|
|
400
|
+
backup_definitions.append(file.split('.')[0])
|
|
401
|
+
|
|
402
|
+
for definition in backup_definitions:
|
|
403
|
+
delete_old_backups(
|
|
404
|
+
config_settings.backup_dir,
|
|
405
|
+
config_settings.diff_age,
|
|
406
|
+
'DIFF',
|
|
407
|
+
args,
|
|
408
|
+
backup_definition=definition,
|
|
409
|
+
config_settings=config_settings
|
|
410
|
+
)
|
|
411
|
+
delete_old_backups(
|
|
412
|
+
config_settings.backup_dir,
|
|
413
|
+
config_settings.incr_age,
|
|
414
|
+
'INCR',
|
|
415
|
+
args,
|
|
416
|
+
backup_definition=definition,
|
|
417
|
+
config_settings=config_settings
|
|
418
|
+
)
|
|
419
|
+
except Exception as e:
|
|
420
|
+
msg = f"Unexpected error during cleanup: {e}"
|
|
421
|
+
logger.error(msg, exc_info=True)
|
|
422
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
423
|
+
send_discord_message(f"{ts} - cleanup: FAILURE - {msg}", config_settings=config_settings)
|
|
424
|
+
sys.exit(1)
|
|
400
425
|
|
|
401
426
|
# run POST scripts
|
|
402
|
-
|
|
427
|
+
try:
|
|
428
|
+
requirements('POSTREQ', config_settings)
|
|
429
|
+
except Exception as exc:
|
|
430
|
+
msg = f"POSTREQ failed: {exc}"
|
|
431
|
+
logger.error(msg)
|
|
432
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
433
|
+
send_discord_message(f"{ts} - cleanup: FAILURE - {msg}", config_settings=config_settings)
|
|
434
|
+
sys.exit(1)
|
|
403
435
|
|
|
404
436
|
|
|
405
437
|
end_time=int(time())
|