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.
Files changed (111) hide show
  1. astrapi_backup/__init__.py +1 -0
  2. astrapi_backup/_app.py +63 -0
  3. astrapi_backup/_cli.py +15 -0
  4. astrapi_backup/_paths.py +9 -0
  5. astrapi_backup/api/fastapi_app.py +48 -0
  6. astrapi_backup/api/routers/__init__.py +0 -0
  7. astrapi_backup/api/routers/run.py +249 -0
  8. astrapi_backup/api/storage.py +129 -0
  9. astrapi_backup/api/templates.py +100 -0
  10. astrapi_backup/app.yaml +2 -0
  11. astrapi_backup/modules/borg/__init__.py +14 -0
  12. astrapi_backup/modules/borg/api.py +488 -0
  13. astrapi_backup/modules/borg/cache.py +120 -0
  14. astrapi_backup/modules/borg/icon-outline.svg +1 -0
  15. astrapi_backup/modules/borg/icon.svg +1 -0
  16. astrapi_backup/modules/borg/jobs.py +334 -0
  17. astrapi_backup/modules/borg/modul.yaml +19 -0
  18. astrapi_backup/modules/borg/schema.yaml +61 -0
  19. astrapi_backup/modules/borg/settings.yaml +112 -0
  20. astrapi_backup/modules/borg/storage.py +215 -0
  21. astrapi_backup/modules/borg/templates/modals/archives.html +56 -0
  22. astrapi_backup/modules/borg/templates/modals/stats.html +55 -0
  23. astrapi_backup/modules/borg/templates/partials/archives_list.html +73 -0
  24. astrapi_backup/modules/borg/templates/partials/browse.html +290 -0
  25. astrapi_backup/modules/borg/templates/partials/card_body.html +9 -0
  26. astrapi_backup/modules/borg/templates/partials/list_header.html +2 -0
  27. astrapi_backup/modules/borg/templates/partials/list_row.html +6 -0
  28. astrapi_backup/modules/borg/templates/partials/stats_content.html +129 -0
  29. astrapi_backup/modules/borg/ui.py +58 -0
  30. astrapi_backup/modules/borg/utils.py +51 -0
  31. astrapi_backup/modules/proxmox_hosts/__init__.py +14 -0
  32. astrapi_backup/modules/proxmox_hosts/api.py +21 -0
  33. astrapi_backup/modules/proxmox_hosts/icon-outline.svg +1 -0
  34. astrapi_backup/modules/proxmox_hosts/icon.svg +1 -0
  35. astrapi_backup/modules/proxmox_hosts/jobs.py +182 -0
  36. astrapi_backup/modules/proxmox_hosts/modul.yaml +15 -0
  37. astrapi_backup/modules/proxmox_hosts/schema.yaml +9 -0
  38. astrapi_backup/modules/proxmox_hosts/settings.yaml +29 -0
  39. astrapi_backup/modules/proxmox_hosts/templates/modals/no_hosts.html +19 -0
  40. astrapi_backup/modules/proxmox_hosts/templates/partials/card_body.html +9 -0
  41. astrapi_backup/modules/proxmox_hosts/templates/partials/list_header.html +0 -0
  42. astrapi_backup/modules/proxmox_hosts/templates/partials/list_row.html +0 -0
  43. astrapi_backup/modules/proxmox_hosts/ui.py +174 -0
  44. astrapi_backup/modules/proxmox_jobs/__init__.py +16 -0
  45. astrapi_backup/modules/proxmox_jobs/api.py +81 -0
  46. astrapi_backup/modules/proxmox_jobs/icon-outline.svg +1 -0
  47. astrapi_backup/modules/proxmox_jobs/icon.svg +1 -0
  48. astrapi_backup/modules/proxmox_jobs/jobs.py +186 -0
  49. astrapi_backup/modules/proxmox_jobs/modul.yaml +15 -0
  50. astrapi_backup/modules/proxmox_jobs/schema.yaml +26 -0
  51. astrapi_backup/modules/proxmox_jobs/settings.yaml +3 -0
  52. astrapi_backup/modules/proxmox_jobs/templates/modals/create.html +53 -0
  53. astrapi_backup/modules/proxmox_jobs/templates/partials/available_select.html +24 -0
  54. astrapi_backup/modules/proxmox_jobs/templates/partials/card_body.html +6 -0
  55. astrapi_backup/modules/proxmox_jobs/templates/partials/list_header.html +2 -0
  56. astrapi_backup/modules/proxmox_jobs/templates/partials/list_row.html +16 -0
  57. astrapi_backup/modules/proxmox_jobs/ui.py +65 -0
  58. astrapi_backup/modules/proxmox_lxc/__init__.py +14 -0
  59. astrapi_backup/modules/proxmox_lxc/api.py +95 -0
  60. astrapi_backup/modules/proxmox_lxc/icon-outline.svg +1 -0
  61. astrapi_backup/modules/proxmox_lxc/icon.svg +1 -0
  62. astrapi_backup/modules/proxmox_lxc/jobs.py +229 -0
  63. astrapi_backup/modules/proxmox_lxc/modul.yaml +15 -0
  64. astrapi_backup/modules/proxmox_lxc/schema.yaml +23 -0
  65. astrapi_backup/modules/proxmox_lxc/settings.yaml +35 -0
  66. astrapi_backup/modules/proxmox_lxc/templates/modals/create.html +53 -0
  67. astrapi_backup/modules/proxmox_lxc/templates/partials/available_select.html +24 -0
  68. astrapi_backup/modules/proxmox_lxc/templates/partials/card_body.html +6 -0
  69. astrapi_backup/modules/proxmox_lxc/templates/partials/list_header.html +2 -0
  70. astrapi_backup/modules/proxmox_lxc/templates/partials/list_row.html +3 -0
  71. astrapi_backup/modules/proxmox_lxc/ui.py +97 -0
  72. astrapi_backup/modules/remotes/__init__.py +33 -0
  73. astrapi_backup/modules/remotes/api.py +152 -0
  74. astrapi_backup/modules/remotes/engine.py +73 -0
  75. astrapi_backup/modules/remotes/icon-outline.svg +1 -0
  76. astrapi_backup/modules/remotes/icon.svg +1 -0
  77. astrapi_backup/modules/remotes/jobs.py +152 -0
  78. astrapi_backup/modules/remotes/modul.yaml +7 -0
  79. astrapi_backup/modules/remotes/schema.yaml +116 -0
  80. astrapi_backup/modules/remotes/templates/modals/power_check.html +24 -0
  81. astrapi_backup/modules/remotes/templates/modals/ssh_check_results.html +32 -0
  82. astrapi_backup/modules/remotes/templates/partials/card_body.html +39 -0
  83. astrapi_backup/modules/remotes/templates/partials/extra_actions.html +9 -0
  84. astrapi_backup/modules/remotes/templates/partials/list_header.html +3 -0
  85. astrapi_backup/modules/remotes/templates/partials/list_row.html +24 -0
  86. astrapi_backup/modules/remotes/templates/partials/page_actions.html +6 -0
  87. astrapi_backup/modules/remotes/templates/partials/power_confirm.html +16 -0
  88. astrapi_backup/modules/remotes/templates/partials/ssh_check_spinner.html +14 -0
  89. astrapi_backup/modules/remotes/templates/partials/ssh_check_table.html +37 -0
  90. astrapi_backup/modules/remotes/ui.py +137 -0
  91. astrapi_backup/modules/rsync/__init__.py +15 -0
  92. astrapi_backup/modules/rsync/api.py +11 -0
  93. astrapi_backup/modules/rsync/icon-outline.svg +1 -0
  94. astrapi_backup/modules/rsync/icon.svg +1 -0
  95. astrapi_backup/modules/rsync/jobs.py +178 -0
  96. astrapi_backup/modules/rsync/modul.yaml +15 -0
  97. astrapi_backup/modules/rsync/schema.yaml +59 -0
  98. astrapi_backup/modules/rsync/settings.yaml +14 -0
  99. astrapi_backup/modules/rsync/templates/partials/card_body.html +21 -0
  100. astrapi_backup/modules/rsync/templates/partials/list_header.html +3 -0
  101. astrapi_backup/modules/rsync/templates/partials/list_row.html +13 -0
  102. astrapi_backup/modules/rsync/ui.py +66 -0
  103. astrapi_backup/navigation.yaml +29 -0
  104. astrapi_backup/overrides/system.py +76 -0
  105. astrapi_backup/runner.py +131 -0
  106. astrapi_backup/settings.py +1 -0
  107. astrapi_backup-26.4.6.dist-info/METADATA +18 -0
  108. astrapi_backup-26.4.6.dist-info/RECORD +111 -0
  109. astrapi_backup-26.4.6.dist-info/WHEEL +5 -0
  110. astrapi_backup-26.4.6.dist-info/entry_points.txt +2 -0
  111. 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()
@@ -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("&", "&amp;")
169
+ .replace("<", "&lt;")
170
+ .replace(">", "&gt;"))
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
@@ -0,0 +1,2 @@
1
+ name: astrapi-backup
2
+ display_name: Backup Control
@@ -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