diff --git a/.dockerignore b/.dockerignore index 9a41bcf..190b30e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ .venv/ .ruff_cache/ .vscode/ +storage/ replays/ diff --git a/.env.example b/.env.example index 158ccd7..94dd4a7 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,8 @@ ACCESS_TOKEN_EXPIRE_MINUTES=1440 # 服务器地址 HOST="0.0.0.0" PORT=8000 +# 服务器 URL +SERVER_URL="http://localhost:8000" # 调试模式,生产环境请设置为 false DEBUG=false # 私有 API 密钥,用于前后端 API 调用,使用 openssl rand -hex 32 生成 @@ -37,7 +39,6 @@ SIGNALR_PING_INTERVAL=15 FETCHER_CLIENT_ID="" FETCHER_CLIENT_SECRET="" FETCHER_SCOPES=public -FETCHER_CALLBACK_URL="http://localhost:8000/fetcher/callback" # 日志设置 LOG_LEVEL="INFO" diff --git a/.gitignore b/.gitignore index 05622b7..dbf8bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -183,9 +183,9 @@ cython_debug/ .abstra/ # Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, +# and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ @@ -210,5 +210,6 @@ bancho.py-master/* .vscode/settings.json # runtime file +storage/ replays/ -osu-master/* \ No newline at end of file +osu-master/* diff --git a/app/config.py b/app/config.py index 397cfe3..54486d7 100644 --- a/app/config.py +++ b/app/config.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import Enum from typing import Annotated, Any -from pydantic import Field, ValidationInfo, field_validator +from pydantic import Field, HttpUrl, ValidationInfo, field_validator from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict @@ -65,6 +65,7 @@ class Settings(BaseSettings): port: int = 8000 debug: bool = False private_api_secret: str = "your_private_api_secret_here" + server_url: HttpUrl = HttpUrl("http://localhost:8000") # SignalR 设置 signalr_negotiate_timeout: int = 30 @@ -74,7 +75,10 @@ class Settings(BaseSettings): fetcher_client_id: str = "" fetcher_client_secret: str = "" fetcher_scopes: Annotated[list[str], NoDecode] = ["public"] - fetcher_callback_url: str = "http://localhost:8000/fetcher/callback" + + @property + def fetcher_callback_url(self) -> str: + return f"{self.server_url}fetcher/callback" # 日志设置 log_level: str = "INFO" diff --git a/app/router/__init__.py b/app/router/__init__.py index a54de39..c5b5b79 100644 --- a/app/router/__init__.py +++ b/app/router/__init__.py @@ -4,6 +4,7 @@ from app.signalr import signalr_router as signalr_router from .auth import router as auth_router from .fetcher import fetcher_router as fetcher_router +from .file import file_router as file_router from .private import private_router as private_router from .v2.router import router as api_v2_router @@ -11,6 +12,7 @@ __all__ = [ "api_v2_router", "auth_router", "fetcher_router", + "file_router", "private_router", "signalr_router", ] diff --git a/app/router/file.py b/app/router/file.py new file mode 100644 index 0000000..0cb2732 --- /dev/null +++ b/app/router/file.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from app.dependencies.storage import get_storage_service +from app.storage import LocalStorageService, StorageService + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import FileResponse + +file_router = APIRouter(prefix="/file") + + +@file_router.get("/{path:path}") +async def get_file(path: str, storage: StorageService = Depends(get_storage_service)): + if not isinstance(storage, LocalStorageService): + raise HTTPException(404, "Not Found") + if not await storage.is_exists(path): + raise HTTPException(404, "Not Found") + + try: + return FileResponse( + path=storage._get_file_path(path), + media_type="application/octet-stream", + filename=path.split("/")[-1], + ) + except FileNotFoundError: + raise HTTPException(404, "Not Found") diff --git a/app/router/v2/__init__.py b/app/router/v2/__init__.py index 7e09509..7f981ee 100644 --- a/app/router/v2/__init__.py +++ b/app/router/v2/__init__.py @@ -1,7 +1,5 @@ from __future__ import annotations -from app.signalr import signalr_router as signalr_router - from . import ( # pyright: ignore[reportUnusedImport] # noqa: F401 beatmap, beatmapset, diff --git a/app/storage/local.py b/app/storage/local.py index 255077b..b60eb35 100644 --- a/app/storage/local.py +++ b/app/storage/local.py @@ -2,6 +2,8 @@ from __future__ import annotations from pathlib import Path +from app.config import settings + from .base import StorageService import aiofiles @@ -75,4 +77,4 @@ class LocalStorageService(StorageService): return full_path.exists() and full_path.is_file() async def get_file_url(self, file_path: str) -> str: - return str(self.storage_path / file_path) + return f"{settings.server_url}file/{file_path.lstrip('/')}" diff --git a/main.py b/main.py index 2081aa0..8d14f94 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ from app.router import ( api_v2_router, auth_router, fetcher_router, + file_router, private_router, signalr_router, ) @@ -38,19 +39,20 @@ async def lifespan(app: FastAPI): app = FastAPI(title="osu! API 模拟服务器", version="1.0.0", lifespan=lifespan) +app.include_router(api_v2_router) +app.include_router(signalr_router) +app.include_router(fetcher_router) +app.include_router(file_router) +app.include_router(auth_router) +app.include_router(private_router) # CORS 配置 app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:5173"], + allow_origins=[str(settings.server_url)], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -app.include_router(api_v2_router) -app.include_router(signalr_router) -app.include_router(fetcher_router) -app.include_router(auth_router) -app.include_router(private_router) @app.get("/")