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>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""恢复 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()
|
||||
Reference in New Issue
Block a user