# retoor import aiofiles from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from molodetz.attachments import format_size from molodetz.database import enqueue_backup, get_backup, list_backups from molodetz.dependencies import json_or_form from molodetz.models import BackupForm from molodetz.responses import action_result, respond from molodetz.routers.admin._shared import admin_seo from molodetz.schemas.admin import BackupsOut from molodetz.services.backup import backup_path from molodetz.utils.audit import record from molodetz.utils.auth import is_primary_admin, not_found, require_admin router = APIRouter() @router.get("/backups") async def backups_index(request: Request): user = require_admin(request) rows = [{**row, "size_label": format_size(row.get("size_bytes"))} for row in list_backups()] ctx = {"backups": rows, "can_download": is_primary_admin(user), "seo": admin_seo(request, "Backups")} return respond(request, "admin/backups.html", ctx, model=BackupsOut) @router.post("/backups") async def backups_enqueue(request: Request): user = require_admin(request) data = await json_or_form(request, BackupForm) row = enqueue_backup(data.target, user["uid"]) record(request, "backup.enqueue", payload={"target": data.target}, targets=[("backup", row["uid"])], user=user) return action_result(request, "/admin/backups", data={"uid": row["uid"]}) @router.get("/backups/{uid}/download") async def backups_download(request: Request, uid: str): user = require_admin(request) if not is_primary_admin(user): record(request, "backup.download", result="denied", targets=[("backup", uid)], user=user) raise HTTPException(403, "Primary administrator only") row = get_backup(uid) if row is None or row.get("status") != "done": not_found("Backup not found") path = backup_path(row) if not path.exists(): not_found("Archive missing") record(request, "backup.download", targets=[("backup", uid)], user=user) return StreamingResponse( _stream_file(path), media_type="application/octet-stream", headers={"Content-Disposition": f'attachment; filename="{row["stored_name"]}"', "Content-Length": str(path.stat().st_size)}, ) async def _stream_file(path, chunk_size=1024 * 256): async with aiofiles.open(path, "rb") as handle: while chunk := await handle.read(chunk_size): yield chunk