|
6 | 6 | import time |
7 | 7 | from contextlib import asynccontextmanager |
8 | 8 |
|
9 | | -from fastapi import FastAPI |
| 9 | +from fastapi import FastAPI, HTTPException |
10 | 10 | from fastapi.middleware.cors import CORSMiddleware |
11 | | -from fastapi.responses import HTMLResponse |
12 | | -from fastapi.staticfiles import StaticFiles |
| 11 | +from fastapi.responses import FileResponse, HTMLResponse |
13 | 12 | from tortoise import Tortoise |
14 | 13 | from tortoise.contrib.fastapi import register_tortoise |
15 | 14 |
|
@@ -77,12 +76,6 @@ async def lifespan(app: FastAPI): |
77 | 76 | # 加载配置(多进程下串行化启动写操作) |
78 | 77 | async with db_startup_lock(): |
79 | 78 | await load_config() |
80 | | - app.mount( |
81 | | - "/assets", |
82 | | - StaticFiles(directory=f"./{settings.themesSelect}/assets"), |
83 | | - name="assets", |
84 | | - ) |
85 | | - |
86 | 79 | # 启动后台任务 |
87 | 80 | task = asyncio.create_task(delete_expire_files()) |
88 | 81 | chunk_cleanup_task = asyncio.create_task(clean_incomplete_uploads()) |
@@ -156,19 +149,46 @@ async def refresh_settings_middleware(request, call_next): |
156 | 149 | app.include_router(admin_api) |
157 | 150 |
|
158 | 151 |
|
| 152 | +def resolve_theme_root(): |
| 153 | + themes_root = (BASE_DIR / "themes").resolve() |
| 154 | + theme_root = (BASE_DIR / str(settings.themesSelect)).resolve() |
| 155 | + try: |
| 156 | + theme_root.relative_to(themes_root) |
| 157 | + except ValueError: |
| 158 | + theme_root = (BASE_DIR / DEFAULT_CONFIG["themesSelect"]).resolve() |
| 159 | + if not theme_root.exists(): |
| 160 | + theme_root = (BASE_DIR / DEFAULT_CONFIG["themesSelect"]).resolve() |
| 161 | + return theme_root |
| 162 | + |
| 163 | + |
| 164 | +def resolve_theme_file(*parts: str): |
| 165 | + theme_root = resolve_theme_root() |
| 166 | + file_path = theme_root.joinpath(*parts).resolve() |
| 167 | + # 防止通过 /assets/../ 读取主题目录外的文件。 |
| 168 | + try: |
| 169 | + file_path.relative_to(theme_root) |
| 170 | + except ValueError: |
| 171 | + raise HTTPException(status_code=404, detail="资源不存在") |
| 172 | + if not file_path.is_file(): |
| 173 | + raise HTTPException(status_code=404, detail="资源不存在") |
| 174 | + return file_path |
| 175 | + |
| 176 | + |
| 177 | +@app.get("/assets/{asset_path:path}", include_in_schema=False) |
| 178 | +async def theme_asset(asset_path: str): |
| 179 | + return FileResponse(resolve_theme_file("assets", asset_path)) |
| 180 | + |
| 181 | + |
159 | 182 | @app.exception_handler(404) |
160 | 183 | @app.get("/") |
161 | 184 | async def index(request=None, exc=None): |
162 | 185 | return HTMLResponse( |
163 | | - content=open( |
164 | | - BASE_DIR / f"{settings.themesSelect}/index.html", "r", encoding="utf-8" |
165 | | - ) |
166 | | - .read() |
| 186 | + content=resolve_theme_file("index.html") |
| 187 | + .read_text(encoding="utf-8") |
167 | 188 | .replace("{{title}}", str(settings.name)) |
168 | 189 | .replace("{{description}}", str(settings.description)) |
169 | 190 | .replace("{{keywords}}", str(settings.keywords)) |
170 | 191 | .replace("{{opacity}}", str(settings.opacity)) |
171 | | - .replace('"/assets/', '"assets/') |
172 | 192 | .replace("{{background}}", str(settings.background)), |
173 | 193 | media_type="text/html", |
174 | 194 | headers={"Cache-Control": "no-cache"}, |
|
0 commit comments