rozdeleni do struktury

This commit is contained in:
JiriUhlir
2026-05-27 12:15:11 +02:00
parent 12f6323185
commit 2b20444324
14 changed files with 749 additions and 661 deletions
+114
View File
@@ -0,0 +1,114 @@
import html
from pathlib import Path
from fastapi import APIRouter, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from ..backups import is_backup_path, list_backups
from ..config import BACKUP_SCRIPT
from ..shell import run_command
from ..templates.layout import page, render_result
router = APIRouter()
@router.get("/backups", response_class=HTMLResponse)
def backups_page():
rows = ""
for path in list_backups():
size_mb = path.stat().st_size / 1024 / 1024
backup_name = html.escape(path.name)
backup_path = html.escape(str(path))
restore_cmd = html.escape(
f"sudo /home/jiri/workspace/appfactory-tools/scripts/restore-appfactory.sh --force {path}"
)
rows += f"""
<tr>
<td>
<strong>{backup_name}</strong><br>
<span class="muted">{size_mb:.2f} MB</span>
</td>
<td>
<div class="cmd-row">
<input readonly value="{backup_path}" id="path-{backup_name}">
<button onclick="copyValue('path-{backup_name}')">Copy</button>
</div>
</td>
<td>
<div class="cmd-row">
<input readonly value="{restore_cmd}" id="restore-{backup_name}">
<button onclick="copyValue('restore-{backup_name}')">Copy</button>
</div>
<div class="muted">Restore is dangerous and must be run manually over SSH.</div>
</td>
<td>
<form method="post" action="/portal/backups/delete" onsubmit="return confirm('Delete backup {backup_name}?');">
<input type="hidden" name="backup_path" value="{backup_path}">
<button type="submit" class="danger">Delete</button>
</form>
</td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="4">No backups found.</td></tr>'
return page(
"Backups",
f"""
<div class="card">
<h2>Backup Management</h2>
<p class="muted">
Backups do not include the backup directory itself.
Restoring an older backup should not delete newer .tar.gz files.
Restore remains manual to avoid accidental overwrite.
</p>
<form method="post" action="/portal/backups/create">
<button type="submit">Create Backup</button>
</form>
</div>
<div class="card">
<h2>Available Backups</h2>
<table>
<tr>
<th>Backup</th>
<th>Path</th>
<th>Restore Command</th>
<th>Actions</th>
</tr>
{rows}
</table>
</div>
""",
)
@router.post("/backups/create", response_class=HTMLResponse)
def create_backup():
result = run_command([BACKUP_SCRIPT])
status = "OK" if result.returncode == 0 else "FAILED"
return render_result(
title=f"Create Backup: {status}",
back_url="/portal/backups",
sections=[("Output", result.stdout), ("Error", result.stderr)],
)
@router.post("/backups/delete")
def delete_backup(backup_path: str = Form(...)):
target = Path(backup_path)
if not is_backup_path(target):
return HTMLResponse("Invalid backup path", status_code=400)
target = target.resolve()
if target.exists():
target.unlink()
return RedirectResponse(url="/portal/backups", status_code=303)