dar-backup 0.6.16__py3-none-any.whl → 0.6.18__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 +217 -0
- dar_backup/README.md +1082 -0
- dar_backup/__about__.py +1 -1
- dar_backup/cleanup.py +42 -57
- dar_backup/command_runner.py +81 -0
- dar_backup/config_settings.py +7 -1
- dar_backup/dar-backup.conf +1 -1
- dar_backup/dar_backup.py +152 -49
- dar_backup/dar_backup_systemd.py +119 -0
- dar_backup/installer.py +39 -23
- dar_backup/manager.py +74 -44
- dar_backup/rich_progress.py +101 -0
- dar_backup/util.py +29 -134
- {dar_backup-0.6.16.dist-info → dar_backup-0.6.18.dist-info}/METADATA +283 -80
- dar_backup-0.6.18.dist-info/RECORD +21 -0
- {dar_backup-0.6.16.dist-info → dar_backup-0.6.18.dist-info}/entry_points.txt +1 -0
- dar_backup-0.6.16.dist-info/RECORD +0 -16
- {dar_backup-0.6.16.dist-info → dar_backup-0.6.18.dist-info}/WHEEL +0 -0
- {dar_backup-0.6.16.dist-info → dar_backup-0.6.18.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import subprocess
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
SERVICE_TEMPLATE = """[Unit]
|
|
6
|
+
Description=dar-backup {mode}
|
|
7
|
+
StartLimitIntervalSec=120
|
|
8
|
+
StartLimitBurst=1
|
|
9
|
+
|
|
10
|
+
[Service]
|
|
11
|
+
Type=oneshot
|
|
12
|
+
TimeoutSec=infinity
|
|
13
|
+
RemainAfterExit=no
|
|
14
|
+
ExecStart=/bin/bash -c '{exec_command}'
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
TIMER_TEMPLATE = """[Unit]
|
|
18
|
+
Description=dar-backup {mode} timer
|
|
19
|
+
|
|
20
|
+
[Timer]
|
|
21
|
+
OnCalendar={calendar}
|
|
22
|
+
Persistent=true
|
|
23
|
+
|
|
24
|
+
[Install]
|
|
25
|
+
WantedBy=timers.target
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
CLEANUP_SERVICE_TEMPLATE = """[Unit]
|
|
29
|
+
Description=cleanup up old DIFF & INCR backups
|
|
30
|
+
StartLimitIntervalSec=120
|
|
31
|
+
StartLimitBurst=1
|
|
32
|
+
|
|
33
|
+
[Service]
|
|
34
|
+
Type=oneshot
|
|
35
|
+
TimeoutSec=60
|
|
36
|
+
RemainAfterExit=no
|
|
37
|
+
ExecStart=/bin/bash -c '{exec_command}'
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
CLEANUP_TIMER = """[Unit]
|
|
41
|
+
Description=dar-cleanup DIFF & INCR timer
|
|
42
|
+
|
|
43
|
+
[Timer]
|
|
44
|
+
OnCalendar=*-*-* 21:07:00
|
|
45
|
+
|
|
46
|
+
[Install]
|
|
47
|
+
WantedBy=timers.target
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
TIMINGS = {
|
|
51
|
+
"FULL": "*-12-30 10:03:00",
|
|
52
|
+
"DIFF": "*-*-01 19:03:00",
|
|
53
|
+
"INCR": "*-*-04/3 19:03:00"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
FLAGS = {
|
|
57
|
+
"FULL": "-F",
|
|
58
|
+
"DIFF": "-D",
|
|
59
|
+
"INCR": "-I"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def build_exec_command(venv, flag, dar_path=None, tool='dar-backup'):
|
|
63
|
+
if dar_path:
|
|
64
|
+
return f"PATH={dar_path}:$PATH && . {venv}/bin/activate && {tool} {flag} --verbose --log-stdout"
|
|
65
|
+
return f". {venv}/bin/activate && {tool} {flag} --verbose --log-stdout"
|
|
66
|
+
|
|
67
|
+
def generate_service(mode, venv, dar_path):
|
|
68
|
+
exec_command = build_exec_command(venv, FLAGS[mode], dar_path)
|
|
69
|
+
return SERVICE_TEMPLATE.format(mode=mode, exec_command=exec_command)
|
|
70
|
+
|
|
71
|
+
def generate_timer(mode):
|
|
72
|
+
return TIMER_TEMPLATE.format(mode=mode, calendar=TIMINGS[mode])
|
|
73
|
+
|
|
74
|
+
def generate_cleanup_service(venv, dar_path):
|
|
75
|
+
exec_command = build_exec_command(venv, "", dar_path, tool='cleanup').strip()
|
|
76
|
+
return CLEANUP_SERVICE_TEMPLATE.format(exec_command=exec_command)
|
|
77
|
+
|
|
78
|
+
def write_unit_file(path, filename, content):
|
|
79
|
+
file_path = path / filename
|
|
80
|
+
file_path.write_text(content)
|
|
81
|
+
print(f"Generated {filename}")
|
|
82
|
+
|
|
83
|
+
def enable_and_start_unit(unit_name):
|
|
84
|
+
subprocess.run(["systemctl", "--user", "enable", unit_name], check=False)
|
|
85
|
+
subprocess.run(["systemctl", "--user", "start", unit_name], check=False)
|
|
86
|
+
|
|
87
|
+
def write_unit_files(venv, dar_path, install=False):
|
|
88
|
+
output_path = Path.home() / ".config/systemd/user" if install else Path.cwd()
|
|
89
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
|
|
91
|
+
for mode in FLAGS:
|
|
92
|
+
service_name = f"dar-{mode.lower()}-backup.service"
|
|
93
|
+
timer_name = f"dar-{mode.lower()}-backup.timer"
|
|
94
|
+
write_unit_file(output_path, service_name, generate_service(mode, venv, dar_path))
|
|
95
|
+
write_unit_file(output_path, timer_name, generate_timer(mode))
|
|
96
|
+
print(f" → Fires on: {TIMINGS[mode]}")
|
|
97
|
+
|
|
98
|
+
write_unit_file(output_path, "dar-cleanup.service", generate_cleanup_service(venv, dar_path))
|
|
99
|
+
write_unit_file(output_path, "dar-cleanup.timer", CLEANUP_TIMER)
|
|
100
|
+
print(f" → Fires on: *-*-* 21:07:00")
|
|
101
|
+
|
|
102
|
+
if install:
|
|
103
|
+
for mode in FLAGS:
|
|
104
|
+
enable_and_start_unit(f"dar-{mode.lower()}-backup.timer")
|
|
105
|
+
enable_and_start_unit("dar-cleanup.timer")
|
|
106
|
+
subprocess.run(["systemctl", "--user", "daemon-reexec"], check=False)
|
|
107
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
|
|
108
|
+
print("Systemd `dar-backup` units and timers installed and user daemon reloaded.")
|
|
109
|
+
|
|
110
|
+
def main():
|
|
111
|
+
parser = argparse.ArgumentParser(description="Generate systemd service and timer units for dar-backup.")
|
|
112
|
+
parser.add_argument("--venv", required=True, help="Path to the Python venv with dar-backup")
|
|
113
|
+
parser.add_argument("--dar-path", help="Optional path to dar binary's directory")
|
|
114
|
+
parser.add_argument("--install", action="store_true", help="Install the units to ~/.config/systemd/user")
|
|
115
|
+
args = parser.parse_args()
|
|
116
|
+
write_unit_files(args.venv, args.dar_path, install=args.install)
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|
dar_backup/installer.py
CHANGED
|
@@ -63,6 +63,7 @@ BACKUP_DEFINITION = '''
|
|
|
63
63
|
--cache-directory-tagging
|
|
64
64
|
'''
|
|
65
65
|
|
|
66
|
+
|
|
66
67
|
def main():
|
|
67
68
|
parser = argparse.ArgumentParser(
|
|
68
69
|
description="Set up `dar-backup` on your system.",
|
|
@@ -72,7 +73,6 @@ def main():
|
|
|
72
73
|
action="store_true",
|
|
73
74
|
help="Deploy a simple config file, use ~/dar-backup/ for log file, archives and restore tests."
|
|
74
75
|
)
|
|
75
|
-
|
|
76
76
|
parser.add_argument(
|
|
77
77
|
"-v", "--version",
|
|
78
78
|
action="version",
|
|
@@ -85,38 +85,54 @@ def main():
|
|
|
85
85
|
errors = []
|
|
86
86
|
if os.path.exists(CONFIG_DIR):
|
|
87
87
|
errors.append(f"Config directory '{CONFIG_DIR}' already exists.")
|
|
88
|
-
|
|
89
88
|
if os.path.exists(DAR_BACKUP_DIR):
|
|
90
89
|
errors.append(f"Directory '{DAR_BACKUP_DIR}' already exists.")
|
|
91
90
|
|
|
92
|
-
if
|
|
91
|
+
if errors:
|
|
93
92
|
for error in errors:
|
|
94
93
|
print(f"Error: {error}")
|
|
95
|
-
sys.exit(1)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
|
|
115
129
|
print("1. Now run `manager --create` to create the catalog database.")
|
|
116
130
|
print("2. Then you can run `dar-backup --full-backup` to create a backup.")
|
|
117
131
|
print("3. List backups with `dar-backup --list`")
|
|
118
132
|
print("4. List contents of a backup with `dar-backup --list-contents <backup-name>`")
|
|
119
133
|
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
120
136
|
|
|
121
137
|
if __name__ == "__main__":
|
|
122
138
|
main()
|
dar_backup/manager.py
CHANGED
|
@@ -29,9 +29,12 @@ import sys
|
|
|
29
29
|
|
|
30
30
|
from . import __about__ as about
|
|
31
31
|
from dar_backup.config_settings import ConfigSettings
|
|
32
|
-
from dar_backup.util import run_command
|
|
33
32
|
from dar_backup.util import setup_logging
|
|
34
33
|
from dar_backup.util import CommandResult
|
|
34
|
+
from dar_backup.util import get_logger
|
|
35
|
+
|
|
36
|
+
from dar_backup.command_runner import CommandRunner
|
|
37
|
+
from dar_backup.command_runner import CommandResult
|
|
35
38
|
|
|
36
39
|
from datetime import datetime
|
|
37
40
|
from time import time
|
|
@@ -44,6 +47,7 @@ SCRIPTDIRPATH = os.path.dirname(SCRIPTPATH)
|
|
|
44
47
|
DB_SUFFIX = ".db"
|
|
45
48
|
|
|
46
49
|
logger = None
|
|
50
|
+
runner = None
|
|
47
51
|
|
|
48
52
|
def show_more_help():
|
|
49
53
|
help_text = f"""
|
|
@@ -66,7 +70,7 @@ def create_db(backup_def: str, config_settings: ConfigSettings):
|
|
|
66
70
|
else:
|
|
67
71
|
logger.info(f'Create catalog database: "{database_path}"')
|
|
68
72
|
command = ['dar_manager', '--create' , database_path]
|
|
69
|
-
process =
|
|
73
|
+
process = runner.run(command)
|
|
70
74
|
logger.debug(f"return code from 'db created': {process.returncode}")
|
|
71
75
|
if process.returncode == 0:
|
|
72
76
|
logger.info(f'Database created: "{database_path}"')
|
|
@@ -95,16 +99,19 @@ def list_catalogs(backup_def: str, config_settings: ConfigSettings) -> NamedTupl
|
|
|
95
99
|
if not os.path.exists(database_path):
|
|
96
100
|
error_msg = f'Database not found: "{database_path}"'
|
|
97
101
|
logger.error(error_msg)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
return CommandResult(1, '', error_msg)
|
|
103
|
+
|
|
104
|
+
# commandResult = CommandResult(
|
|
105
|
+
# process=None,
|
|
106
|
+
# stdout='',
|
|
107
|
+
# stderr=error_msg,
|
|
108
|
+
# returncode=1,
|
|
109
|
+
# timeout=1,
|
|
110
|
+
# command=[])
|
|
111
|
+
|
|
112
|
+
# return commandResult
|
|
106
113
|
command = ['dar_manager', '--base', database_path, '--list']
|
|
107
|
-
process =
|
|
114
|
+
process = runner.run(command)
|
|
108
115
|
stdout, stderr = process.stdout, process.stderr
|
|
109
116
|
if process.returncode != 0:
|
|
110
117
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
@@ -132,7 +139,7 @@ def cat_no_for_name(archive: str, config_settings: ConfigSettings) -> int:
|
|
|
132
139
|
for line in process.stdout.splitlines():
|
|
133
140
|
#print(f"{line_no}: '{line}'")
|
|
134
141
|
line_no += 1
|
|
135
|
-
search = re.search(
|
|
142
|
+
search = re.search(rf".*?(\d+)\s+.*?({archive}).*", line)
|
|
136
143
|
if search:
|
|
137
144
|
#print(f"FOUND: archive: {search.group(2)}, catalog #: '{search.group(1)}'")
|
|
138
145
|
logger.info(f"Found archive: '{archive}', catalog #: '{search.group(1)}'")
|
|
@@ -156,7 +163,7 @@ def list_archive_contents(archive: str, config_settings: ConfigSettings) -> int
|
|
|
156
163
|
logger.error(f"archive: '{archive}' not found in database: '{database_path}'")
|
|
157
164
|
return 1
|
|
158
165
|
command = ['dar_manager', '--base', database_path, '-u', f"{cat_no}"]
|
|
159
|
-
process =
|
|
166
|
+
process = runner.run(command)
|
|
160
167
|
stdout, stderr = process.stdout, process.stderr
|
|
161
168
|
if process.returncode != 0:
|
|
162
169
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
@@ -178,7 +185,7 @@ def list_catalog_contents(catalog_number: int, backup_def: str, config_settings:
|
|
|
178
185
|
logger.error(f'Catalog database not found: "{database_path}"')
|
|
179
186
|
return 1
|
|
180
187
|
command = ['dar_manager', '--base', database_path, '-u', f"{catalog_number}"]
|
|
181
|
-
process =
|
|
188
|
+
process = runner.run(command)
|
|
182
189
|
stdout, stderr = process.stdout, process.stderr
|
|
183
190
|
if process.returncode != 0:
|
|
184
191
|
logger.error(f'Error listing catalogs for: "{database_path}"')
|
|
@@ -199,7 +206,7 @@ def find_file(file, backup_def, config_settings):
|
|
|
199
206
|
logger.error(f'Database not found: "{database_path}"')
|
|
200
207
|
return 1
|
|
201
208
|
command = ['dar_manager', '--base', database_path, '-f', f"{file}"]
|
|
202
|
-
process =
|
|
209
|
+
process = runner.run(command)
|
|
203
210
|
stdout, stderr = process.stdout, process.stderr
|
|
204
211
|
if process.returncode != 0:
|
|
205
212
|
logger.error(f'Error finding file: {file} in: "{database_path}"')
|
|
@@ -234,7 +241,7 @@ def add_specific_archive(archive: str, config_settings: ConfigSettings, director
|
|
|
234
241
|
logger.info(f'Add "{archive_path}" to catalog: "{database}"')
|
|
235
242
|
|
|
236
243
|
command = ['dar_manager', '--base', database_path, "--add", archive_path, "-Q"]
|
|
237
|
-
process =
|
|
244
|
+
process = runner.run(command)
|
|
238
245
|
stdout, stderr = process.stdout, process.stderr
|
|
239
246
|
|
|
240
247
|
if process.returncode == 0:
|
|
@@ -338,7 +345,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
|
|
|
338
345
|
cat_no:int = cat_no_for_name(archive, config_settings)
|
|
339
346
|
if cat_no >= 0:
|
|
340
347
|
command = ['dar_manager', '--base', database_path, "--delete", str(cat_no)]
|
|
341
|
-
process: CommandResult =
|
|
348
|
+
process: CommandResult = runner.run(command)
|
|
342
349
|
logger.info(f"CommandResult: {process}")
|
|
343
350
|
else:
|
|
344
351
|
logger.warning(f"archive: '{archive}' not found in it's catalog database: {database_path}")
|
|
@@ -353,15 +360,7 @@ def remove_specific_archive(archive: str, config_settings: ConfigSettings) -> in
|
|
|
353
360
|
return 1
|
|
354
361
|
|
|
355
362
|
|
|
356
|
-
|
|
357
|
-
def main():
|
|
358
|
-
global logger
|
|
359
|
-
|
|
360
|
-
MIN_PYTHON_VERSION = (3, 9)
|
|
361
|
-
if sys.version_info < MIN_PYTHON_VERSION:
|
|
362
|
-
sys.stderr.write(f"Error: This script requires Python {'.'.join(map(str, MIN_PYTHON_VERSION))} or higher.\n")
|
|
363
|
-
sys.exit(1)
|
|
364
|
-
|
|
363
|
+
def build_arg_parser():
|
|
365
364
|
parser = argparse.ArgumentParser(description="Creates/maintains `dar` database catalogs")
|
|
366
365
|
parser.add_argument('-c', '--config-file', type=str, help="Path to 'dar-backup.conf'", default='~/.config/dar-backup/dar-backup.conf')
|
|
367
366
|
parser.add_argument('--create-db', action='store_true', help='Create missing databases for all backup definitions')
|
|
@@ -380,11 +379,30 @@ def main():
|
|
|
380
379
|
parser.add_argument('--more-help', action='store_true', help='Show extended help message')
|
|
381
380
|
parser.add_argument('--version', action='store_true', help='Show version & license')
|
|
382
381
|
|
|
382
|
+
return parser
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def main():
|
|
388
|
+
global logger, runner
|
|
389
|
+
|
|
390
|
+
MIN_PYTHON_VERSION = (3, 9)
|
|
391
|
+
if sys.version_info < MIN_PYTHON_VERSION:
|
|
392
|
+
sys.stderr.write(f"Error: This script requires Python {'.'.join(map(str, MIN_PYTHON_VERSION))} or higher.\n")
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
parser = argparse.ArgumentParser(description="Creates/maintains `dar` database catalogs")
|
|
397
|
+
# [parser.add_argument(...) as before...]
|
|
398
|
+
|
|
399
|
+
parser = build_arg_parser()
|
|
383
400
|
args = parser.parse_args()
|
|
384
401
|
|
|
385
402
|
if args.more_help:
|
|
386
403
|
show_more_help()
|
|
387
404
|
sys.exit(0)
|
|
405
|
+
return
|
|
388
406
|
|
|
389
407
|
if args.version:
|
|
390
408
|
print(f"{SCRIPTNAME} {about.__version__}")
|
|
@@ -393,84 +411,93 @@ def main():
|
|
|
393
411
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW, not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
394
412
|
See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
395
413
|
sys.exit(0)
|
|
414
|
+
return
|
|
396
415
|
|
|
397
|
-
# setup logging
|
|
398
416
|
args.config_file = os.path.expanduser(os.path.expandvars(args.config_file))
|
|
399
417
|
config_settings = ConfigSettings(args.config_file)
|
|
400
418
|
if not os.path.dirname(config_settings.logfile_location):
|
|
401
419
|
print(f"Directory for log file '{config_settings.logfile_location}' does not exist, exiting")
|
|
402
|
-
sys.exit(1)
|
|
420
|
+
sys.exit(1)
|
|
421
|
+
return
|
|
403
422
|
|
|
404
|
-
# command_output_log = os.path.join(config_settings.logfile_location.removesuffix("dar-backup.log"), "dar-backup-commands.log")
|
|
405
423
|
command_output_log = config_settings.logfile_location.replace("dar-backup.log", "dar-backup-commands.log")
|
|
406
424
|
logger = setup_logging(config_settings.logfile_location, command_output_log, args.log_level, args.log_stdout)
|
|
425
|
+
command_logger = get_logger(command_output_logger=True)
|
|
426
|
+
runner = CommandRunner(logger=logger, command_logger=command_logger)
|
|
407
427
|
|
|
408
|
-
|
|
409
|
-
start_time=int(time())
|
|
428
|
+
start_time = int(time())
|
|
410
429
|
logger.info(f"=====================================")
|
|
411
430
|
logger.info(f"{SCRIPTNAME} started, version: {about.__version__}")
|
|
412
431
|
logger.info(f"START TIME: {start_time}")
|
|
413
432
|
logger.debug(f"`args`:\n{args}")
|
|
414
433
|
logger.debug(f"`config_settings`:\n{config_settings}")
|
|
415
434
|
|
|
416
|
-
|
|
417
|
-
# Sanity checks before starting
|
|
435
|
+
# --- Sanity checks ---
|
|
418
436
|
if args.add_dir and not args.add_dir.strip():
|
|
419
437
|
logger.error("archive dir not given, exiting")
|
|
420
438
|
sys.exit(1)
|
|
439
|
+
return
|
|
421
440
|
|
|
422
|
-
if args.add_specific_archive and not args.add_specific_archive.strip():
|
|
441
|
+
if args.add_specific_archive is not None and not args.add_specific_archive.strip():
|
|
423
442
|
logger.error("specific archive to add not given, exiting")
|
|
424
443
|
sys.exit(1)
|
|
444
|
+
return
|
|
425
445
|
|
|
426
446
|
if args.remove_specific_archive and not args.remove_specific_archive.strip():
|
|
427
447
|
logger.error("specific archive to remove not given, exiting")
|
|
428
448
|
sys.exit(1)
|
|
449
|
+
return
|
|
429
450
|
|
|
430
451
|
if args.add_specific_archive and args.remove_specific_archive:
|
|
431
452
|
logger.error("you can't add and remove archives in the same operation, exiting")
|
|
432
453
|
sys.exit(1)
|
|
454
|
+
return
|
|
433
455
|
|
|
434
456
|
if args.add_dir and args.add_specific_archive:
|
|
435
457
|
logger.error("you cannot add both a directory and an archive")
|
|
436
458
|
sys.exit(1)
|
|
459
|
+
return
|
|
437
460
|
|
|
438
461
|
if args.backup_def and not args.backup_def.strip():
|
|
439
462
|
logger.error(f"No backup definition given to --backup-def")
|
|
463
|
+
sys.exit(1)
|
|
464
|
+
return
|
|
440
465
|
|
|
441
466
|
if args.backup_def:
|
|
442
467
|
backup_def_path = os.path.join(config_settings.backup_d_dir, args.backup_def)
|
|
443
468
|
if not os.path.exists(backup_def_path):
|
|
444
469
|
logger.error(f"Backup definition {args.backup_def} does not exist, exiting")
|
|
445
470
|
sys.exit(1)
|
|
446
|
-
|
|
471
|
+
return
|
|
447
472
|
|
|
448
473
|
if args.list_archive_contents and not args.list_archive_contents.strip():
|
|
449
474
|
logger.error(f"--list-archive-contents <param> not given, exiting")
|
|
450
475
|
sys.exit(1)
|
|
451
|
-
|
|
476
|
+
return
|
|
452
477
|
|
|
453
478
|
if args.list_catalog_contents and not args.backup_def:
|
|
454
479
|
logger.error(f"--list-catalog-contents requires the --backup-def, exiting")
|
|
455
480
|
sys.exit(1)
|
|
481
|
+
return
|
|
456
482
|
|
|
457
483
|
if args.find_file and not args.backup_def:
|
|
458
484
|
logger.error(f"--find-file requires the --backup-def, exiting")
|
|
459
485
|
sys.exit(1)
|
|
460
|
-
|
|
461
|
-
|
|
486
|
+
return
|
|
462
487
|
|
|
463
|
-
# Modify
|
|
488
|
+
# --- Modify settings ---
|
|
464
489
|
if args.alternate_archive_dir:
|
|
465
490
|
if not os.path.exists(args.alternate_archive_dir):
|
|
466
491
|
logger.error(f"Alternate archive dir '{args.alternate_archive_dir}' does not exist, exiting")
|
|
467
492
|
sys.exit(1)
|
|
493
|
+
return
|
|
468
494
|
config_settings.backup_dir = args.alternate_archive_dir
|
|
469
495
|
|
|
470
|
-
|
|
496
|
+
# --- Functional logic ---
|
|
471
497
|
if args.create_db:
|
|
472
498
|
if args.backup_def:
|
|
473
499
|
sys.exit(create_db(args.backup_def, config_settings))
|
|
500
|
+
return
|
|
474
501
|
else:
|
|
475
502
|
for root, dirs, files in os.walk(config_settings.backup_d_dir):
|
|
476
503
|
for file in files:
|
|
@@ -479,19 +506,19 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
479
506
|
result = create_db(current_backupdef, config_settings)
|
|
480
507
|
if result != 0:
|
|
481
508
|
sys.exit(result)
|
|
509
|
+
return
|
|
482
510
|
|
|
483
511
|
if args.add_specific_archive:
|
|
484
512
|
sys.exit(add_specific_archive(args.add_specific_archive, config_settings))
|
|
513
|
+
return
|
|
485
514
|
|
|
486
515
|
if args.add_dir:
|
|
487
516
|
sys.exit(add_directory(args, config_settings))
|
|
488
|
-
|
|
517
|
+
return
|
|
489
518
|
|
|
490
519
|
if args.remove_specific_archive:
|
|
491
520
|
return remove_specific_archive(args.remove_specific_archive, config_settings)
|
|
492
521
|
|
|
493
|
-
|
|
494
|
-
|
|
495
522
|
if args.list_catalogs:
|
|
496
523
|
if args.backup_def:
|
|
497
524
|
process = list_catalogs(args.backup_def, config_settings)
|
|
@@ -504,19 +531,22 @@ See section 15 and section 16 in the supplied "LICENSE" file.''')
|
|
|
504
531
|
if list_catalogs(current_backupdef, config_settings).returncode != 0:
|
|
505
532
|
result = 1
|
|
506
533
|
sys.exit(result)
|
|
507
|
-
|
|
534
|
+
return
|
|
508
535
|
|
|
509
536
|
if args.list_archive_contents:
|
|
510
537
|
result = list_archive_contents(args.list_archive_contents, config_settings)
|
|
511
538
|
sys.exit(result)
|
|
539
|
+
return
|
|
512
540
|
|
|
513
541
|
if args.list_catalog_contents:
|
|
514
542
|
result = list_catalog_contents(args.list_catalog_contents, args.backup_def, config_settings)
|
|
515
543
|
sys.exit(result)
|
|
544
|
+
return
|
|
516
545
|
|
|
517
546
|
if args.find_file:
|
|
518
547
|
result = find_file(args.find_file, args.backup_def, config_settings)
|
|
519
548
|
sys.exit(result)
|
|
549
|
+
return
|
|
520
550
|
|
|
521
551
|
|
|
522
552
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from threading import Event
|
|
4
|
+
from rich.console import Console, Group
|
|
5
|
+
from rich.live import Live
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
def is_terminal():
|
|
9
|
+
return Console().is_terminal
|
|
10
|
+
|
|
11
|
+
def tail_log_file(log_path, stop_event, session_marker=None):
|
|
12
|
+
"""Yields new lines from the log file, starting only after the session_marker is found."""
|
|
13
|
+
last_size = 0
|
|
14
|
+
marker_found = session_marker is None
|
|
15
|
+
|
|
16
|
+
while not stop_event.is_set():
|
|
17
|
+
if not os.path.exists(log_path):
|
|
18
|
+
time.sleep(0.5)
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
with open(log_path, "r") as f:
|
|
23
|
+
if last_size > os.path.getsize(log_path):
|
|
24
|
+
f.seek(0)
|
|
25
|
+
else:
|
|
26
|
+
f.seek(last_size)
|
|
27
|
+
|
|
28
|
+
while not stop_event.is_set():
|
|
29
|
+
line = f.readline()
|
|
30
|
+
if not line:
|
|
31
|
+
break
|
|
32
|
+
|
|
33
|
+
line = line.strip()
|
|
34
|
+
last_size = f.tell()
|
|
35
|
+
|
|
36
|
+
if not marker_found:
|
|
37
|
+
if session_marker in line:
|
|
38
|
+
marker_found = True
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
yield line
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"[!] Error reading log: {e}")
|
|
45
|
+
|
|
46
|
+
time.sleep(0.5)
|
|
47
|
+
|
|
48
|
+
def get_green_shade(step, max_width):
|
|
49
|
+
"""Returns a green color from light to dark across the bar."""
|
|
50
|
+
start = 180
|
|
51
|
+
end = 20
|
|
52
|
+
value = int(start - ((start - end) * (step / max_width)))
|
|
53
|
+
return f"rgb(0,{value},0)"
|
|
54
|
+
|
|
55
|
+
def show_log_driven_bar(log_path: str, stop_event: Event, session_marker: str, max_width=50):
|
|
56
|
+
console = Console()
|
|
57
|
+
|
|
58
|
+
if not console.is_terminal:
|
|
59
|
+
console.log("[~] Not a terminal — progress bar skipped.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
progress = 0
|
|
63
|
+
dir_count = 0
|
|
64
|
+
last_dir = "Waiting for directory..."
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
with Live(console=console, refresh_per_second=5) as live:
|
|
69
|
+
for line in tail_log_file(log_path, stop_event, session_marker):
|
|
70
|
+
lowered = line.lower()
|
|
71
|
+
|
|
72
|
+
updated = False
|
|
73
|
+
|
|
74
|
+
# Update directory name on "Inspecting"
|
|
75
|
+
if "inspecting directory" in lowered and "finished" not in lowered:
|
|
76
|
+
last_dir = line.split("Inspecting directory")[-1].strip()
|
|
77
|
+
updated = True
|
|
78
|
+
|
|
79
|
+
# Advance progress on "Finished"
|
|
80
|
+
if "finished inspecting directory" in lowered:
|
|
81
|
+
dir_count += 1
|
|
82
|
+
progress = (progress + 1) % (max_width + 1)
|
|
83
|
+
updated = True
|
|
84
|
+
|
|
85
|
+
if updated:
|
|
86
|
+
bar_text = ""
|
|
87
|
+
for i in range(max_width):
|
|
88
|
+
if i < progress:
|
|
89
|
+
color = get_green_shade(i, max_width)
|
|
90
|
+
bar_text += f"[{color}]#[/{color}]"
|
|
91
|
+
else:
|
|
92
|
+
bar_text += "-"
|
|
93
|
+
|
|
94
|
+
bar = Text.from_markup(f"[white][{bar_text}][/white] [dim]Dirs: {dir_count}[/dim]")
|
|
95
|
+
dir_display = Text(f"📂 {last_dir}", style="dim")
|
|
96
|
+
|
|
97
|
+
live.update(Group(bar, dir_display))
|
|
98
|
+
|
|
99
|
+
if stop_event.is_set():
|
|
100
|
+
break
|
|
101
|
+
|