dar-backup 1.0.0.1__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/Changelog.md +64 -1
- dar_backup/README.md +355 -34
- dar_backup/__about__.py +3 -2
- dar_backup/clean_log.py +102 -63
- dar_backup/cleanup.py +225 -76
- dar_backup/command_runner.py +198 -103
- dar_backup/config_settings.py +158 -1
- dar_backup/dar-backup.conf +18 -0
- dar_backup/dar-backup.conf.j2 +44 -0
- dar_backup/dar_backup.py +806 -131
- dar_backup/demo.py +18 -9
- dar_backup/installer.py +18 -1
- dar_backup/manager.py +304 -91
- dar_backup/util.py +502 -141
- {dar_backup-1.0.0.1.dist-info → dar_backup-1.0.2.dist-info}/METADATA +358 -37
- dar_backup-1.0.2.dist-info/RECORD +25 -0
- {dar_backup-1.0.0.1.dist-info → dar_backup-1.0.2.dist-info}/WHEEL +1 -1
- dar_backup-1.0.0.1.dist-info/RECORD +0 -25
- {dar_backup-1.0.0.1.dist-info → dar_backup-1.0.2.dist-info}/entry_points.txt +0 -0
- {dar_backup-1.0.0.1.dist-info → dar_backup-1.0.2.dist-info}/licenses/LICENSE +0 -0
dar_backup/demo.py
CHANGED
|
@@ -37,17 +37,22 @@ DAR_BACKUP_DIR = util.normalize_dir(util.expand_path("~/dar-backup"))
|
|
|
37
37
|
|
|
38
38
|
def check_directories(args, vars_map: Dict[str,str]) -> bool:
|
|
39
39
|
"""
|
|
40
|
-
Check if
|
|
40
|
+
Check if target paths already exist and are directories.
|
|
41
41
|
|
|
42
42
|
Returns:
|
|
43
|
-
bool: True if
|
|
43
|
+
bool: True if it is safe to proceed, False otherwise.
|
|
44
44
|
"""
|
|
45
45
|
result = True
|
|
46
46
|
for key in ("DAR_BACKUP_DIR","BACKUP_DIR","TEST_RESTORE_DIR","CONFIG_DIR","BACKUP_D_DIR"):
|
|
47
47
|
path = Path(vars_map[key])
|
|
48
|
-
if path.exists()
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
if path.exists():
|
|
49
|
+
if not path.is_dir():
|
|
50
|
+
print(f"Error: '{path}' exists and is not a directory")
|
|
51
|
+
result = False
|
|
52
|
+
continue
|
|
53
|
+
if not args.override:
|
|
54
|
+
print(f"Directory '{path}' already exists")
|
|
55
|
+
result = False
|
|
51
56
|
return result
|
|
52
57
|
|
|
53
58
|
|
|
@@ -72,12 +77,17 @@ def generate_file(args, template: str, file_path: Path, vars_map: Dict[str, str]
|
|
|
72
77
|
if rendered is None:
|
|
73
78
|
print(f"Error: Template '{template}' could not be rendered.")
|
|
74
79
|
return False
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
if file_path.exists():
|
|
81
|
+
if file_path.is_dir():
|
|
82
|
+
print(f"Error: '{file_path}' is a directory, expected a file.")
|
|
83
|
+
return False
|
|
84
|
+
if not args.override:
|
|
85
|
+
print(f"Error: File '{file_path}' already exists. Use --override to overwrite.")
|
|
86
|
+
return False
|
|
78
87
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
88
|
file_path.write_text(rendered)
|
|
80
89
|
print(f"File generated at '{file_path}'")
|
|
90
|
+
return True
|
|
81
91
|
|
|
82
92
|
|
|
83
93
|
|
|
@@ -152,7 +162,6 @@ def main():
|
|
|
152
162
|
parser.error(
|
|
153
163
|
"Options --root-dir, --dir-to-backup must all be specified together."
|
|
154
164
|
)
|
|
155
|
-
exit(1)
|
|
156
165
|
|
|
157
166
|
args.root_dir = util.normalize_dir(util.expand_path(args.root_dir)) if args.root_dir else None
|
|
158
167
|
args.backup_dir = util.normalize_dir(util.expand_path(args.backup_dir)) if args.backup_dir else None
|
dar_backup/installer.py
CHANGED
|
@@ -43,6 +43,9 @@ def install_autocompletion():
|
|
|
43
43
|
|
|
44
44
|
# ensure RC file and parent directory exist
|
|
45
45
|
rc_file.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
if rc_file.exists() and rc_file.is_dir():
|
|
47
|
+
print(f"Error: RC path is a directory: {rc_file}")
|
|
48
|
+
return
|
|
46
49
|
if not rc_file.exists():
|
|
47
50
|
rc_file.touch()
|
|
48
51
|
|
|
@@ -76,11 +79,17 @@ def uninstall_autocompletion() -> str:
|
|
|
76
79
|
if not rc_file.exists():
|
|
77
80
|
print(f"❌ RC file not found: {rc_file}")
|
|
78
81
|
return
|
|
82
|
+
if rc_file.is_dir():
|
|
83
|
+
print(f"Error: RC path is a directory: {rc_file}")
|
|
84
|
+
return
|
|
79
85
|
|
|
80
86
|
content = rc_file.read_text()
|
|
81
87
|
if marker not in content:
|
|
82
88
|
print(f"No autocompletion block found in {rc_file}")
|
|
83
89
|
return f"No autocompletion block found in {rc_file}" # for unit test
|
|
90
|
+
if end_marker not in content:
|
|
91
|
+
print(f"Error: Autocompletion end marker not found in {rc_file}")
|
|
92
|
+
return f"Autocompletion end marker not found in {rc_file}"
|
|
84
93
|
|
|
85
94
|
lines = content.splitlines(keepends=True)
|
|
86
95
|
new_lines = []
|
|
@@ -128,7 +137,11 @@ def run_installer(config_file: str, create_db_flag: bool):
|
|
|
128
137
|
log_to_stdout=True,
|
|
129
138
|
)
|
|
130
139
|
command_logger = get_logger(command_output_logger=True)
|
|
131
|
-
runner = CommandRunner(
|
|
140
|
+
runner = CommandRunner(
|
|
141
|
+
logger=logger,
|
|
142
|
+
command_logger=command_logger,
|
|
143
|
+
default_capture_limit_bytes=getattr(config_settings, "command_capture_max_bytes", None)
|
|
144
|
+
)
|
|
132
145
|
|
|
133
146
|
|
|
134
147
|
# Create required directories
|
|
@@ -151,6 +164,10 @@ def run_installer(config_file: str, create_db_flag: bool):
|
|
|
151
164
|
# Optionally create databases for all backup definitions
|
|
152
165
|
if create_db_flag:
|
|
153
166
|
for file in os.listdir(config_settings.backup_d_dir):
|
|
167
|
+
file_path = os.path.join(config_settings.backup_d_dir, file)
|
|
168
|
+
if not os.path.isfile(file_path):
|
|
169
|
+
logger.info(f"Skipping non-file backup definition: {file}")
|
|
170
|
+
continue
|
|
154
171
|
backup_def = os.path.basename(file)
|
|
155
172
|
print(f"Creating catalog for: {backup_def}")
|
|
156
173
|
result = create_db(backup_def, config_settings, logger, runner)
|
dar_backup/manager.py
CHANGED
|
@@ -27,6 +27,8 @@ import os
|
|
|
27
27
|
import re
|
|
28
28
|
import sys
|
|
29
29
|
import subprocess
|
|
30
|
+
import threading
|
|
31
|
+
import shlex
|
|
30
32
|
|
|
31
33
|
from inputimeout import inputimeout, TimeoutOccurred
|
|
32
34
|
|
|
@@ -35,6 +37,8 @@ from . import __about__ as about
|
|
|
35
37
|
from dar_backup.config_settings import ConfigSettings
|
|
36
38
|
from dar_backup.util import setup_logging
|
|
37
39
|
from dar_backup.util import CommandResult
|
|
40
|
+
from dar_backup.util import get_config_file
|
|
41
|
+
from dar_backup.util import send_discord_message
|
|
38
42
|
from dar_backup.util import get_logger
|
|
39
43
|
from dar_backup.util import get_binary_info
|
|
40
44
|
from dar_backup.util import show_version
|
|
@@ -47,6 +51,7 @@ from dar_backup.command_runner import CommandResult
|
|
|
47
51
|
from dar_backup.util import backup_definition_completer, list_archive_completer, archive_content_completer, add_specific_archive_completer
|
|
48
52
|
|
|
49
53
|
from datetime import datetime
|
|
54
|
+
from sys import stderr
|
|
50
55
|
from time import time
|
|
51
56
|
from typing import Dict, List, NamedTuple, Tuple
|
|
52
57
|
|
|
@@ -60,6 +65,25 @@ logger = None
|
|
|
60
65
|
runner = None
|
|
61
66
|
|
|
62
67
|
|
|
68
|
+
def _open_command_log(command: List[str]):
|
|
69
|
+
command_logger = get_logger(command_output_logger=True)
|
|
70
|
+
log_path = None
|
|
71
|
+
for handler in getattr(command_logger, "handlers", []):
|
|
72
|
+
if hasattr(handler, "baseFilename"):
|
|
73
|
+
log_path = handler.baseFilename
|
|
74
|
+
break
|
|
75
|
+
if not log_path:
|
|
76
|
+
return None, None
|
|
77
|
+
log_file = open(log_path, "ab")
|
|
78
|
+
header = (
|
|
79
|
+
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - COMMAND: "
|
|
80
|
+
f"{' '.join(map(shlex.quote, command))}\n"
|
|
81
|
+
).encode("utf-8", errors="replace")
|
|
82
|
+
log_file.write(header)
|
|
83
|
+
log_file.flush()
|
|
84
|
+
return log_file, threading.Lock()
|
|
85
|
+
|
|
86
|
+
|
|
63
87
|
def get_db_dir(config_settings: ConfigSettings) -> str:
|
|
64
88
|
"""
|
|
65
89
|
Return the correct directory for storing catalog databases.
|
|
@@ -129,24 +153,106 @@ def list_catalogs(backup_def: str, config_settings: ConfigSettings, suppress_out
|
|
|
129
153
|
return CommandResult(1, '', error_msg)
|
|
130
154
|
|
|
131
155
|
command = ['dar_manager', '--base', database_path, '--list']
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
if runner is not None and not hasattr(runner, "default_capture_limit_bytes"):
|
|
157
|
+
process = runner.run(command, capture_output_limit_bytes=-1)
|
|
158
|
+
stdout, stderr = process.stdout, process.stderr
|
|
134
159
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
line
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
if process.returncode != 0:
|
|
161
|
+
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
162
|
+
logger.error(f"stderr: {stderr}")
|
|
163
|
+
logger.error(f"stdout: {stdout}")
|
|
164
|
+
return process
|
|
165
|
+
|
|
166
|
+
# Extract only archive basenames from stdout
|
|
167
|
+
archive_names = []
|
|
168
|
+
archive_lines = []
|
|
169
|
+
for line in stdout.splitlines():
|
|
170
|
+
line = line.strip()
|
|
171
|
+
if not line or "archive #" in line or "dar path" in line or "compression" in line:
|
|
172
|
+
continue
|
|
173
|
+
parts = line.split("\t")
|
|
174
|
+
if len(parts) >= 3:
|
|
175
|
+
archive_names.append(parts[2].strip())
|
|
176
|
+
archive_lines.append(line)
|
|
177
|
+
else:
|
|
178
|
+
stderr_lines: List[str] = []
|
|
179
|
+
stderr_bytes = 0
|
|
180
|
+
cap = getattr(config_settings, "command_capture_max_bytes", None)
|
|
181
|
+
if not isinstance(cap, int):
|
|
182
|
+
cap = None
|
|
183
|
+
log_file, log_lock = _open_command_log(command)
|
|
184
|
+
|
|
185
|
+
process = subprocess.Popen(
|
|
186
|
+
command,
|
|
187
|
+
stdout=subprocess.PIPE,
|
|
188
|
+
stderr=subprocess.PIPE,
|
|
189
|
+
text=False,
|
|
190
|
+
bufsize=0
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def read_stderr():
|
|
194
|
+
nonlocal stderr_bytes
|
|
195
|
+
if process.stderr is None:
|
|
196
|
+
return
|
|
197
|
+
while True:
|
|
198
|
+
chunk = process.stderr.read(1024)
|
|
199
|
+
if not chunk:
|
|
200
|
+
break
|
|
201
|
+
if log_file:
|
|
202
|
+
with log_lock:
|
|
203
|
+
log_file.write(chunk)
|
|
204
|
+
log_file.flush()
|
|
205
|
+
if cap is None:
|
|
206
|
+
stderr_lines.append(chunk)
|
|
207
|
+
elif cap > 0 and stderr_bytes < cap:
|
|
208
|
+
remaining = cap - stderr_bytes
|
|
209
|
+
if len(chunk) <= remaining:
|
|
210
|
+
stderr_lines.append(chunk)
|
|
211
|
+
stderr_bytes += len(chunk)
|
|
212
|
+
else:
|
|
213
|
+
stderr_lines.append(chunk[:remaining])
|
|
214
|
+
stderr_bytes = cap
|
|
215
|
+
|
|
216
|
+
stderr_thread = threading.Thread(target=read_stderr)
|
|
217
|
+
stderr_thread.start()
|
|
218
|
+
|
|
219
|
+
archive_names = []
|
|
220
|
+
archive_lines = []
|
|
221
|
+
if process.stdout is not None:
|
|
222
|
+
buffer = b""
|
|
223
|
+
while True:
|
|
224
|
+
chunk = process.stdout.read(1024)
|
|
225
|
+
if not chunk:
|
|
226
|
+
break
|
|
227
|
+
if log_file:
|
|
228
|
+
with log_lock:
|
|
229
|
+
log_file.write(chunk)
|
|
230
|
+
buffer += chunk
|
|
231
|
+
while b"\n" in buffer:
|
|
232
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
233
|
+
stripped = line.strip()
|
|
234
|
+
if not stripped:
|
|
235
|
+
continue
|
|
236
|
+
decoded = stripped.decode("utf-8", errors="replace")
|
|
237
|
+
if "archive #" in decoded or "dar path" in decoded or "compression" in decoded:
|
|
238
|
+
continue
|
|
239
|
+
parts = decoded.split("\t")
|
|
240
|
+
if len(parts) >= 3:
|
|
241
|
+
archive_names.append(parts[2].strip())
|
|
242
|
+
archive_lines.append(decoded)
|
|
243
|
+
process.stdout.close()
|
|
244
|
+
|
|
245
|
+
process.wait()
|
|
246
|
+
stderr_thread.join()
|
|
247
|
+
if log_file:
|
|
248
|
+
log_file.close()
|
|
249
|
+
|
|
250
|
+
if process.returncode != 0:
|
|
251
|
+
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
252
|
+
stderr_text = "".join(stderr_lines)
|
|
253
|
+
if stderr_text:
|
|
254
|
+
logger.error(f"stderr: {stderr_text}")
|
|
255
|
+
return CommandResult(process.returncode, "", stderr_text)
|
|
150
256
|
|
|
151
257
|
# Sort by prefix and date
|
|
152
258
|
def extract_date(arch_name):
|
|
@@ -165,7 +271,7 @@ def list_catalogs(backup_def: str, config_settings: ConfigSettings, suppress_out
|
|
|
165
271
|
for name in archive_names:
|
|
166
272
|
print(name)
|
|
167
273
|
|
|
168
|
-
return
|
|
274
|
+
return CommandResult(0, "\n".join(archive_lines), "")
|
|
169
275
|
|
|
170
276
|
|
|
171
277
|
def cat_no_for_name(archive: str, config_settings: ConfigSettings) -> int:
|
|
@@ -182,9 +288,7 @@ def cat_no_for_name(archive: str, config_settings: ConfigSettings) -> int:
|
|
|
182
288
|
if process.returncode != 0:
|
|
183
289
|
logger.error(f"Error listing catalogs for backup def: '{backup_def}'")
|
|
184
290
|
return -1
|
|
185
|
-
line_no = 1
|
|
186
291
|
for line in process.stdout.splitlines():
|
|
187
|
-
line_no += 1
|
|
188
292
|
search = re.search(rf".*?(\d+)\s+.*?({archive}).*", line)
|
|
189
293
|
if search:
|
|
190
294
|
logger.info(f"Found archive: '{archive}', catalog #: '{search.group(1)}'")
|
|
@@ -213,26 +317,104 @@ def list_archive_contents(archive: str, config_settings: ConfigSettings) -> int:
|
|
|
213
317
|
|
|
214
318
|
|
|
215
319
|
command = ['dar_manager', '--base', database_path, '-u', f"{cat_no}"]
|
|
216
|
-
|
|
320
|
+
if runner is not None and not hasattr(runner, "default_capture_limit_bytes"):
|
|
321
|
+
process = runner.run(command, timeout=10)
|
|
322
|
+
stdout = process.stdout or ""
|
|
323
|
+
stderr = process.stderr or ""
|
|
324
|
+
if process.returncode != 0:
|
|
325
|
+
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
326
|
+
logger.error(f"stderr: {stderr}")
|
|
327
|
+
logger.error(f"stdout: {stdout}")
|
|
328
|
+
return process.returncode
|
|
217
329
|
|
|
330
|
+
combined_lines = (stdout + "\n" + stderr).splitlines()
|
|
331
|
+
file_lines = [line for line in combined_lines if line.strip().startswith("[ Saved ]")]
|
|
218
332
|
|
|
219
|
-
|
|
220
|
-
|
|
333
|
+
if file_lines:
|
|
334
|
+
for line in file_lines:
|
|
335
|
+
print(line)
|
|
336
|
+
else:
|
|
337
|
+
print(f"[info] Archive '{archive}' is empty.")
|
|
338
|
+
|
|
339
|
+
return process.returncode
|
|
340
|
+
|
|
341
|
+
stderr_lines: List[str] = []
|
|
342
|
+
stderr_bytes = 0
|
|
343
|
+
cap = getattr(config_settings, "command_capture_max_bytes", None)
|
|
344
|
+
log_file, log_lock = _open_command_log(command)
|
|
345
|
+
|
|
346
|
+
process = subprocess.Popen(
|
|
347
|
+
command,
|
|
348
|
+
stdout=subprocess.PIPE,
|
|
349
|
+
stderr=subprocess.PIPE,
|
|
350
|
+
text=False,
|
|
351
|
+
bufsize=0
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def read_stderr():
|
|
355
|
+
nonlocal stderr_bytes
|
|
356
|
+
if process.stderr is None:
|
|
357
|
+
return
|
|
358
|
+
while True:
|
|
359
|
+
chunk = process.stderr.read(1024)
|
|
360
|
+
if not chunk:
|
|
361
|
+
break
|
|
362
|
+
if log_file:
|
|
363
|
+
with log_lock:
|
|
364
|
+
log_file.write(chunk)
|
|
365
|
+
log_file.flush()
|
|
366
|
+
if cap is None:
|
|
367
|
+
stderr_lines.append(chunk)
|
|
368
|
+
elif cap > 0 and stderr_bytes < cap:
|
|
369
|
+
remaining = cap - stderr_bytes
|
|
370
|
+
if len(chunk) <= remaining:
|
|
371
|
+
stderr_lines.append(chunk)
|
|
372
|
+
stderr_bytes += len(chunk)
|
|
373
|
+
else:
|
|
374
|
+
stderr_lines.append(chunk[:remaining])
|
|
375
|
+
stderr_bytes = cap
|
|
376
|
+
|
|
377
|
+
stderr_thread = threading.Thread(target=read_stderr)
|
|
378
|
+
stderr_thread.start()
|
|
379
|
+
|
|
380
|
+
found = False
|
|
381
|
+
if process.stdout is not None:
|
|
382
|
+
buffer = b""
|
|
383
|
+
while True:
|
|
384
|
+
chunk = process.stdout.read(1024)
|
|
385
|
+
if not chunk:
|
|
386
|
+
break
|
|
387
|
+
if log_file:
|
|
388
|
+
with log_lock:
|
|
389
|
+
log_file.write(chunk)
|
|
390
|
+
buffer += chunk
|
|
391
|
+
while b"\n" in buffer:
|
|
392
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
393
|
+
if line.strip().startswith(b"[ Saved ]"):
|
|
394
|
+
print(line.decode("utf-8", errors="replace"))
|
|
395
|
+
found = True
|
|
396
|
+
process.stdout.close()
|
|
221
397
|
|
|
398
|
+
try:
|
|
399
|
+
process.wait(timeout=10)
|
|
400
|
+
except subprocess.TimeoutExpired:
|
|
401
|
+
process.kill()
|
|
402
|
+
stderr_thread.join()
|
|
403
|
+
logger.error(f"Timeout listing contents of archive: '{archive}'")
|
|
404
|
+
return 1
|
|
405
|
+
|
|
406
|
+
stderr_thread.join()
|
|
407
|
+
if log_file:
|
|
408
|
+
log_file.close()
|
|
222
409
|
|
|
223
410
|
if process.returncode != 0:
|
|
224
411
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
combined_lines = (stdout + "\n" + stderr).splitlines()
|
|
230
|
-
file_lines = [line for line in combined_lines if line.strip().startswith("[ Saved ]")]
|
|
412
|
+
stderr_text = "".join(stderr_lines)
|
|
413
|
+
if stderr_text:
|
|
414
|
+
logger.error(f"stderr: {stderr_text}")
|
|
415
|
+
return process.returncode
|
|
231
416
|
|
|
232
|
-
if
|
|
233
|
-
for line in file_lines:
|
|
234
|
-
print(line)
|
|
235
|
-
else:
|
|
417
|
+
if not found:
|
|
236
418
|
print(f"[info] Archive '{archive}' is empty.")
|
|
237
419
|
|
|
238
420
|
return process.returncode
|
|
@@ -250,7 +432,7 @@ def list_catalog_contents(catalog_number: int, backup_def: str, config_settings:
|
|
|
250
432
|
logger.error(f'Catalog database not found: "{database_path}"')
|
|
251
433
|
return 1
|
|
252
434
|
command = ['dar_manager', '--base', database_path, '-u', f"{catalog_number}"]
|
|
253
|
-
process = runner.run(command)
|
|
435
|
+
process = runner.run(command, capture_output_limit_bytes=-1)
|
|
254
436
|
stdout, stderr = process.stdout, process.stderr
|
|
255
437
|
if process.returncode != 0:
|
|
256
438
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
@@ -271,7 +453,7 @@ def find_file(file, backup_def, config_settings):
|
|
|
271
453
|
logger.error(f'Database not found: "{database_path}"')
|
|
272
454
|
return 1
|
|
273
455
|
command = ['dar_manager', '--base', database_path, '-f', f"{file}"]
|
|
274
|
-
process = runner.run(command)
|
|
456
|
+
process = runner.run(command, capture_output_limit_bytes=-1)
|
|
275
457
|
stdout, stderr = process.stdout, process.stderr
|
|
276
458
|
if process.returncode != 0:
|
|
277
459
|
logger.error(f'Error finding file: {file} in: "{database_path}"')
|
|
@@ -492,7 +674,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
|
|
|
492
674
|
|
|
493
675
|
def build_arg_parser():
|
|
494
676
|
parser = argparse.ArgumentParser(description="Creates/maintains `dar` database catalogs")
|
|
495
|
-
parser.add_argument('-c', '--config-file', type=str, help="Path to 'dar-backup.conf'", default=
|
|
677
|
+
parser.add_argument('-c', '--config-file', type=str, help="Path to 'dar-backup.conf'", default=None)
|
|
496
678
|
parser.add_argument('--create-db', action='store_true', help='Create missing databases for all backup definitions')
|
|
497
679
|
parser.add_argument('--alternate-archive-dir', type=str, help='Use this directory instead of BACKUP_DIR in config file')
|
|
498
680
|
parser.add_argument('--add-dir', type=str, help='Add all archive catalogs in this directory to databases')
|
|
@@ -536,8 +718,20 @@ def main():
|
|
|
536
718
|
show_version()
|
|
537
719
|
sys.exit(0)
|
|
538
720
|
|
|
539
|
-
|
|
540
|
-
|
|
721
|
+
config_settings_path = get_config_file(args)
|
|
722
|
+
if not (os.path.isfile(config_settings_path) and os.access(config_settings_path, os.R_OK)):
|
|
723
|
+
print(f"Config file {config_settings_path} must exist and be readable.", file=stderr)
|
|
724
|
+
raise SystemExit(127)
|
|
725
|
+
args.config_file = config_settings_path
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
config_settings = ConfigSettings(args.config_file)
|
|
729
|
+
except Exception as exc:
|
|
730
|
+
msg = f"Config error: {exc}"
|
|
731
|
+
print(msg, file=stderr)
|
|
732
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
733
|
+
send_discord_message(f"{ts} - manager: FAILURE - {msg}")
|
|
734
|
+
sys.exit(127)
|
|
541
735
|
|
|
542
736
|
if not os.path.dirname(config_settings.logfile_location):
|
|
543
737
|
print(f"Directory for log file '{config_settings.logfile_location}' does not exist, exiting")
|
|
@@ -545,16 +739,28 @@ def main():
|
|
|
545
739
|
return
|
|
546
740
|
|
|
547
741
|
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
548
|
-
logger = setup_logging(
|
|
742
|
+
logger = setup_logging(
|
|
743
|
+
config_settings.logfile_location,
|
|
744
|
+
command_output_log,
|
|
745
|
+
args.log_level,
|
|
746
|
+
args.log_stdout,
|
|
747
|
+
logfile_max_bytes=config_settings.logfile_max_bytes,
|
|
748
|
+
logfile_backup_count=config_settings.logfile_backup_count,
|
|
749
|
+
trace_log_max_bytes=getattr(config_settings, "trace_log_max_bytes", 10485760),
|
|
750
|
+
trace_log_backup_count=getattr(config_settings, "trace_log_backup_count", 1)
|
|
751
|
+
)
|
|
549
752
|
command_logger = get_logger(command_output_logger=True)
|
|
550
|
-
runner = CommandRunner(
|
|
753
|
+
runner = CommandRunner(
|
|
754
|
+
logger=logger,
|
|
755
|
+
command_logger=command_logger,
|
|
756
|
+
default_capture_limit_bytes=getattr(config_settings, "command_capture_max_bytes", None)
|
|
757
|
+
)
|
|
551
758
|
|
|
552
759
|
start_msgs: List[Tuple[str, str]] = []
|
|
553
760
|
|
|
554
761
|
start_time = int(time())
|
|
555
762
|
|
|
556
763
|
start_msgs.append((f"{show_scriptname()}:", about.__version__))
|
|
557
|
-
logger.info(f"START TIME: {start_time}")
|
|
558
764
|
logger.debug(f"Command line: {get_invocation_command_line()}")
|
|
559
765
|
logger.debug(f"`args`:\n{args}")
|
|
560
766
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
@@ -620,63 +826,70 @@ def main():
|
|
|
620
826
|
return
|
|
621
827
|
|
|
622
828
|
# --- Modify settings ---
|
|
623
|
-
|
|
624
|
-
if
|
|
625
|
-
|
|
626
|
-
|
|
829
|
+
try:
|
|
830
|
+
if args.alternate_archive_dir:
|
|
831
|
+
if not os.path.exists(args.alternate_archive_dir):
|
|
832
|
+
logger.error(f"Alternate archive dir '{args.alternate_archive_dir}' does not exist, exiting")
|
|
833
|
+
sys.exit(1)
|
|
834
|
+
return
|
|
835
|
+
config_settings.backup_dir = args.alternate_archive_dir
|
|
836
|
+
|
|
837
|
+
# --- Functional logic ---
|
|
838
|
+
if args.create_db:
|
|
839
|
+
if args.backup_def:
|
|
840
|
+
sys.exit(create_db(args.backup_def, config_settings, logger, runner))
|
|
841
|
+
return
|
|
842
|
+
else:
|
|
843
|
+
for root, dirs, files in os.walk(config_settings.backup_d_dir):
|
|
844
|
+
for file in files:
|
|
845
|
+
current_backupdef = os.path.basename(file)
|
|
846
|
+
logger.debug(f"Create catalog db for backup definition: '{current_backupdef}'")
|
|
847
|
+
result = create_db(current_backupdef, config_settings, logger, runner)
|
|
848
|
+
if result != 0:
|
|
849
|
+
sys.exit(result)
|
|
850
|
+
return
|
|
851
|
+
|
|
852
|
+
if args.add_specific_archive:
|
|
853
|
+
sys.exit(add_specific_archive(args.add_specific_archive, config_settings))
|
|
627
854
|
return
|
|
628
|
-
config_settings.backup_dir = args.alternate_archive_dir
|
|
629
855
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if args.backup_def:
|
|
633
|
-
sys.exit(create_db(args.backup_def, config_settings, logger, runner))
|
|
856
|
+
if args.add_dir:
|
|
857
|
+
sys.exit(add_directory(args, config_settings))
|
|
634
858
|
return
|
|
635
|
-
else:
|
|
636
|
-
for root, dirs, files in os.walk(config_settings.backup_d_dir):
|
|
637
|
-
for file in files:
|
|
638
|
-
current_backupdef = os.path.basename(file)
|
|
639
|
-
logger.debug(f"Create catalog db for backup definition: '{current_backupdef}'")
|
|
640
|
-
result = create_db(current_backupdef, config_settings, logger, runner)
|
|
641
|
-
if result != 0:
|
|
642
|
-
sys.exit(result)
|
|
643
|
-
return
|
|
644
|
-
|
|
645
|
-
if args.add_specific_archive:
|
|
646
|
-
sys.exit(add_specific_archive(args.add_specific_archive, config_settings))
|
|
647
|
-
return
|
|
648
859
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if list_catalogs(current_backupdef, config_settings).returncode != 0:
|
|
666
|
-
result = 1
|
|
667
|
-
sys.exit(result)
|
|
668
|
-
return
|
|
860
|
+
if args.remove_specific_archive:
|
|
861
|
+
return remove_specific_archive(args.remove_specific_archive, config_settings)
|
|
862
|
+
|
|
863
|
+
if args.list_catalogs:
|
|
864
|
+
if args.backup_def:
|
|
865
|
+
process = list_catalogs(args.backup_def, config_settings)
|
|
866
|
+
result = process.returncode
|
|
867
|
+
else:
|
|
868
|
+
result = 0
|
|
869
|
+
for root, dirs, files in os.walk(config_settings.backup_d_dir):
|
|
870
|
+
for file in files:
|
|
871
|
+
current_backupdef = os.path.basename(file)
|
|
872
|
+
if list_catalogs(current_backupdef, config_settings).returncode != 0:
|
|
873
|
+
result = 1
|
|
874
|
+
sys.exit(result)
|
|
875
|
+
return
|
|
669
876
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
877
|
+
if args.list_archive_contents:
|
|
878
|
+
result = list_archive_contents(args.list_archive_contents, config_settings)
|
|
879
|
+
sys.exit(result)
|
|
880
|
+
return
|
|
674
881
|
|
|
675
882
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
883
|
+
if args.find_file:
|
|
884
|
+
result = find_file(args.find_file, args.backup_def, config_settings)
|
|
885
|
+
sys.exit(result)
|
|
886
|
+
return
|
|
887
|
+
except Exception as e:
|
|
888
|
+
msg = f"Unexpected error during manager operation: {e}"
|
|
889
|
+
logger.error(msg, exc_info=True)
|
|
890
|
+
ts = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
|
891
|
+
send_discord_message(f"{ts} - manager: FAILURE - {msg}", config_settings=config_settings)
|
|
892
|
+
sys.exit(1)
|
|
680
893
|
|
|
681
894
|
|
|
682
895
|
if __name__ == "__main__":
|