From 721464975967aa70de776b2931d7bb6ae098464d Mon Sep 17 00:00:00 2001 From: shijing Date: Tue, 14 Jul 2026 09:51:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20storage=20=E6=94=B9=E6=8C=89=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E5=88=86=E7=BB=84=EF=BC=8C=E5=8A=A0=20FastAPI=20?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 上传落到 storage/YYYY/MM/DD/{uuid}.ext,storage_key 记录相对路径 - 新增 GET /api/files/{key:path} 读文件,防目录穿越;旧的扁平 key 也兼容 - temporary_url 返回 /api/files/{key},前端可直接用 - storage 目录已在 .gitignore(backend/storage/),无需再改 Co-Authored-By: Claude Opus 4.7 --- backend/app/api/__init__.py | 3 ++- backend/app/api/files.py | 17 ++++++++++++++ backend/app/integrations/storage_client.py | 27 ++++++++++++++++------ 3 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 backend/app/api/files.py diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index a1e5471..d351a8e 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api import audit, auth, chat, documents, permissions, roles, stats, units, users +from app.api import audit, auth, chat, documents, files, permissions, roles, stats, units, users api_router = APIRouter(prefix="/api") api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -12,3 +12,4 @@ api_router.include_router(chat.router, prefix="/chat", tags=["chat"]) api_router.include_router(documents.router, prefix="/documents", tags=["documents"]) api_router.include_router(audit.router, prefix="/audit-logs", tags=["audit"]) api_router.include_router(stats.router, prefix="/stats", tags=["stats"]) +api_router.include_router(files.router, prefix="/files", tags=["files"]) diff --git a/backend/app/api/files.py b/backend/app/api/files.py new file mode 100644 index 0000000..be5face --- /dev/null +++ b/backend/app/api/files.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse + +from app.integrations.storage_client import StorageClient + +router = APIRouter() + + +@router.get("/{key:path}") +def get_file(key: str) -> FileResponse: + try: + path = StorageClient().resolve(key) + except ValueError: + raise HTTPException(status_code=400, detail="非法路径") + if not path.is_file(): + raise HTTPException(status_code=404, detail="文件不存在") + return FileResponse(path) diff --git a/backend/app/integrations/storage_client.py b/backend/app/integrations/storage_client.py index f3ebbec..0f88a38 100644 --- a/backend/app/integrations/storage_client.py +++ b/backend/app/integrations/storage_client.py @@ -1,3 +1,4 @@ +from datetime import datetime from pathlib import Path from uuid import uuid4 @@ -11,12 +12,24 @@ class StorageClient: return self.save_bytes(file.file.read(), suffix=Path(file.filename or "upload.bin").suffix) def save_bytes(self, raw: bytes, suffix: str = "") -> str: - settings = get_settings() - storage_dir = Path(settings.local_storage_dir) - storage_dir.mkdir(parents=True, exist_ok=True) - key = f"{uuid4().hex}{suffix}" - (storage_dir / key).write_bytes(raw) - return key + base = self._base_dir() + date_seg = datetime.now().strftime("%Y/%m/%d") + target_dir = base / date_seg + target_dir.mkdir(parents=True, exist_ok=True) + filename = f"{uuid4().hex}{suffix}" + (target_dir / filename).write_bytes(raw) + return f"{date_seg}/{filename}" def temporary_url(self, key: str) -> str: - return f"local://{key}" + return f"/api/files/{key}" + + def resolve(self, key: str) -> Path: + base = self._base_dir().resolve() + target = (base / key).resolve() + if base != target and base not in target.parents: + raise ValueError("非法存储路径") + return target + + @staticmethod + def _base_dir() -> Path: + return Path(get_settings().local_storage_dir)