86 lines
3.3 KiB
Python
86 lines
3.3 KiB
Python
"""Резервные копии (только администратор): список, создание, скачивание, загрузка, восстановление, удаление."""
|
|
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)
|