Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit 67e411eea8
246 changed files with 23152 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
"""Резервные копии (только администратор): список, создание, скачивание, загрузка, восстановление, удаление."""
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)