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)