"""Резервные копии (только администратор): список, создание, скачивание, загрузка, восстановление, удаление.""" from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import FileResponse from pydantic import BaseModel, Field from sqlalchemy.orm import Session from .. import backup as B, config from ..db import get_db from ..models import User from ..security import require_admin router = APIRouter(prefix="/backups", tags=["backups"]) MAX_UPLOAD_MB = 2048 class CreateIn(BaseModel): include_media: bool = False note: str = Field(default="", max_length=200) class RestoreIn(BaseModel): confirm: bool = False def _err(e: B.BackupError): return HTTPException(409 if "демонстрацион" in str(e) else 422, str(e)) @router.get("") def list_(db: Session = Depends(get_db), _: User = Depends(require_admin)): return {"items": B.list_backups(), "folder": str(B.folder().resolve()), "auto_hours": int(B.setting(db, "backup_auto_hours") or 0), "keep": int(B.setting(db, "backup_keep") or 14), "media": B.setting(db, "backup_media") == "1", "next_auto": B.next_auto(db), "now": config.iso(config.now())} @router.post("", status_code=201) def create(data: CreateIn, db: Session = Depends(get_db), _: User = Depends(require_admin)): try: p = B.create(db, include_media=data.include_media, note=data.note.strip() or "ручная копия") except B.BackupError as e: raise _err(e) return next(x for x in B.list_backups() if x["name"] == p.name) @router.get("/{name}") def download(name: str, _: User = Depends(require_admin)): try: f = B.path_of(name) except B.BackupError as e: raise HTTPException(404, str(e)) return FileResponse(f, media_type="application/zip", filename=f.name) @router.delete("/{name}", status_code=204) def delete(name: str, _: User = Depends(require_admin)): try: B.path_of(name).unlink() except B.BackupError as e: raise HTTPException(404, str(e)) @router.post("/upload", status_code=201) async def upload(request: Request, _: User = Depends(require_admin)): """Загрузить файл копии (тело запроса — zip), например с другого сервера, чтобы потом восстановить.""" data = b"" async for chunk in request.stream(): data += chunk if len(data) > MAX_UPLOAD_MB * 1024 * 1024: raise HTTPException(413, "Файл слишком большой") try: p = B.save_upload(data) except B.BackupError as e: raise _err(e) return next(x for x in B.list_backups() if x["name"] == p.name) @router.post("/{name}/restore") def restore(name: str, data: RestoreIn, db: Session = Depends(get_db), _: User = Depends(require_admin)): """Заменить ВСЕ данные продуктовой базы содержимым копии. Текущее состояние сначала сохраняется отдельной копией.""" if not data.confirm: raise HTTPException(422, "Подтвердите восстановление (confirm: true)") try: return B.restore(db, name) except B.BackupError as e: raise _err(e)