Files
system-backu/backend/app/api/restore.py
T
publ 4e3a4b4602 feat: 数据备份管理系统 v0.1.0
带 Web 操作界面的备份系统,支持:
- 数据源:MySQL 数据库(mysqldump)、服务器目录(tar.gz)
- 存储目标:本地目录、S3 兼容对象存储(MinIO/Ceph/AWS S3)
- 触发方式:手动 + Cron 定时调度(APScheduler)
- 备份还原:MySQL 库、目录
- JWT 登录认证 + Fernet 字段加密
- 保留策略:按数量 + 按天数双重清理

技术栈:FastAPI + SQLAlchemy 2 + APScheduler + aioboto3;
前端 Vite + React 18 + TypeScript + Ant Design 5 + Zustand。
Docker Compose 一键起。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-22 18:27:41 +08:00

77 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""恢复 API。"""
import asyncio
from datetime import datetime
from fastapi import APIRouter, HTTPException
from app.core.restore import execute_restore
from app.deps import AdminUser, DBSession
from app.models.job import Job
from app.models.restore import RestoreHistory
from app.models.run import Run
from app.schemas.restore import RestoreOut, RestoreRequest, RestoreTargetsResponse
from app.utils.crypto import decrypt_dict
router = APIRouter(prefix="/restore", tags=["restore"])
@router.get("/targets", response_model=RestoreTargetsResponse)
def get_restore_targets(run_id: int, db: DBSession, _: AdminUser) -> RestoreTargetsResponse:
run = db.get(Run, run_id)
if not run:
raise HTTPException(status_code=404, detail="Run not found")
job = db.get(Job, run.job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
src = decrypt_dict(job.source_config_json)
suggested: list[str] = []
if job.type == "mysql":
suggested.append(src.get("database", ""))
elif job.type == "directory":
suggested.append(src.get("path", ""))
return RestoreTargetsResponse(
run_id=run.id,
job_type=job.type,
artifact_size=run.artifact_size,
artifact_checksum=run.artifact_checksum,
suggested_targets=[s for s in suggested if s],
)
@router.post("", response_model=RestoreOut, status_code=201)
async def trigger_restore(payload: RestoreRequest, db: DBSession, _: AdminUser) -> RestoreHistory:
run = db.get(Run, payload.run_id)
if not run or not run.artifact_path:
raise HTTPException(status_code=404, detail="Run not found or no artifact")
job = db.get(Job, run.job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
# 校验 target_type 与 job.type 匹配
if job.type == "mysql" and payload.target_type != "mysql_db":
raise HTTPException(status_code=400, detail="target_type 与源类型不匹配(应为 mysql_db)")
if job.type == "directory" and payload.target_type != "directory_path":
raise HTTPException(status_code=400, detail="target_type 与源类型不匹配(应为 directory_path")
rh = RestoreHistory(
run_id=run.id,
target=payload.target,
status="pending",
)
db.add(rh)
db.commit()
db.refresh(rh)
restore_id = rh.id
asyncio.create_task(execute_restore(restore_id))
return rh
@router.get("/history", response_model=list[RestoreOut])
def restore_history(db: DBSession, _: AdminUser, run_id: int | None = None) -> list[RestoreHistory]:
q = db.query(RestoreHistory)
if run_id is not None:
q = q.filter(RestoreHistory.run_id == run_id)
return q.order_by(RestoreHistory.id.desc()).limit(200).all()