astrapi-backup 26.4.6__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.
- astrapi_backup/__init__.py +1 -0
- astrapi_backup/_app.py +63 -0
- astrapi_backup/_cli.py +15 -0
- astrapi_backup/_paths.py +9 -0
- astrapi_backup/api/fastapi_app.py +48 -0
- astrapi_backup/api/routers/__init__.py +0 -0
- astrapi_backup/api/routers/run.py +249 -0
- astrapi_backup/api/storage.py +129 -0
- astrapi_backup/api/templates.py +100 -0
- astrapi_backup/app.yaml +2 -0
- astrapi_backup/modules/borg/__init__.py +14 -0
- astrapi_backup/modules/borg/api.py +488 -0
- astrapi_backup/modules/borg/cache.py +120 -0
- astrapi_backup/modules/borg/icon-outline.svg +1 -0
- astrapi_backup/modules/borg/icon.svg +1 -0
- astrapi_backup/modules/borg/jobs.py +334 -0
- astrapi_backup/modules/borg/modul.yaml +19 -0
- astrapi_backup/modules/borg/schema.yaml +61 -0
- astrapi_backup/modules/borg/settings.yaml +112 -0
- astrapi_backup/modules/borg/storage.py +215 -0
- astrapi_backup/modules/borg/templates/modals/archives.html +56 -0
- astrapi_backup/modules/borg/templates/modals/stats.html +55 -0
- astrapi_backup/modules/borg/templates/partials/archives_list.html +73 -0
- astrapi_backup/modules/borg/templates/partials/browse.html +290 -0
- astrapi_backup/modules/borg/templates/partials/card_body.html +9 -0
- astrapi_backup/modules/borg/templates/partials/list_header.html +2 -0
- astrapi_backup/modules/borg/templates/partials/list_row.html +6 -0
- astrapi_backup/modules/borg/templates/partials/stats_content.html +129 -0
- astrapi_backup/modules/borg/ui.py +58 -0
- astrapi_backup/modules/borg/utils.py +51 -0
- astrapi_backup/modules/proxmox_hosts/__init__.py +14 -0
- astrapi_backup/modules/proxmox_hosts/api.py +21 -0
- astrapi_backup/modules/proxmox_hosts/icon-outline.svg +1 -0
- astrapi_backup/modules/proxmox_hosts/icon.svg +1 -0
- astrapi_backup/modules/proxmox_hosts/jobs.py +182 -0
- astrapi_backup/modules/proxmox_hosts/modul.yaml +15 -0
- astrapi_backup/modules/proxmox_hosts/schema.yaml +9 -0
- astrapi_backup/modules/proxmox_hosts/settings.yaml +29 -0
- astrapi_backup/modules/proxmox_hosts/templates/modals/no_hosts.html +19 -0
- astrapi_backup/modules/proxmox_hosts/templates/partials/card_body.html +9 -0
- astrapi_backup/modules/proxmox_hosts/templates/partials/list_header.html +0 -0
- astrapi_backup/modules/proxmox_hosts/templates/partials/list_row.html +0 -0
- astrapi_backup/modules/proxmox_hosts/ui.py +174 -0
- astrapi_backup/modules/proxmox_jobs/__init__.py +16 -0
- astrapi_backup/modules/proxmox_jobs/api.py +81 -0
- astrapi_backup/modules/proxmox_jobs/icon-outline.svg +1 -0
- astrapi_backup/modules/proxmox_jobs/icon.svg +1 -0
- astrapi_backup/modules/proxmox_jobs/jobs.py +186 -0
- astrapi_backup/modules/proxmox_jobs/modul.yaml +15 -0
- astrapi_backup/modules/proxmox_jobs/schema.yaml +26 -0
- astrapi_backup/modules/proxmox_jobs/settings.yaml +3 -0
- astrapi_backup/modules/proxmox_jobs/templates/modals/create.html +53 -0
- astrapi_backup/modules/proxmox_jobs/templates/partials/available_select.html +24 -0
- astrapi_backup/modules/proxmox_jobs/templates/partials/card_body.html +6 -0
- astrapi_backup/modules/proxmox_jobs/templates/partials/list_header.html +2 -0
- astrapi_backup/modules/proxmox_jobs/templates/partials/list_row.html +16 -0
- astrapi_backup/modules/proxmox_jobs/ui.py +65 -0
- astrapi_backup/modules/proxmox_lxc/__init__.py +14 -0
- astrapi_backup/modules/proxmox_lxc/api.py +95 -0
- astrapi_backup/modules/proxmox_lxc/icon-outline.svg +1 -0
- astrapi_backup/modules/proxmox_lxc/icon.svg +1 -0
- astrapi_backup/modules/proxmox_lxc/jobs.py +229 -0
- astrapi_backup/modules/proxmox_lxc/modul.yaml +15 -0
- astrapi_backup/modules/proxmox_lxc/schema.yaml +23 -0
- astrapi_backup/modules/proxmox_lxc/settings.yaml +35 -0
- astrapi_backup/modules/proxmox_lxc/templates/modals/create.html +53 -0
- astrapi_backup/modules/proxmox_lxc/templates/partials/available_select.html +24 -0
- astrapi_backup/modules/proxmox_lxc/templates/partials/card_body.html +6 -0
- astrapi_backup/modules/proxmox_lxc/templates/partials/list_header.html +2 -0
- astrapi_backup/modules/proxmox_lxc/templates/partials/list_row.html +3 -0
- astrapi_backup/modules/proxmox_lxc/ui.py +97 -0
- astrapi_backup/modules/remotes/__init__.py +33 -0
- astrapi_backup/modules/remotes/api.py +152 -0
- astrapi_backup/modules/remotes/engine.py +73 -0
- astrapi_backup/modules/remotes/icon-outline.svg +1 -0
- astrapi_backup/modules/remotes/icon.svg +1 -0
- astrapi_backup/modules/remotes/jobs.py +152 -0
- astrapi_backup/modules/remotes/modul.yaml +7 -0
- astrapi_backup/modules/remotes/schema.yaml +116 -0
- astrapi_backup/modules/remotes/templates/modals/power_check.html +24 -0
- astrapi_backup/modules/remotes/templates/modals/ssh_check_results.html +32 -0
- astrapi_backup/modules/remotes/templates/partials/card_body.html +39 -0
- astrapi_backup/modules/remotes/templates/partials/extra_actions.html +9 -0
- astrapi_backup/modules/remotes/templates/partials/list_header.html +3 -0
- astrapi_backup/modules/remotes/templates/partials/list_row.html +24 -0
- astrapi_backup/modules/remotes/templates/partials/page_actions.html +6 -0
- astrapi_backup/modules/remotes/templates/partials/power_confirm.html +16 -0
- astrapi_backup/modules/remotes/templates/partials/ssh_check_spinner.html +14 -0
- astrapi_backup/modules/remotes/templates/partials/ssh_check_table.html +37 -0
- astrapi_backup/modules/remotes/ui.py +137 -0
- astrapi_backup/modules/rsync/__init__.py +15 -0
- astrapi_backup/modules/rsync/api.py +11 -0
- astrapi_backup/modules/rsync/icon-outline.svg +1 -0
- astrapi_backup/modules/rsync/icon.svg +1 -0
- astrapi_backup/modules/rsync/jobs.py +178 -0
- astrapi_backup/modules/rsync/modul.yaml +15 -0
- astrapi_backup/modules/rsync/schema.yaml +59 -0
- astrapi_backup/modules/rsync/settings.yaml +14 -0
- astrapi_backup/modules/rsync/templates/partials/card_body.html +21 -0
- astrapi_backup/modules/rsync/templates/partials/list_header.html +3 -0
- astrapi_backup/modules/rsync/templates/partials/list_row.html +13 -0
- astrapi_backup/modules/rsync/ui.py +66 -0
- astrapi_backup/navigation.yaml +29 -0
- astrapi_backup/overrides/system.py +76 -0
- astrapi_backup/runner.py +131 -0
- astrapi_backup/settings.py +1 -0
- astrapi_backup-26.4.6.dist-info/METADATA +18 -0
- astrapi_backup-26.4.6.dist-info/RECORD +111 -0
- astrapi_backup-26.4.6.dist-info/WHEEL +5 -0
- astrapi_backup-26.4.6.dist-info/entry_points.txt +2 -0
- astrapi_backup-26.4.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# astrapi_backup
|
astrapi_backup/_app.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""astrapi_backup._app – ASGI-App-Factory.
|
|
2
|
+
|
|
3
|
+
Wird von astrapi_backup._cli (Console-Script) und direkt von uvicorn importiert:
|
|
4
|
+
uvicorn astrapi_backup._app:app
|
|
5
|
+
"""
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from astrapi_core.system.paths import configure as _configure_paths
|
|
9
|
+
_configure_paths("astrapi-backup")
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
from fastapi.staticfiles import StaticFiles
|
|
13
|
+
|
|
14
|
+
from astrapi_core.ui import create as create_ui
|
|
15
|
+
from astrapi_core.ui.module_registry import load_modules
|
|
16
|
+
from astrapi_core.ui.settings_registry import init as settings_init
|
|
17
|
+
from astrapi_core.system.health import register_health
|
|
18
|
+
from astrapi_core.system.systemd import sd_notify, start_watchdog
|
|
19
|
+
from astrapi_core.system.version import get_display_name
|
|
20
|
+
from astrapi_core.modules.settings.engine import configure as configure_settings
|
|
21
|
+
from astrapi_core.modules.system.updater import configure as configure_updater
|
|
22
|
+
|
|
23
|
+
from astrapi_backup._paths import package_dir, work_dir
|
|
24
|
+
from astrapi_backup.api.fastapi_app import create as create_api
|
|
25
|
+
|
|
26
|
+
_START_TIME = time.time()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _db_check() -> tuple[bool, dict]:
|
|
30
|
+
from astrapi_core.system.db import _conn
|
|
31
|
+
try:
|
|
32
|
+
_conn().execute("SELECT 1").fetchone()
|
|
33
|
+
return True, {"db": True}
|
|
34
|
+
except Exception:
|
|
35
|
+
return False, {"db": False}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_app() -> FastAPI:
|
|
39
|
+
_pkg = package_dir()
|
|
40
|
+
configure_settings(health_fn=_db_check, app_name=get_display_name(_pkg))
|
|
41
|
+
configure_updater(_pkg)
|
|
42
|
+
|
|
43
|
+
from astrapi_backup.api.storage import init_db
|
|
44
|
+
init_db()
|
|
45
|
+
|
|
46
|
+
settings_init(work_dir())
|
|
47
|
+
modules, _ = load_modules(_pkg)
|
|
48
|
+
api = create_api(modules=modules)
|
|
49
|
+
|
|
50
|
+
import astrapi_core.ui
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
core_static = Path(astrapi_core.ui.__file__).parent / "static"
|
|
53
|
+
api.mount("/static", StaticFiles(directory=str(core_static)), name="static")
|
|
54
|
+
|
|
55
|
+
create_ui(api, app_root=_pkg, modules=modules)
|
|
56
|
+
|
|
57
|
+
register_health(api, check_fn=_db_check, start_time=_START_TIME)
|
|
58
|
+
start_watchdog(check_fn=lambda: _db_check()[0])
|
|
59
|
+
sd_notify("READY=1")
|
|
60
|
+
return api
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
app = create_app()
|
astrapi_backup/_cli.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""astrapi_backup._cli – Console-Script-Einstiegspunkt.
|
|
2
|
+
|
|
3
|
+
Start:
|
|
4
|
+
astrapi-backup --work-dir /opt/astrapi-backup --port 5001
|
|
5
|
+
astrapi-backup --work-dir /opt/astrapi-backup --port 5001 --debug # Debug-Modus (inkl. reload)
|
|
6
|
+
"""
|
|
7
|
+
from astrapi_core.system.paths import run_app
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
run_app("astrapi_backup._app:app", "astrapi-backup", default_port=5001)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main()
|
astrapi_backup/_paths.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# astrapi_backup/_paths.py
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from astrapi_core.system.paths import work_dir, db_path, log_dir # noqa: F401 – re-export
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def package_dir() -> Path:
|
|
8
|
+
"""Pfad zum installierten Package – für app.yaml, Templates, Modul-YAMLs."""
|
|
9
|
+
return Path(__file__).resolve().parent
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""astrapi_backup.api.fastapi_app – FastAPI-Factory."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
from astrapi_core.system.version import get_app_version
|
|
5
|
+
|
|
6
|
+
from astrapi_backup._paths import package_dir, log_dir
|
|
7
|
+
|
|
8
|
+
APP_ROOT = package_dir()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create(modules: list | None = None) -> FastAPI:
|
|
12
|
+
"""Erstellt die FastAPI-Anwendung.
|
|
13
|
+
|
|
14
|
+
modules: Vorgeladene Modulliste (z.B. aus _app.py). Wird nicht neu geladen
|
|
15
|
+
wenn angegeben – verhindert doppelten Modulaufruf.
|
|
16
|
+
"""
|
|
17
|
+
_version = get_app_version(APP_ROOT, default="1.0.0")
|
|
18
|
+
app = FastAPI(
|
|
19
|
+
title="BackupCtl API",
|
|
20
|
+
version=_version,
|
|
21
|
+
docs_url="/api/docs",
|
|
22
|
+
redoc_url="/api/redoc",
|
|
23
|
+
openapi_url="/api/openapi.json",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from astrapi_core.system.logger import configure_log_root
|
|
27
|
+
configure_log_root(log_dir())
|
|
28
|
+
|
|
29
|
+
from astrapi_core.system.secrets import configure as configure_secrets
|
|
30
|
+
configure_secrets(
|
|
31
|
+
key_path = Path("/var/lib/backupadm/secret.key"),
|
|
32
|
+
dev_key_path = package_dir() / "secret.key",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# ── Modul-Router registrieren (nur laden wenn nicht übergeben) ────────────────────
|
|
36
|
+
from astrapi_core.ui.module_registry import load_modules, register_fastapi_modules
|
|
37
|
+
if modules is None:
|
|
38
|
+
modules, _ = load_modules(APP_ROOT)
|
|
39
|
+
register_fastapi_modules(app, modules)
|
|
40
|
+
|
|
41
|
+
# ── Run/Log-Router pro Modul (Framework-Standard: /api/{module}/{item}/run) ─
|
|
42
|
+
from astrapi_backup.api.routers.run import make_run_router
|
|
43
|
+
_RUN_MODULES = ["borg", "rsync", "proxmox_lxc", "proxmox_hosts", "proxmox_jobs"]
|
|
44
|
+
for _mod_key in _RUN_MODULES:
|
|
45
|
+
app.include_router(make_run_router(_mod_key), prefix=f"/api/{_mod_key}")
|
|
46
|
+
app.include_router(make_run_router(_mod_key), prefix=f"/ui/{_mod_key}")
|
|
47
|
+
|
|
48
|
+
return app
|
|
File without changes
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# api/routers/run.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
7
|
+
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
8
|
+
|
|
9
|
+
from astrapi_core.system.logger import set_tee_context, clear_tee_context, set_active_log_id, clear_active_log_id
|
|
10
|
+
from astrapi_core.system.activity_log import (
|
|
11
|
+
history_start, history_finish,
|
|
12
|
+
get_log_lines, get_latest_activity_log_id, list_runs_for_item,
|
|
13
|
+
)
|
|
14
|
+
from astrapi_backup.api.storage import load_config
|
|
15
|
+
|
|
16
|
+
_templates = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_templates():
|
|
20
|
+
global _templates
|
|
21
|
+
if _templates is None:
|
|
22
|
+
from astrapi_backup.api.templates import templates as t
|
|
23
|
+
_templates = t
|
|
24
|
+
return _templates
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_running: dict = {}
|
|
28
|
+
_running_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_running(module: str, item_id: str) -> bool:
|
|
32
|
+
return f"{module}:{item_id}" in _running
|
|
33
|
+
|
|
34
|
+
def _mark_running(module: str, item_id: str, mode: str) -> None:
|
|
35
|
+
with _running_lock:
|
|
36
|
+
_running[f"{module}:{item_id}"] = mode
|
|
37
|
+
|
|
38
|
+
def _mark_done(module: str, item_id: str) -> None:
|
|
39
|
+
with _running_lock:
|
|
40
|
+
_running.pop(f"{module}:{item_id}", None)
|
|
41
|
+
|
|
42
|
+
def get_running() -> dict:
|
|
43
|
+
return dict(_running)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def make_run_router(module: str, *, has_run_buttons: bool = True) -> APIRouter:
|
|
47
|
+
"""Erzeugt einen APIRouter mit Run/Log-Routen für ein einzelnes Modul.
|
|
48
|
+
|
|
49
|
+
Einbinden mit prefix=f"/{module}" → externe URLs:
|
|
50
|
+
POST /api/{module}/{item_id}/run
|
|
51
|
+
POST /api/{module}/run
|
|
52
|
+
GET /api/{module}/status
|
|
53
|
+
GET /api/{module}/{item_id}/logs
|
|
54
|
+
GET /api/{module}/{item_id}/logs/stream
|
|
55
|
+
GET /api/{module}/{item_id}/logs/{log_id}
|
|
56
|
+
"""
|
|
57
|
+
router = APIRouter(tags=[module])
|
|
58
|
+
|
|
59
|
+
# ── Status-Endpunkt für Badge-Refresh ────────────────────────────
|
|
60
|
+
|
|
61
|
+
@router.get("/status", response_class=HTMLResponse)
|
|
62
|
+
def module_status(request: Request):
|
|
63
|
+
from astrapi_core.ui.crud_blueprint import resolve_filters_for_request
|
|
64
|
+
cfg = load_config(module)
|
|
65
|
+
cfg, extra = resolve_filters_for_request(module, request, cfg)
|
|
66
|
+
return _get_templates().TemplateResponse(
|
|
67
|
+
request,
|
|
68
|
+
"partials/list_wrapper_inner.html",
|
|
69
|
+
{
|
|
70
|
+
"cfg": cfg, "module": module,
|
|
71
|
+
"container_id": f"tab-{module}", "loading_id": f"{module}-loading",
|
|
72
|
+
"content_template": f"{module}/partials/card_body.html",
|
|
73
|
+
"running": get_running(),
|
|
74
|
+
"has_run_buttons": has_run_buttons,
|
|
75
|
+
**extra,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ── Einzelnen Eintrag ausführen ───────────────────────────────────
|
|
80
|
+
|
|
81
|
+
@router.post("/{item_id}/run", response_class=HTMLResponse)
|
|
82
|
+
def run_item(item_id: str, request: Request, debug: bool = False):
|
|
83
|
+
if _is_running(module, item_id):
|
|
84
|
+
raise HTTPException(status_code=409, detail="Läuft bereits")
|
|
85
|
+
|
|
86
|
+
_mark_running(module, item_id, "debug" if debug else "run")
|
|
87
|
+
|
|
88
|
+
log_id = f"{item_id}_debug" if debug else item_id
|
|
89
|
+
|
|
90
|
+
def _execute():
|
|
91
|
+
import time
|
|
92
|
+
desc = _item_description(module, item_id)
|
|
93
|
+
hist_id = history_start(module, item_id, desc, "debug" if debug else "run")
|
|
94
|
+
t0 = time.time()
|
|
95
|
+
set_tee_context(module, log_id)
|
|
96
|
+
set_active_log_id(hist_id)
|
|
97
|
+
status = "ok"
|
|
98
|
+
try:
|
|
99
|
+
_dispatch_single(module, item_id)
|
|
100
|
+
except Exception:
|
|
101
|
+
status = "error"
|
|
102
|
+
finally:
|
|
103
|
+
duration = int(time.time() - t0)
|
|
104
|
+
# Status aus Log-Zeilen ableiten falls kein Exception-Fehler
|
|
105
|
+
if status == "ok":
|
|
106
|
+
levels = {r["level"] for r in get_log_lines(hist_id)}
|
|
107
|
+
if "ERROR" in levels:
|
|
108
|
+
status = "error"
|
|
109
|
+
elif "WARNING" in levels:
|
|
110
|
+
status = "warning"
|
|
111
|
+
history_finish(hist_id, status, duration)
|
|
112
|
+
clear_active_log_id()
|
|
113
|
+
clear_tee_context()
|
|
114
|
+
_mark_done(module, item_id)
|
|
115
|
+
if not debug:
|
|
116
|
+
from astrapi_core.modules.scheduler.job_runner import _notify
|
|
117
|
+
_notify(module, desc, status, duration)
|
|
118
|
+
|
|
119
|
+
threading.Thread(target=_execute, daemon=True).start()
|
|
120
|
+
|
|
121
|
+
from astrapi_core.ui.crud_blueprint import resolve_filters_for_request
|
|
122
|
+
cfg = load_config(module)
|
|
123
|
+
cfg, extra = resolve_filters_for_request(module, request, cfg)
|
|
124
|
+
list_html = _get_templates().TemplateResponse(
|
|
125
|
+
request,
|
|
126
|
+
"partials/list_wrapper_inner.html",
|
|
127
|
+
{
|
|
128
|
+
"cfg": cfg, "module": module,
|
|
129
|
+
"container_id": f"tab-{module}", "loading_id": f"{module}-loading",
|
|
130
|
+
"content_template": f"{module}/partials/card_body.html",
|
|
131
|
+
"running": get_running(),
|
|
132
|
+
"has_run_buttons": has_run_buttons,
|
|
133
|
+
**extra,
|
|
134
|
+
},
|
|
135
|
+
).body.decode()
|
|
136
|
+
|
|
137
|
+
trigger = json.dumps({"openLogModal": {"module": module, "itemId": log_id}})
|
|
138
|
+
return HTMLResponse(list_html, headers={"HX-Trigger": trigger})
|
|
139
|
+
|
|
140
|
+
# ── SSE: Live-Log-Stream (DB-basiert) ─────────────────────────────
|
|
141
|
+
|
|
142
|
+
@router.get("/{item_id}/logs/stream")
|
|
143
|
+
async def stream_log_ep(item_id: str):
|
|
144
|
+
async def event_generator():
|
|
145
|
+
|
|
146
|
+
# Warte bis activity_log-Eintrag existiert (Job startet im Thread)
|
|
147
|
+
act_log_id = None
|
|
148
|
+
waited = 0.0
|
|
149
|
+
while act_log_id is None and waited < 15:
|
|
150
|
+
act_log_id = get_latest_activity_log_id(module, item_id)
|
|
151
|
+
if act_log_id is None:
|
|
152
|
+
await asyncio.sleep(0.3)
|
|
153
|
+
waited += 0.3
|
|
154
|
+
|
|
155
|
+
if act_log_id is None:
|
|
156
|
+
yield "event: done\ndata: \n\n"
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
last_id = 0
|
|
160
|
+
idle_after_done = 0.0
|
|
161
|
+
|
|
162
|
+
while True:
|
|
163
|
+
rows = get_log_lines(act_log_id, after_id=last_id)
|
|
164
|
+
for row in rows:
|
|
165
|
+
last_id = row["id"]
|
|
166
|
+
level = row["level"].lower()
|
|
167
|
+
safe = (row["line"]
|
|
168
|
+
.replace("&", "&")
|
|
169
|
+
.replace("<", "<")
|
|
170
|
+
.replace(">", ">"))
|
|
171
|
+
yield f"data: <div class=\"log-line log-{level}\">{safe}</div>\n\n"
|
|
172
|
+
|
|
173
|
+
running_key = item_id.removesuffix("_debug") if item_id.endswith("_debug") else item_id
|
|
174
|
+
still_running = _is_running(module, running_key) or _is_running(module, item_id)
|
|
175
|
+
|
|
176
|
+
if not still_running:
|
|
177
|
+
idle_after_done += 0.5
|
|
178
|
+
if idle_after_done >= 3:
|
|
179
|
+
yield "event: done\ndata: \n\n"
|
|
180
|
+
return
|
|
181
|
+
else:
|
|
182
|
+
idle_after_done = 0.0
|
|
183
|
+
|
|
184
|
+
await asyncio.sleep(0.5)
|
|
185
|
+
|
|
186
|
+
return StreamingResponse(
|
|
187
|
+
event_generator(),
|
|
188
|
+
media_type="text/event-stream",
|
|
189
|
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# ── Log-Endpunkte (DB-basiert) ────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
@router.get("/{item_id}/logs", response_class=HTMLResponse)
|
|
195
|
+
def get_logs(item_id: str, request: Request, live: int = 0):
|
|
196
|
+
runs = list_runs_for_item(module, item_id)
|
|
197
|
+
act_log_id = runs[0]["id"] if runs else None
|
|
198
|
+
lines = [r["line"] for r in get_log_lines(act_log_id)] if act_log_id else []
|
|
199
|
+
|
|
200
|
+
# "dates" = Liste von Runs für den Datum-Wähler im Modal
|
|
201
|
+
dates = [{"id": str(r["id"]), "label": r["started_at"] or str(r["id"])} for r in runs]
|
|
202
|
+
selected = str(act_log_id) if act_log_id else None
|
|
203
|
+
|
|
204
|
+
return _get_templates().TemplateResponse(
|
|
205
|
+
request,
|
|
206
|
+
"partials/log_modal.html",
|
|
207
|
+
{
|
|
208
|
+
"module": module, "item_id": item_id,
|
|
209
|
+
"description": _item_description(module, item_id),
|
|
210
|
+
"dates": dates, "selected": selected, "lines": lines,
|
|
211
|
+
"live": bool(live),
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@router.get("/{item_id}/logs/{log_id}", response_class=HTMLResponse)
|
|
216
|
+
def get_log_by_id(item_id: str, log_id: str, request: Request):
|
|
217
|
+
lines = [r["line"] for r in get_log_lines(int(log_id))] if log_id.isdigit() else []
|
|
218
|
+
return _get_templates().TemplateResponse(
|
|
219
|
+
request,
|
|
220
|
+
"partials/log_content.html",
|
|
221
|
+
{"lines": lines, "date": log_id},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return router
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── Hilfsfunktionen ───────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
def _item_description(module: str, item_id: str) -> str:
|
|
230
|
+
debug = item_id.endswith("_debug")
|
|
231
|
+
base = item_id.removesuffix("_debug")
|
|
232
|
+
try:
|
|
233
|
+
cfg = load_config(module)
|
|
234
|
+
raw = cfg.get(base) or cfg.get(int(base) if base.isdigit() else base) or {}
|
|
235
|
+
desc = raw.get("description") or raw.get("job") or raw.get("host") or raw.get("source_path") or base
|
|
236
|
+
return f"{desc} (Debug)" if debug else desc
|
|
237
|
+
except Exception:
|
|
238
|
+
return item_id
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _dispatch_single(module: str, item_id: str) -> None:
|
|
242
|
+
import importlib
|
|
243
|
+
try:
|
|
244
|
+
mod = importlib.import_module(f"astrapi_backup.modules.{module}.jobs")
|
|
245
|
+
except ModuleNotFoundError:
|
|
246
|
+
from astrapi_core.system.logger import log
|
|
247
|
+
log("ERROR", f"Unbekanntes Modul: {module}")
|
|
248
|
+
return
|
|
249
|
+
mod.run_single(item_id)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# api/storage.py
|
|
2
|
+
"""SQLite-Backend mit einer Tabelle pro Modul."""
|
|
3
|
+
|
|
4
|
+
from astrapi_core.system.db import (
|
|
5
|
+
configure as _configure_db,
|
|
6
|
+
register_table, create_all_registered_tables,
|
|
7
|
+
load_config, get_item, save_item, delete_item, next_item_id, get_entry, patch_item,
|
|
8
|
+
)
|
|
9
|
+
from astrapi_core.system.activity_log import (
|
|
10
|
+
log_activity, update_activity_log,
|
|
11
|
+
list_activity, get_activity_log, clear_activity_log,
|
|
12
|
+
get_latest_activity_log_id, list_runs_for_item,
|
|
13
|
+
history_start, history_finish, list_history,
|
|
14
|
+
append_log_line, get_log_lines,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from astrapi_backup._paths import db_path as _db_path
|
|
18
|
+
|
|
19
|
+
DB_PATH = _db_path()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── App-Tabellen-Konfiguration ─────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
_APP_TABLES = {
|
|
25
|
+
"borg": {
|
|
26
|
+
"ddl": """
|
|
27
|
+
CREATE TABLE IF NOT EXISTS borg (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
30
|
+
description TEXT NOT NULL DEFAULT '',
|
|
31
|
+
source_remote_id TEXT,
|
|
32
|
+
source_path TEXT NOT NULL DEFAULT '',
|
|
33
|
+
target_remote_id TEXT,
|
|
34
|
+
target_path TEXT NOT NULL DEFAULT '',
|
|
35
|
+
pre_hooks TEXT,
|
|
36
|
+
post_hooks TEXT,
|
|
37
|
+
exclude TEXT,
|
|
38
|
+
last_run TEXT,
|
|
39
|
+
last_status TEXT
|
|
40
|
+
)""",
|
|
41
|
+
"list_fields": ["pre_hooks", "post_hooks", "exclude"],
|
|
42
|
+
"col_in": {"pre_hooks": "pre", "post_hooks": "post"},
|
|
43
|
+
"col_out": {"pre": "pre_hooks", "post": "post_hooks"},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"rsync": {
|
|
47
|
+
"ddl": """
|
|
48
|
+
CREATE TABLE IF NOT EXISTS rsync (
|
|
49
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
50
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
51
|
+
description TEXT NOT NULL DEFAULT '',
|
|
52
|
+
type TEXT NOT NULL DEFAULT '',
|
|
53
|
+
source_remote_id TEXT,
|
|
54
|
+
source_path TEXT NOT NULL DEFAULT '',
|
|
55
|
+
target_remote_id TEXT,
|
|
56
|
+
target_path TEXT NOT NULL DEFAULT '',
|
|
57
|
+
last_run TEXT,
|
|
58
|
+
last_status TEXT
|
|
59
|
+
)""",
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
"proxmox_lxc": {
|
|
63
|
+
"ddl": """
|
|
64
|
+
CREATE TABLE IF NOT EXISTS proxmox_lxc (
|
|
65
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
66
|
+
vmid INTEGER NOT NULL,
|
|
67
|
+
description TEXT NOT NULL DEFAULT '',
|
|
68
|
+
node TEXT NOT NULL DEFAULT '',
|
|
69
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
70
|
+
last_run TEXT,
|
|
71
|
+
last_status TEXT
|
|
72
|
+
)""",
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"proxmox_hosts": {
|
|
76
|
+
"ddl": """
|
|
77
|
+
CREATE TABLE IF NOT EXISTS proxmox_hosts (
|
|
78
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
|
+
description TEXT NOT NULL DEFAULT '',
|
|
80
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
81
|
+
remote_id TEXT,
|
|
82
|
+
extra_sources TEXT,
|
|
83
|
+
last_run TEXT,
|
|
84
|
+
last_status TEXT
|
|
85
|
+
)""",
|
|
86
|
+
"list_fields": ["extra_sources"],
|
|
87
|
+
"col_in": {"extra_sources": "source"},
|
|
88
|
+
"col_out": {"source": "extra_sources"},
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
"proxmox_jobs": {
|
|
92
|
+
"ddl": """
|
|
93
|
+
CREATE TABLE IF NOT EXISTS proxmox_jobs (
|
|
94
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
95
|
+
job TEXT NOT NULL DEFAULT '',
|
|
96
|
+
remote_id TEXT,
|
|
97
|
+
type TEXT NOT NULL DEFAULT '',
|
|
98
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
99
|
+
last_run TEXT,
|
|
100
|
+
last_status TEXT
|
|
101
|
+
)""",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _register_app_tables() -> None:
|
|
107
|
+
for key, cfg in _APP_TABLES.items():
|
|
108
|
+
register_table(
|
|
109
|
+
key,
|
|
110
|
+
cfg["ddl"],
|
|
111
|
+
list_fields=cfg.get("list_fields"),
|
|
112
|
+
col_in=cfg.get("col_in"),
|
|
113
|
+
col_out=cfg.get("col_out"),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def init_db() -> None:
|
|
118
|
+
_configure_db(DB_PATH)
|
|
119
|
+
_register_app_tables()
|
|
120
|
+
create_all_registered_tables()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Borg-spezifischer Cache → app/modules/borg/storage.py
|
|
124
|
+
from astrapi_backup.modules.borg.storage import (
|
|
125
|
+
save_archive_list_cache, save_archive_cache,
|
|
126
|
+
get_archive_cache, archive_is_cached,
|
|
127
|
+
get_file_cache, save_file_cache_for_archive,
|
|
128
|
+
get_stats_cache, save_stats_cache,
|
|
129
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# api/templates.py – zentrale Jinja2-Instanz mit ChoiceLoader (app/ > module/ > core/)
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from fastapi.templating import Jinja2Templates
|
|
4
|
+
from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
|
|
5
|
+
|
|
6
|
+
from astrapi_backup._paths import package_dir as _package_dir
|
|
7
|
+
|
|
8
|
+
_APP_ROOT = _package_dir()
|
|
9
|
+
_APP_TEMPLATES = _APP_ROOT / "templates"
|
|
10
|
+
|
|
11
|
+
import astrapi_core.ui as _astrapi_core_ui
|
|
12
|
+
_CORE_TEMPLATES = Path(_astrapi_core_ui.__file__).resolve().parent / "templates"
|
|
13
|
+
|
|
14
|
+
# Basis-Loader: app/templates/ > core/ui/templates/
|
|
15
|
+
_base_loaders: list = [
|
|
16
|
+
FileSystemLoader(str(_APP_TEMPLATES)),
|
|
17
|
+
FileSystemLoader(str(_CORE_TEMPLATES)),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# PrefixLoader für jedes Modul das ein templates/-Unterverzeichnis hat
|
|
21
|
+
# → render_template("borg/partials/list.html") → modules/borg/templates/partials/list.html
|
|
22
|
+
_prefix_loaders: list = []
|
|
23
|
+
_CORE_MODULES = Path(_astrapi_core_ui.__file__).resolve().parents[1] / "modules"
|
|
24
|
+
for _search_root in (_APP_ROOT / "modules", _CORE_MODULES):
|
|
25
|
+
if not _search_root.is_dir():
|
|
26
|
+
continue
|
|
27
|
+
for _mod_dir in sorted(_search_root.iterdir()):
|
|
28
|
+
if not _mod_dir.is_dir() or _mod_dir.name.startswith("_"):
|
|
29
|
+
continue
|
|
30
|
+
_tpl_dir = _mod_dir / "templates"
|
|
31
|
+
if _tpl_dir.is_dir():
|
|
32
|
+
_prefix_loaders.append(
|
|
33
|
+
PrefixLoader({_mod_dir.name: FileSystemLoader(str(_tpl_dir))})
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Environment direkt bauen und per env= übergeben – vermeidet den Jinja2 3.1.5+
|
|
37
|
+
# Cache-Key-Bug bei dem globals (dict) unhashbar als LRU-Key verwendet wird.
|
|
38
|
+
_env = Environment(loader=ChoiceLoader(_prefix_loaders + _base_loaders), autoescape=True, cache_size=0)
|
|
39
|
+
templates = Jinja2Templates(env=_env)
|
|
40
|
+
|
|
41
|
+
# Jinja2-Instanz für core-Module bereitstellen
|
|
42
|
+
from astrapi_core.ui.fastapi_templates import configure as _configure_fastapi_templates
|
|
43
|
+
_configure_fastapi_templates(templates)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Template-Globals: Funktionen die content.html / list_wrapper_inner.html braucht ──
|
|
47
|
+
# (entsprechen den Flask-Context-Processor-Funktionen in core/ui/app.py)
|
|
48
|
+
|
|
49
|
+
def _module_label(key: str) -> str:
|
|
50
|
+
from astrapi_core.ui.module_registry import _mod_registry
|
|
51
|
+
m = _mod_registry.get(key)
|
|
52
|
+
return m.label if m else key.replace("_", " ").title()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _module_has_settings(key: str) -> bool:
|
|
56
|
+
from astrapi_core.ui.module_registry import _mod_registry
|
|
57
|
+
m = _mod_registry.get(key)
|
|
58
|
+
return bool(m and m.settings_schema and m.settings_button)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _module_card_actions(key: str) -> list:
|
|
62
|
+
from astrapi_core.ui.module_registry import _mod_registry
|
|
63
|
+
m = _mod_registry.get(key)
|
|
64
|
+
return m.card_actions if m else []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _col_widths(module_key: str) -> str:
|
|
68
|
+
from astrapi_core.ui.settings_registry import get as settings_get
|
|
69
|
+
return settings_get(f"ui.col_widths.{module_key}", "{}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _resolve_remote_host(remote_id) -> str:
|
|
73
|
+
if not remote_id:
|
|
74
|
+
return "—"
|
|
75
|
+
try:
|
|
76
|
+
from astrapi_backup.modules.remotes.engine import get_remote
|
|
77
|
+
r = get_remote(remote_id)
|
|
78
|
+
return r.get("host") or "—" if r else "—"
|
|
79
|
+
except Exception:
|
|
80
|
+
return "—"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _last_run_status(module: str, item_id) -> str | None:
|
|
84
|
+
try:
|
|
85
|
+
from astrapi_core.system.activity_log import list_runs_for_item
|
|
86
|
+
runs = list_runs_for_item(module, str(item_id), limit=5)
|
|
87
|
+
for run in runs:
|
|
88
|
+
if run.get("status") != "running":
|
|
89
|
+
return run.get("status")
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
_env.globals["module_label"] = _module_label
|
|
96
|
+
_env.globals["module_has_settings"] = _module_has_settings
|
|
97
|
+
_env.globals["module_card_actions"] = _module_card_actions
|
|
98
|
+
_env.globals["col_widths"] = _col_widths
|
|
99
|
+
_env.globals["resolve_remote_host"] = _resolve_remote_host
|
|
100
|
+
_env.globals["last_run_status"] = _last_run_status
|
astrapi_backup/app.yaml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from astrapi_core.ui.module_loader import load_modul
|
|
3
|
+
from .jobs import run, run_single # re-export fuer api/routers/run.py
|
|
4
|
+
from .api import router
|
|
5
|
+
from .ui import router as ui_router
|
|
6
|
+
|
|
7
|
+
_KEY = Path(__file__).parent.name
|
|
8
|
+
module = load_modul(Path(__file__).parent, _KEY, router, ui_router)
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from astrapi_core.modules.scheduler.engine import register_action
|
|
12
|
+
register_action(f"{_KEY}.run", "Borg: Backup ausführen", run, source=_KEY, source_label="Borg")
|
|
13
|
+
except Exception:
|
|
14
|
+
pass
|