4e3a4b4602
带 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>
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
"""恢复 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()
|