feat: storage 改按日期分组,加 FastAPI 代理路由
- 上传落到 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 <noreply@anthropic.com>
This commit is contained in:
parent
83b3fac62c
commit
7214649759
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue