dar-backup 0.6.21__py3-none-any.whl → 0.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dar_backup/Changelog.md +35 -2
- dar_backup/README.md +433 -97
- dar_backup/__about__.py +1 -1
- dar_backup/clean_log.py +3 -0
- dar_backup/cleanup.py +13 -7
- dar_backup/command_runner.py +2 -0
- dar_backup/config_settings.py +18 -0
- dar_backup/dar-backup.conf +4 -2
- dar_backup/dar-backup.conf.j2 +64 -0
- dar_backup/dar_backup.py +19 -13
- dar_backup/dar_backup_systemd.py +3 -0
- dar_backup/demo.py +153 -77
- dar_backup/demo_backup_def.j2 +62 -0
- dar_backup/exceptions.py +2 -0
- dar_backup/installer.py +119 -4
- dar_backup/manager.py +9 -4
- dar_backup/rich_progress.py +4 -0
- dar_backup/util.py +107 -13
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/METADATA +454 -98
- dar_backup-0.7.2.dist-info/RECORD +25 -0
- dar_backup-0.6.21.dist-info/RECORD +0 -23
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/entry_points.txt +0 -0
- {dar_backup-0.6.21.dist-info → dar_backup-0.7.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
# ------------------------------------------------------------------------
|
|
5
|
+
# Demo of a `dar-backup` definition file
|
|
6
|
+
# This file was generated by dar-backup's `demo` program.
|
|
7
|
+
#
|
|
8
|
+
{%- if opts_dict | length > 0 %}
|
|
9
|
+
# Options given to the `demo` program:
|
|
10
|
+
{% endif %}
|
|
11
|
+
{%- if opts_dict.ROOT_DIR -%}
|
|
12
|
+
# --root-dir : {{ opts_dict.ROOT_DIR }}
|
|
13
|
+
{% endif %}
|
|
14
|
+
{%- if opts_dict.DIR_TO_BACKUP -%}
|
|
15
|
+
# --dir-to-backup : {{ opts_dict.DIR_TO_BACKUP }}
|
|
16
|
+
{% endif -%}
|
|
17
|
+
{%- if opts_dict.BACKUP_DIR -%}
|
|
18
|
+
# --backup-dir : {{ opts_dict.BACKUP_DIR }}
|
|
19
|
+
{% endif %}
|
|
20
|
+
#
|
|
21
|
+
# Variables used to generate this file:
|
|
22
|
+
# =====================================
|
|
23
|
+
{% for k,v in vars_map|dictsort %}# {{ k }} : {{ v }}
|
|
24
|
+
{% endfor -%}
|
|
25
|
+
# ------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
# Switch to ordered selection mode, which means that the following options
|
|
28
|
+
# will be considered top to bottom
|
|
29
|
+
-am
|
|
30
|
+
|
|
31
|
+
# Backup Root dir
|
|
32
|
+
{%- if vars_map.ROOT_DIR %}
|
|
33
|
+
-R {{ vars_map.ROOT_DIR }}
|
|
34
|
+
{% endif -%}
|
|
35
|
+
|
|
36
|
+
{% if vars_map.DIR_TO_BACKUP %}
|
|
37
|
+
# Directories to backup below the Root dir
|
|
38
|
+
-g {{ vars_map.DIR_TO_BACKUP }}
|
|
39
|
+
|
|
40
|
+
# This is an example of exclusion of a `.private` directory inside the
|
|
41
|
+
# directory that is backed up
|
|
42
|
+
-P {{ vars_map.DIR_TO_BACKUP }}/.private
|
|
43
|
+
{%- else %}
|
|
44
|
+
# Examples of directories to exclude below the Root dir
|
|
45
|
+
-P mnt
|
|
46
|
+
-P .cache
|
|
47
|
+
{% endif %}
|
|
48
|
+
|
|
49
|
+
# compression level
|
|
50
|
+
-z5
|
|
51
|
+
|
|
52
|
+
# no overwrite, if you rerun a backup, 'dar' halts and asks what to do
|
|
53
|
+
# as `dar-backup` gives the `-Q` option to `dar`, the net effect of `-n` and `-Q` is
|
|
54
|
+
# that `dar` will quit and not overwrite the existing backup
|
|
55
|
+
-n
|
|
56
|
+
|
|
57
|
+
# size of each slice in the archive (10G is 10 Gigabytes)
|
|
58
|
+
--slice 10G
|
|
59
|
+
|
|
60
|
+
# bypass directores marked as cache directories
|
|
61
|
+
# http://dar.linux.free.fr/doc/Features.html
|
|
62
|
+
--cache-directory-tagging
|
dar_backup/exceptions.py
CHANGED
dar_backup/installer.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
1
4
|
import argparse
|
|
2
5
|
import os
|
|
3
6
|
from . import __about__ as about
|
|
@@ -9,6 +12,91 @@ from dar_backup.manager import create_db
|
|
|
9
12
|
# Always expand manager DB dir correctly, using helper function
|
|
10
13
|
from dar_backup.manager import get_db_dir
|
|
11
14
|
from dar_backup.util import expand_path
|
|
15
|
+
from dar_backup.util import is_safe_path
|
|
16
|
+
|
|
17
|
+
def install_autocompletion():
|
|
18
|
+
"""Detect user shell, choose RC file, and idempotently append autocompletion."""
|
|
19
|
+
shell = Path(os.environ.get("SHELL", "")).name
|
|
20
|
+
home = Path.home()
|
|
21
|
+
|
|
22
|
+
# pick RC file based on shell
|
|
23
|
+
if shell == "zsh":
|
|
24
|
+
rc_file = home / ".zshrc"
|
|
25
|
+
elif shell == "bash":
|
|
26
|
+
# prefer ~/.bash_profile on macOS if present
|
|
27
|
+
rc_file = home / ".bash_profile" if (home / ".bash_profile").exists() else home / ".bashrc"
|
|
28
|
+
else:
|
|
29
|
+
rc_file = home / ".bashrc"
|
|
30
|
+
|
|
31
|
+
marker = "# >>> dar-backup autocompletion >>>"
|
|
32
|
+
end_marker = "# <<< dar-backup autocompletion <<<"
|
|
33
|
+
|
|
34
|
+
block = "\n".join([
|
|
35
|
+
marker,
|
|
36
|
+
'eval "$(register-python-argcomplete dar-backup)"',
|
|
37
|
+
'eval "$(register-python-argcomplete cleanup)"',
|
|
38
|
+
'eval "$(register-python-argcomplete manager)"',
|
|
39
|
+
"#complete -o nosort -C 'python -m argcomplete cleanup' cleanup",
|
|
40
|
+
"#complete -o nosort -C 'python -m argcomplete manager' manager",
|
|
41
|
+
end_marker,
|
|
42
|
+
]) + "\n"
|
|
43
|
+
|
|
44
|
+
# ensure RC file and parent directory exist
|
|
45
|
+
rc_file.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
if not rc_file.exists():
|
|
47
|
+
rc_file.touch()
|
|
48
|
+
|
|
49
|
+
content = rc_file.read_text()
|
|
50
|
+
if marker in content:
|
|
51
|
+
print(f"Autocompletion already installed in {rc_file}")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# append the autocompletion block
|
|
55
|
+
rc_file.open("a").write("\n" + block)
|
|
56
|
+
print(f"✔️ Appended autocompletion block to {rc_file}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def uninstall_autocompletion() -> str:
|
|
61
|
+
"""Remove previously installed autocompletion block from shell RC file."""
|
|
62
|
+
shell = Path(os.environ.get("SHELL", "")).name
|
|
63
|
+
home = Path.home()
|
|
64
|
+
|
|
65
|
+
# pick RC file based on shell
|
|
66
|
+
if shell == "zsh":
|
|
67
|
+
rc_file = home / ".zshrc"
|
|
68
|
+
elif shell == "bash":
|
|
69
|
+
rc_file = home / ".bash_profile" if (home / ".bash_profile").exists() else home / ".bashrc"
|
|
70
|
+
else:
|
|
71
|
+
rc_file = home / ".bashrc"
|
|
72
|
+
|
|
73
|
+
marker = "# >>> dar-backup autocompletion >>>"
|
|
74
|
+
end_marker = "# <<< dar-backup autocompletion <<<"
|
|
75
|
+
|
|
76
|
+
if not rc_file.exists():
|
|
77
|
+
print(f"❌ RC file not found: {rc_file}")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
content = rc_file.read_text()
|
|
81
|
+
if marker not in content:
|
|
82
|
+
print(f"No autocompletion block found in {rc_file}")
|
|
83
|
+
return f"No autocompletion block found in {rc_file}" # for unit test
|
|
84
|
+
|
|
85
|
+
lines = content.splitlines(keepends=True)
|
|
86
|
+
new_lines = []
|
|
87
|
+
skipping = False
|
|
88
|
+
for line in lines:
|
|
89
|
+
if marker in line:
|
|
90
|
+
skipping = True
|
|
91
|
+
continue
|
|
92
|
+
if end_marker in line and skipping:
|
|
93
|
+
skipping = False
|
|
94
|
+
continue
|
|
95
|
+
if not skipping:
|
|
96
|
+
new_lines.append(line)
|
|
97
|
+
|
|
98
|
+
rc_file.write_text(''.join(new_lines))
|
|
99
|
+
print(f"✔️ Removed autocompletion block from {rc_file}")
|
|
12
100
|
|
|
13
101
|
|
|
14
102
|
|
|
@@ -29,6 +117,8 @@ def run_installer(config_file: str, create_db_flag: bool):
|
|
|
29
117
|
config_file = os.path.expanduser(os.path.expandvars(config_file))
|
|
30
118
|
config_settings = ConfigSettings(config_file)
|
|
31
119
|
|
|
120
|
+
print(f"Using config settings: {config_settings}")
|
|
121
|
+
|
|
32
122
|
# Set up logging
|
|
33
123
|
command_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
34
124
|
logger = setup_logging(
|
|
@@ -50,6 +140,9 @@ def run_installer(config_file: str, create_db_flag: bool):
|
|
|
50
140
|
}
|
|
51
141
|
|
|
52
142
|
for name, dir_path in required_dirs.items():
|
|
143
|
+
if not is_safe_path(dir_path):
|
|
144
|
+
logger.error(f"Unsafe path detected: {dir_path} ({name})")
|
|
145
|
+
raise ValueError(f"Unsafe path detected: {dir_path} ({name})")
|
|
53
146
|
expanded = Path(expand_path(dir_path))
|
|
54
147
|
if not expanded.exists():
|
|
55
148
|
logger.info(f"Creating directory: {expanded} ({name})")
|
|
@@ -62,22 +155,44 @@ def run_installer(config_file: str, create_db_flag: bool):
|
|
|
62
155
|
print(f"Creating catalog for: {backup_def}")
|
|
63
156
|
result = create_db(backup_def, config_settings, logger, runner)
|
|
64
157
|
if result == 0:
|
|
65
|
-
print(f"✔️ Catalog created (or already
|
|
158
|
+
print(f"✔️ Catalog created (or already exist): {backup_def}")
|
|
66
159
|
else:
|
|
67
160
|
print(f"❌ Failed to create catalog: {backup_def}")
|
|
68
161
|
|
|
69
162
|
|
|
70
163
|
def main():
|
|
71
164
|
parser = argparse.ArgumentParser(description="dar-backup installer")
|
|
72
|
-
parser.add_argument("--config", required=
|
|
165
|
+
parser.add_argument("--config", required=False, help="Path to config file")
|
|
73
166
|
parser.add_argument("--create-db", action="store_true", help="Create catalog databases")
|
|
74
|
-
|
|
167
|
+
group = parser.add_mutually_exclusive_group()
|
|
168
|
+
group.add_argument(
|
|
169
|
+
"--install-autocompletion", action="store_true",
|
|
170
|
+
help="Append shell-completion setup to your shell RC"
|
|
171
|
+
)
|
|
172
|
+
group.add_argument(
|
|
173
|
+
"--remove-autocompletion", action="store_true",
|
|
174
|
+
help="Remove shell-completion setup from your shell RC"
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"-v", "--version", action="version",
|
|
178
|
+
version=f"%(prog)s version {about.__version__}, {about.__license__}"
|
|
75
179
|
)
|
|
76
180
|
|
|
77
181
|
args = parser.parse_args()
|
|
78
182
|
|
|
79
|
-
run_installer(args.config, args.create_db)
|
|
80
183
|
|
|
184
|
+
if args.config:
|
|
185
|
+
if not os.path.exists(args.config):
|
|
186
|
+
print(f"❌ Config file does not exist: {args.config}")
|
|
187
|
+
return
|
|
188
|
+
run_installer(args.config, args.create_db)
|
|
189
|
+
|
|
190
|
+
if args.install_autocompletion:
|
|
191
|
+
install_autocompletion()
|
|
192
|
+
elif args.remove_autocompletion:
|
|
193
|
+
uninstall_autocompletion()
|
|
194
|
+
|
|
195
|
+
|
|
81
196
|
|
|
82
197
|
if __name__ == "__main__":
|
|
83
198
|
main()
|
dar_backup/manager.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
3
|
|
|
3
4
|
"""
|
|
4
5
|
Copyright (C) 2024 Per Jensen
|
|
@@ -39,6 +40,7 @@ from dar_backup.util import get_binary_info
|
|
|
39
40
|
from dar_backup.util import show_version
|
|
40
41
|
from dar_backup.util import get_invocation_command_line
|
|
41
42
|
from dar_backup.util import print_aligned_settings
|
|
43
|
+
from dar_backup.util import show_scriptname
|
|
42
44
|
|
|
43
45
|
from dar_backup.command_runner import CommandRunner
|
|
44
46
|
from dar_backup.command_runner import CommandResult
|
|
@@ -543,14 +545,15 @@ def main():
|
|
|
543
545
|
return
|
|
544
546
|
|
|
545
547
|
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
546
|
-
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
548
|
+
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout, logfile_max_bytes=config_settings.logfile_max_bytes, logfile_backup_count=config_settings.logfile_backup_count)
|
|
547
549
|
command_logger = get_logger(command_output_logger=True)
|
|
548
550
|
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
549
551
|
|
|
550
552
|
start_msgs: List[Tuple[str, str]] = []
|
|
551
553
|
|
|
552
554
|
start_time = int(time())
|
|
553
|
-
|
|
555
|
+
|
|
556
|
+
start_msgs.append((f"{show_scriptname()}:", about.__version__))
|
|
554
557
|
logger.info(f"START TIME: {start_time}")
|
|
555
558
|
logger.debug(f"Command line: {get_invocation_command_line()}")
|
|
556
559
|
logger.debug(f"`args`:\n{args}")
|
|
@@ -558,13 +561,15 @@ def main():
|
|
|
558
561
|
start_msgs.append(("Config file:", args.config_file))
|
|
559
562
|
args.verbose and start_msgs.append(("Backup dir:", config_settings.backup_dir))
|
|
560
563
|
start_msgs.append(("Logfile:", config_settings.logfile_location))
|
|
564
|
+
args.verbose and start_msgs.append(("Logfile max size (bytes):", config_settings.logfile_max_bytes))
|
|
565
|
+
args.verbose and start_msgs.append(("Logfile backup count:", config_settings.logfile_backup_count))
|
|
561
566
|
args.verbose and start_msgs.append(("--alternate-archive-dir:", args.alternate_archive_dir))
|
|
562
|
-
args.verbose and start_msgs.append(("--
|
|
567
|
+
args.verbose and start_msgs.append(("--remove-specific-archive:", args.remove_specific_archive))
|
|
563
568
|
dar_manager_properties = get_binary_info(command='dar_manager')
|
|
564
569
|
start_msgs.append(("dar_manager:", dar_manager_properties['path']))
|
|
565
570
|
start_msgs.append(("dar_manager v.:", dar_manager_properties['version']))
|
|
566
571
|
|
|
567
|
-
print_aligned_settings(start_msgs)
|
|
572
|
+
print_aligned_settings(start_msgs, quiet=not args.verbose)
|
|
568
573
|
|
|
569
574
|
# --- Sanity checks ---
|
|
570
575
|
if args.add_dir and not args.add_dir.strip():
|
dar_backup/rich_progress.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
1
4
|
import os
|
|
2
5
|
import time
|
|
3
6
|
from threading import Event
|
|
@@ -99,3 +102,4 @@ def show_log_driven_bar(log_path: str, stop_event: Event, session_marker: str, m
|
|
|
99
102
|
if stop_event.is_set():
|
|
100
103
|
break
|
|
101
104
|
|
|
105
|
+
# Rich prints a \n here, I will live with it
|
dar_backup/util.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
util.py source code is here: https://github.com/per2jensen/dar-backup
|
|
3
5
|
|
|
@@ -10,7 +12,9 @@ See section 15 and section 16 in the supplied "LICENSE" file
|
|
|
10
12
|
import typing
|
|
11
13
|
import locale
|
|
12
14
|
import configparser
|
|
15
|
+
import inspect
|
|
13
16
|
import logging
|
|
17
|
+
|
|
14
18
|
import os
|
|
15
19
|
import re
|
|
16
20
|
import subprocess
|
|
@@ -19,11 +23,15 @@ import shutil
|
|
|
19
23
|
import sys
|
|
20
24
|
import threading
|
|
21
25
|
import traceback
|
|
26
|
+
|
|
27
|
+
import dar_backup.__about__ as about
|
|
28
|
+
|
|
29
|
+
|
|
22
30
|
from argcomplete.completers import ChoicesCompleter
|
|
23
31
|
from datetime import datetime
|
|
24
32
|
from dar_backup.config_settings import ConfigSettings
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
from logging.handlers import RotatingFileHandler
|
|
34
|
+
from pathlib import Path
|
|
27
35
|
from rich.console import Console
|
|
28
36
|
from rich.text import Text
|
|
29
37
|
|
|
@@ -34,7 +42,16 @@ from typing import Tuple
|
|
|
34
42
|
logger=None
|
|
35
43
|
secondary_logger=None
|
|
36
44
|
|
|
37
|
-
def setup_logging(log_file: str, command_output_log_file: str, log_level: str = "info", log_to_stdout: bool = False) -> logging.Logger:
|
|
45
|
+
#def setup_logging(log_file: str, command_output_log_file: str, log_level: str = "info", log_to_stdout: bool = False) -> logging.Logger:
|
|
46
|
+
def setup_logging(
|
|
47
|
+
log_file: str,
|
|
48
|
+
command_output_log_file: str,
|
|
49
|
+
log_level: str = "info",
|
|
50
|
+
log_to_stdout: bool = False,
|
|
51
|
+
logfile_max_bytes: int = 26214400,
|
|
52
|
+
logfile_backup_count: int = 5,
|
|
53
|
+
) -> logging.Logger:
|
|
54
|
+
|
|
38
55
|
"""
|
|
39
56
|
Sets up logging for the main program and a separate secondary logfile for command outputs.
|
|
40
57
|
|
|
@@ -43,9 +60,11 @@ def setup_logging(log_file: str, command_output_log_file: str, log_level: str =
|
|
|
43
60
|
command_output_log_file (str): The path to the secondary log file for command outputs.
|
|
44
61
|
log_level (str): The log level to use. Can be "info", "debug", or "trace". Defaults to "info".
|
|
45
62
|
log_to_stdout (bool): If True, log messages will be printed to the console. Defaults to False.
|
|
63
|
+
logfile_max_bytes: max file size of a log file, defailt = 26214400.
|
|
64
|
+
logfile_backup_count: max numbers of logs files, default = 5.
|
|
46
65
|
|
|
47
66
|
Returns:
|
|
48
|
-
|
|
67
|
+
a RotatingFileHandler logger instance.
|
|
49
68
|
|
|
50
69
|
Raises:
|
|
51
70
|
Exception: If an error occurs during logging initialization
|
|
@@ -61,20 +80,34 @@ def setup_logging(log_file: str, command_output_log_file: str, log_level: str =
|
|
|
61
80
|
|
|
62
81
|
logging.Logger.trace = trace
|
|
63
82
|
|
|
83
|
+
file_handler = RotatingFileHandler(
|
|
84
|
+
log_file,
|
|
85
|
+
maxBytes=logfile_max_bytes,
|
|
86
|
+
backupCount=logfile_backup_count,
|
|
87
|
+
encoding="utf-8",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
command_handler = RotatingFileHandler(
|
|
91
|
+
command_output_log_file,
|
|
92
|
+
maxBytes=logfile_max_bytes,
|
|
93
|
+
backupCount=logfile_backup_count,
|
|
94
|
+
encoding="utf-8",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
98
|
+
file_handler.setFormatter(formatter)
|
|
99
|
+
command_handler.setFormatter(formatter)
|
|
100
|
+
|
|
101
|
+
|
|
64
102
|
# Setup main logger
|
|
65
103
|
logger = logging.getLogger("main_logger")
|
|
66
104
|
logger.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
|
|
67
|
-
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
68
|
-
file_handler = logging.FileHandler(log_file)
|
|
69
|
-
file_handler.setFormatter(formatter)
|
|
70
105
|
logger.addHandler(file_handler)
|
|
71
106
|
|
|
72
107
|
# Setup secondary logger for command outputs
|
|
73
108
|
secondary_logger = logging.getLogger("command_output_logger")
|
|
74
109
|
secondary_logger.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
|
|
75
|
-
|
|
76
|
-
sec_file_handler.setFormatter(formatter)
|
|
77
|
-
secondary_logger.addHandler(sec_file_handler)
|
|
110
|
+
secondary_logger.addHandler(command_handler)
|
|
78
111
|
|
|
79
112
|
if log_to_stdout:
|
|
80
113
|
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
@@ -119,6 +152,15 @@ completer_logger = _setup_completer_logger()
|
|
|
119
152
|
completer_logger.debug("Completer logger initialized.")
|
|
120
153
|
|
|
121
154
|
|
|
155
|
+
def print_debug(msg):
|
|
156
|
+
"""
|
|
157
|
+
Print a debug message with the filename and line number of the caller.
|
|
158
|
+
"""
|
|
159
|
+
frame = inspect.currentframe().f_back
|
|
160
|
+
print(f"[DEBUG] {frame.f_code.co_filename}:{frame.f_lineno} - {repr(msg)}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
122
164
|
def get_invocation_command_line() -> str:
|
|
123
165
|
"""
|
|
124
166
|
Safely retrieves the exact command line used to invoke the current Python process.
|
|
@@ -141,6 +183,17 @@ def get_invocation_command_line() -> str:
|
|
|
141
183
|
return f"[error: could not read /proc/[pid]/cmdline: {e}]"
|
|
142
184
|
|
|
143
185
|
|
|
186
|
+
def show_scriptname() -> str:
|
|
187
|
+
"""
|
|
188
|
+
Return script name, useful in start banner for example
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
scriptname = os.path.basename(sys.argv[0])
|
|
192
|
+
except:
|
|
193
|
+
scriptname = "unknown"
|
|
194
|
+
return scriptname
|
|
195
|
+
|
|
196
|
+
|
|
144
197
|
def show_version():
|
|
145
198
|
script_name = os.path.basename(sys.argv[0])
|
|
146
199
|
print(f"{script_name} {about.__version__}")
|
|
@@ -606,6 +659,7 @@ def print_aligned_settings(
|
|
|
606
659
|
settings: List[Tuple[str, str]],
|
|
607
660
|
log: bool = True,
|
|
608
661
|
header: str = "Startup Settings",
|
|
662
|
+
quiet: bool = True,
|
|
609
663
|
highlight_keywords: List[str] = None
|
|
610
664
|
) -> None:
|
|
611
665
|
"""
|
|
@@ -624,7 +678,7 @@ def print_aligned_settings(
|
|
|
624
678
|
header_line = f"========== {header} =========="
|
|
625
679
|
footer_line = "=" * len(header_line)
|
|
626
680
|
|
|
627
|
-
console.print(f"[bold cyan]{header_line}[/bold cyan]")
|
|
681
|
+
not quiet and console.print(f"[bold cyan]{header_line}[/bold cyan]")
|
|
628
682
|
if log and logger:
|
|
629
683
|
logger.info(header_line)
|
|
630
684
|
|
|
@@ -654,13 +708,53 @@ def print_aligned_settings(
|
|
|
654
708
|
|
|
655
709
|
line_text.append(text, style="white")
|
|
656
710
|
|
|
657
|
-
console.print(line_text)
|
|
711
|
+
not quiet and console.print(line_text)
|
|
658
712
|
|
|
659
713
|
# Always log clean text (no [!] in log)
|
|
660
714
|
final_line_for_log = f"{padded_label} {text}"
|
|
661
715
|
if log and logger:
|
|
662
716
|
logger.info(final_line_for_log)
|
|
663
717
|
|
|
664
|
-
console.print(f"[bold cyan]{footer_line}[/bold cyan]")
|
|
718
|
+
not quiet and console.print(f"[bold cyan]{footer_line}[/bold cyan]")
|
|
665
719
|
if log and logger:
|
|
666
720
|
logger.info(footer_line)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def normalize_dir(path: str) -> str:
|
|
726
|
+
"""
|
|
727
|
+
Strip any trailing slash/backslash but leave root (“/” or “C:\\”) intact.
|
|
728
|
+
"""
|
|
729
|
+
p = Path(path)
|
|
730
|
+
# Path(__str__) drops any trailing separators
|
|
731
|
+
normalized = str(p)
|
|
732
|
+
return normalized
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
# Reusable pattern for archive file naming
|
|
737
|
+
archive_pattern = re.compile(
|
|
738
|
+
r'^.+?_(FULL|DIFF|INCR)_(\d{4}-\d{2}-\d{2})\.\d+\.dar'
|
|
739
|
+
r'(?:\.vol\d+(?:\+\d+)?\.par2|\.par2)?$'
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
def is_safe_filename(filename: str) -> bool:
|
|
743
|
+
"""
|
|
744
|
+
Validates that the filename matches acceptable dar/par2 naming convention.
|
|
745
|
+
"""
|
|
746
|
+
return archive_pattern.match(filename) is not None
|
|
747
|
+
|
|
748
|
+
def is_safe_path(path: str) -> bool:
|
|
749
|
+
"""
|
|
750
|
+
Validates that the full path is absolute, has no '..'.
|
|
751
|
+
"""
|
|
752
|
+
normalized = os.path.normpath(path)
|
|
753
|
+
filename = os.path.basename(normalized)
|
|
754
|
+
|
|
755
|
+
return (
|
|
756
|
+
os.path.isabs(normalized)
|
|
757
|
+
and '..' not in normalized.split(os.sep)
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
|