DocHub/backend/app/routers/documents.py
Tianyang 9ef22f9f0a Fix wave 2: CORS default, path traversal, delete UI, shared clients, bucket check
- Default CORS_ORIGINS to http://localhost:5173 instead of "*" (still overridable)
- Reject ".." and leading "/" in document keys/paths across get/save/delete/upload,
  mapped to HTTP 400 via new InvalidPathError
- Add a delete button to DocumentEditor wired to deleteDocument + onDeleted,
  giving the existing DELETE API an actual UI entry point
- Share a single lru_cache'd Meilisearch (and Minio) SDK client instance instead
  of constructing duplicates in dependencies.py
- Add a startup check that creates the MinIO bucket if missing, tolerating
  MinIO being unreachable at boot without crashing the app
2026-08-01 09:15:16 +08:00

68 lines
2.2 KiB
Python

from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from pydantic import BaseModel
from app.dependencies import get_document_service
from app.document_service import (
DocumentNotFoundError,
DocumentService,
FileTooLargeError,
InvalidPathError,
UnsupportedFileTypeError,
)
router = APIRouter()
class SaveDocumentRequest(BaseModel):
content: str
contentType: str
@router.get("/api/documents")
def list_documents(path: str = "", service: DocumentService = Depends(get_document_service)):
return service.list_documents(path)
@router.get("/api/documents/{key:path}")
def get_document(key: str, service: DocumentService = Depends(get_document_service)):
try:
return service.get_document(key)
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail="Document not found")
except InvalidPathError as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.put("/api/documents/{key:path}")
def save_document(
key: str, body: SaveDocumentRequest, service: DocumentService = Depends(get_document_service)
):
try:
return service.save_document(key, body.content.encode("utf-8"), body.contentType)
except InvalidPathError as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.delete("/api/documents/{key:path}", status_code=204)
def delete_document(key: str, service: DocumentService = Depends(get_document_service)):
try:
service.delete_document(key)
except InvalidPathError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return None
@router.post("/api/upload", status_code=201)
async def upload_document(
file: UploadFile = File(...),
path: str = Form(...),
service: DocumentService = Depends(get_document_service),
):
data = await file.read()
key = f"{path.rstrip('/')}/{file.filename}" if path else file.filename
content_type = file.content_type or "application/octet-stream"
try:
return service.upload_document(key, data, content_type)
except (UnsupportedFileTypeError, FileTooLargeError, InvalidPathError) as exc:
raise HTTPException(status_code=400, detail=str(exc))