530a8b70a1
首页卡片支持修改/删除;详情页设置 Tab 可维护学校、年级与头像;系统设置新增数据备份下载与恢复;备份默认存 /root/grade-archive-backups,详见 docs/BACKUP.md。 Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||
from fastapi.responses import FileResponse
|
||
|
||
from app.core.deps import get_superuser
|
||
from app.models.user import User
|
||
from app.schemas import BackupInfoOut
|
||
from app.services import backup as backup_service
|
||
|
||
router = APIRouter(prefix="/admin/backups", tags=["admin-backups"])
|
||
|
||
|
||
@router.get("", response_model=list[BackupInfoOut])
|
||
def list_backups(_: User = Depends(get_superuser)):
|
||
return backup_service.list_backups()
|
||
|
||
|
||
@router.post("/run", response_model=BackupInfoOut)
|
||
def run_backup(_: User = Depends(get_superuser)):
|
||
try:
|
||
path = backup_service.create_backup()
|
||
except FileNotFoundError as exc:
|
||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||
except Exception as exc:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"备份失败: {exc}",
|
||
) from exc
|
||
stat = path.stat()
|
||
from datetime import datetime, timezone
|
||
|
||
return BackupInfoOut(
|
||
filename=path.name,
|
||
size_bytes=stat.st_size,
|
||
created_at=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc),
|
||
)
|
||
|
||
|
||
@router.get("/{filename}/download")
|
||
def download_backup(filename: str, _: User = Depends(get_superuser)):
|
||
try:
|
||
path = backup_service.resolve_backup_file(filename)
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||
except FileNotFoundError as exc:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="备份不存在") from exc
|
||
return FileResponse(
|
||
path,
|
||
media_type="application/gzip",
|
||
filename=filename,
|
||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||
)
|
||
|
||
|
||
@router.post("/restore")
|
||
async def restore_backup(file: UploadFile = File(...), _: User = Depends(get_superuser)):
|
||
if not file.filename or not file.filename.endswith(".tar.gz"):
|
||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请上传 .tar.gz 备份包")
|
||
content = await file.read()
|
||
if len(content) > 512 * 1024 * 1024:
|
||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="备份文件过大(最大 512MB)")
|
||
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
tmp = Path(tempfile.mkdtemp(prefix="grade-archive-upload-"))
|
||
archive = tmp / "restore.tar.gz"
|
||
try:
|
||
archive.write_bytes(content)
|
||
backup_service.restore_backup(archive)
|
||
except Exception as exc:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=f"恢复失败: {exc}",
|
||
) from exc
|
||
finally:
|
||
import shutil
|
||
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
|
||
return {"ok": True, "message": "数据已恢复,建议重启服务以确保缓存刷新"}
|