feat: 拆分新旧前端访问端口
This commit is contained in:
+110
-37
@@ -1,10 +1,11 @@
|
||||
from pathlib import Path
|
||||
import socket
|
||||
from typing import Literal
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app import state
|
||||
from app.api.routers import auth, cache, config, database, health, history, script, service, tasks, upload
|
||||
@@ -13,12 +14,15 @@ from app.config import BASE_DIR
|
||||
|
||||
|
||||
APP_VERSION = "2.0.2"
|
||||
APP_HOST = "0.0.0.0"
|
||||
NEW_FRONTEND_PORT = 9081
|
||||
OLD_FRONTEND_PORT = 9082
|
||||
FRONTEND_DIST_DIR = BASE_DIR / "frontend" / "dist"
|
||||
FRONTEND_ASSETS_DIR = FRONTEND_DIST_DIR / "assets"
|
||||
FRONTEND_OLD_DIR = BASE_DIR / "frontend_old"
|
||||
FrontendMode = Literal["new", "old", "split"]
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
def create_app(frontend_mode: FrontendMode = "new") -> FastAPI:
|
||||
app = FastAPI(
|
||||
title="CapacityReport",
|
||||
description="容量报表数据处理系统",
|
||||
@@ -38,7 +42,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
app.middleware("http")(jwt_middleware)
|
||||
register_routes(app)
|
||||
register_frontend(app)
|
||||
register_frontend(app, frontend_mode)
|
||||
return app
|
||||
|
||||
|
||||
@@ -73,61 +77,130 @@ def register_routes(app: FastAPI) -> None:
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
def register_frontend(app: FastAPI) -> None:
|
||||
def register_frontend(app: FastAPI, frontend_mode: FrontendMode) -> None:
|
||||
@app.get("/old", include_in_schema=False)
|
||||
async def serve_old_frontend():
|
||||
index_file = FRONTEND_OLD_DIR / "index.html"
|
||||
if index_file.exists():
|
||||
return FileResponse(str(index_file))
|
||||
return JSONResponse(status_code=503, content={"detail": "旧版前端入口文件不存在"})
|
||||
async def serve_old_frontend(request: Request):
|
||||
if not _should_serve_old_frontend(request, frontend_mode):
|
||||
raise HTTPException(status_code=404, detail=f"旧版前端请访问 {OLD_FRONTEND_PORT} 端口")
|
||||
return _serve_old_index()
|
||||
|
||||
if FRONTEND_OLD_DIR.exists():
|
||||
app.mount("/old", StaticFiles(directory=str(FRONTEND_OLD_DIR), html=True), name="frontend_old")
|
||||
|
||||
if FRONTEND_ASSETS_DIR.exists():
|
||||
app.mount("/assets", StaticFiles(directory=str(FRONTEND_ASSETS_DIR)), name="assets")
|
||||
@app.get("/old/{path:path}", include_in_schema=False)
|
||||
async def serve_old_frontend_file(request: Request, path: str):
|
||||
if not _should_serve_old_frontend(request, frontend_mode):
|
||||
raise HTTPException(status_code=404, detail=f"旧版前端请访问 {OLD_FRONTEND_PORT} 端口")
|
||||
return _serve_old_file(path)
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
@app.get("/{path:path}", include_in_schema=False)
|
||||
async def serve_spa(path: str = ""):
|
||||
async def serve_frontend(request: Request, path: str = ""):
|
||||
if path.startswith("api/"):
|
||||
raise HTTPException(status_code=404, detail="接口不存在")
|
||||
|
||||
if not FRONTEND_DIST_DIR.exists():
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={
|
||||
"detail": "前端构建产物不存在,请先执行 cd frontend && npm install && npm run build"
|
||||
},
|
||||
)
|
||||
if _should_serve_old_frontend(request, frontend_mode):
|
||||
return _serve_old_frontend_path(path)
|
||||
|
||||
requested_file = _safe_frontend_file(path)
|
||||
if requested_file and requested_file.exists() and requested_file.is_file():
|
||||
return FileResponse(str(requested_file))
|
||||
|
||||
index_file = FRONTEND_DIST_DIR / "index.html"
|
||||
if index_file.exists():
|
||||
return FileResponse(str(index_file))
|
||||
|
||||
return JSONResponse(status_code=503, content={"detail": "前端入口文件不存在"})
|
||||
return _serve_new_frontend_path(path)
|
||||
|
||||
|
||||
def _safe_frontend_file(path: str) -> Path | None:
|
||||
def _should_serve_old_frontend(request: Request, frontend_mode: FrontendMode) -> bool:
|
||||
if frontend_mode == "old":
|
||||
return True
|
||||
if frontend_mode == "new":
|
||||
return False
|
||||
return _request_port(request) == OLD_FRONTEND_PORT
|
||||
|
||||
|
||||
def _request_port(request: Request) -> int | None:
|
||||
server = request.scope.get("server")
|
||||
if isinstance(server, tuple) and len(server) >= 2:
|
||||
return server[1]
|
||||
return request.url.port
|
||||
|
||||
|
||||
def _serve_new_frontend_path(path: str):
|
||||
if not FRONTEND_DIST_DIR.exists():
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={"detail": "前端构建产物不存在,请先执行 cd frontend && npm install && npm run build"},
|
||||
)
|
||||
|
||||
requested_file = _safe_file(FRONTEND_DIST_DIR, path)
|
||||
if requested_file and requested_file.exists() and requested_file.is_file():
|
||||
return FileResponse(str(requested_file))
|
||||
|
||||
index_file = FRONTEND_DIST_DIR / "index.html"
|
||||
if index_file.exists():
|
||||
return FileResponse(str(index_file))
|
||||
|
||||
return JSONResponse(status_code=503, content={"detail": "前端入口文件不存在"})
|
||||
|
||||
|
||||
def _serve_old_frontend_path(path: str):
|
||||
if not path:
|
||||
return _serve_old_index()
|
||||
|
||||
requested_file = _safe_file(FRONTEND_OLD_DIR, path)
|
||||
if requested_file and requested_file.exists() and requested_file.is_file():
|
||||
return FileResponse(str(requested_file))
|
||||
|
||||
return _serve_old_index()
|
||||
|
||||
|
||||
def _serve_old_file(path: str):
|
||||
requested_file = _safe_file(FRONTEND_OLD_DIR, path)
|
||||
if requested_file and requested_file.exists() and requested_file.is_file():
|
||||
return FileResponse(str(requested_file))
|
||||
raise HTTPException(status_code=404, detail="旧版前端文件不存在")
|
||||
|
||||
|
||||
def _serve_old_index():
|
||||
if not FRONTEND_OLD_DIR.exists():
|
||||
return JSONResponse(status_code=503, content={"detail": "旧版前端目录不存在"})
|
||||
|
||||
index_file = FRONTEND_OLD_DIR / "index.html"
|
||||
if index_file.exists():
|
||||
return FileResponse(str(index_file))
|
||||
|
||||
return JSONResponse(status_code=503, content={"detail": "旧版前端入口文件不存在"})
|
||||
|
||||
|
||||
def _safe_file(root: Path, path: str) -> Path | None:
|
||||
if not path:
|
||||
return None
|
||||
|
||||
requested = (FRONTEND_DIST_DIR / path).resolve()
|
||||
root = root.resolve()
|
||||
requested = (root / path).resolve()
|
||||
try:
|
||||
requested.relative_to(FRONTEND_DIST_DIR.resolve())
|
||||
requested.relative_to(root)
|
||||
except ValueError:
|
||||
return None
|
||||
return requested
|
||||
|
||||
|
||||
app = create_app()
|
||||
def _create_listen_socket(port: int) -> socket.socket:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((APP_HOST, port))
|
||||
sock.listen(2048)
|
||||
sock.set_inheritable(True)
|
||||
return sock
|
||||
|
||||
|
||||
def run_split_frontend_servers() -> None:
|
||||
sockets = [_create_listen_socket(NEW_FRONTEND_PORT), _create_listen_socket(OLD_FRONTEND_PORT)]
|
||||
config = uvicorn.Config(split_app, host=APP_HOST, port=NEW_FRONTEND_PORT, reload=False)
|
||||
server = uvicorn.Server(config)
|
||||
server.run(sockets=sockets)
|
||||
|
||||
|
||||
app = create_app("new")
|
||||
old_app = create_app("old")
|
||||
split_app = create_app("split")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"CapacityReport v{APP_VERSION}")
|
||||
print(f"配置更新时间: {state.config.update}")
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=9081, reload=False)
|
||||
print(f"新版前端: http://localhost:{NEW_FRONTEND_PORT}")
|
||||
print(f"旧版前端: http://localhost:{OLD_FRONTEND_PORT}")
|
||||
run_split_frontend_servers()
|
||||
|
||||
Reference in New Issue
Block a user