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>
100 lines
2.8 KiB
Python
100 lines
2.8 KiB
Python
"""FastAPI 应用入口:lifespan 管理 scheduler、首次启动创建 admin。"""
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
from app.api.router import api_router
|
|
from app.config import get_settings
|
|
from app.database import Base, SessionLocal, engine
|
|
from app.models.user import User
|
|
from app.scheduler.scheduler import scheduler_lifespan
|
|
from app.security import hash_password
|
|
from app.utils.logging import setup_logging
|
|
|
|
setup_logging()
|
|
log = logging.getLogger(__name__)
|
|
settings = get_settings()
|
|
|
|
|
|
def init_database() -> None:
|
|
"""建表(Alembic 是首选;这里兜底确保首次启动也能跑)。"""
|
|
try:
|
|
# 实际生产用 alembic upgrade head;这里仅当表不存在时创建
|
|
Base.metadata.create_all(bind=engine)
|
|
log.info("Database tables ensured")
|
|
except OperationalError as e:
|
|
log.error("Database init failed: %s", e)
|
|
raise
|
|
|
|
|
|
def init_admin() -> None:
|
|
"""首次启动创建 admin 账号(仅当 users 表为空时)。"""
|
|
with SessionLocal() as db:
|
|
try:
|
|
existing = db.query(User).count()
|
|
if existing > 0:
|
|
log.info("Existing users found (%d), skip admin init", existing)
|
|
return
|
|
admin = User(
|
|
username="admin",
|
|
password_hash=hash_password(settings.INITIAL_ADMIN_PASSWORD),
|
|
is_admin=True,
|
|
is_active=True,
|
|
)
|
|
db.add(admin)
|
|
db.commit()
|
|
log.warning(
|
|
"Initial admin account created. username=admin "
|
|
"Please change the password after first login."
|
|
)
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
init_database()
|
|
init_admin()
|
|
async with scheduler_lifespan():
|
|
log.info("Application ready")
|
|
yield
|
|
log.info("Application shutting down")
|
|
|
|
|
|
app = FastAPI(
|
|
title="Backup System",
|
|
description="数据备份管理系统 - MySQL / 目录 备份到本地 / S3",
|
|
version="0.1.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origins_list,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
@app.get("/api/health")
|
|
def health() -> dict:
|
|
return {"status": "ok"}
|
|
|
|
|
|
app.include_router(api_router, prefix="/api")
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
async def global_exception_handler(request, exc):
|
|
log.exception("Unhandled error on %s %s", request.method, request.url)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"detail": f"Internal server error: {type(exc).__name__}"},
|
|
)
|