feat: add documents router for list/get/save/delete/upload
This commit is contained in:
parent
6a286d422d
commit
5b4bc4e719
29
backend/app/dependencies.py
Normal file
29
backend/app/dependencies.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
import meilisearch
|
||||||
|
from minio import Minio
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.document_service import DocumentService
|
||||||
|
from app.minio_client import MinioClient
|
||||||
|
from app.search_client import SearchClient
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_document_service() -> DocumentService:
|
||||||
|
minio_sdk = Minio(
|
||||||
|
settings.minio_endpoint,
|
||||||
|
access_key=settings.minio_access_key,
|
||||||
|
secret_key=settings.minio_secret_key,
|
||||||
|
secure=False,
|
||||||
|
)
|
||||||
|
meili_sdk = meilisearch.Client(settings.meilisearch_host, settings.meilisearch_api_key)
|
||||||
|
minio_client = MinioClient(minio_sdk, settings.minio_bucket)
|
||||||
|
search_client = SearchClient(meili_sdk, settings.meilisearch_index)
|
||||||
|
return DocumentService(minio_client, search_client)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_search_client() -> SearchClient:
|
||||||
|
meili_sdk = meilisearch.Client(settings.meilisearch_host, settings.meilisearch_api_key)
|
||||||
|
return SearchClient(meili_sdk, settings.meilisearch_index)
|
||||||
0
backend/app/routers/__init__.py
Normal file
0
backend/app/routers/__init__.py
Normal file
50
backend/app/routers/documents.py
Normal file
50
backend/app/routers/documents.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/documents/{key:path}")
|
||||||
|
def save_document(
|
||||||
|
key: str, body: SaveDocumentRequest, service: DocumentService = Depends(get_document_service)
|
||||||
|
):
|
||||||
|
return service.save_document(key, body.content.encode("utf-8"), body.contentType)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/documents/{key:path}", status_code=204)
|
||||||
|
def delete_document(key: str, service: DocumentService = Depends(get_document_service)):
|
||||||
|
service.delete_document(key)
|
||||||
|
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"
|
||||||
|
return service.upload_document(key, data, content_type)
|
||||||
97
backend/tests/test_routers_documents.py
Normal file
97
backend/tests/test_routers_documents.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
from app.dependencies import get_document_service
|
||||||
|
from app.document_service import DocumentNotFoundError
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDocumentService:
|
||||||
|
def __init__(self):
|
||||||
|
self.saved = []
|
||||||
|
self.deleted = []
|
||||||
|
self.uploaded = []
|
||||||
|
|
||||||
|
def list_documents(self, path):
|
||||||
|
return {
|
||||||
|
"folders": ["产品文档", "团队规范"],
|
||||||
|
"files": [
|
||||||
|
{"key": "架构设计.md", "name": "架构设计.md", "size": 512, "modifiedAt": "2026-08-01T10:00:00Z"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_document(self, key):
|
||||||
|
if key == "不存在/文档.md":
|
||||||
|
raise DocumentNotFoundError(key)
|
||||||
|
return {"key": key, "content": "# 架构设计\n\n内容", "contentType": "text/markdown"}
|
||||||
|
|
||||||
|
def save_document(self, key, content, content_type):
|
||||||
|
self.saved.append((key, content, content_type))
|
||||||
|
return {"key": key, "updatedAt": "2026-08-01T10:05:00Z"}
|
||||||
|
|
||||||
|
def delete_document(self, key):
|
||||||
|
self.deleted.append(key)
|
||||||
|
|
||||||
|
def upload_document(self, key, data, content_type):
|
||||||
|
self.uploaded.append((key, data, content_type))
|
||||||
|
return {"key": key, "name": key.rsplit("/", 1)[-1], "size": len(data)}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
fake_service = FakeDocumentService()
|
||||||
|
app.dependency_overrides[get_document_service] = lambda: fake_service
|
||||||
|
with TestClient(app) as test_client:
|
||||||
|
test_client.fake_service = fake_service
|
||||||
|
yield test_client
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_documents_returns_folders_and_files(client):
|
||||||
|
response = client.get("/api/documents", params={"path": "产品文档"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
assert body["folders"] == ["产品文档", "团队规范"]
|
||||||
|
assert body["files"][0]["key"] == "架构设计.md"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_document_returns_content(client):
|
||||||
|
response = client.get("/api/documents/产品文档/架构设计.md")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["contentType"] == "text/markdown"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_document_returns_404_for_missing_key(client):
|
||||||
|
response = client.get("/api/documents/不存在/文档.md")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_document_saves_content(client):
|
||||||
|
response = client.put(
|
||||||
|
"/api/documents/产品文档/架构设计.md",
|
||||||
|
json={"content": "# 更新后的内容", "contentType": "text/markdown"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["key"] == "产品文档/架构设计.md"
|
||||||
|
assert client.fake_service.saved[0][0] == "产品文档/架构设计.md"
|
||||||
|
assert client.fake_service.saved[0][1] == "# 更新后的内容".encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_document_returns_204(client):
|
||||||
|
response = client.delete("/api/documents/产品文档/架构设计.md")
|
||||||
|
assert response.status_code == 204
|
||||||
|
assert response.content == b""
|
||||||
|
assert client.fake_service.deleted == ["产品文档/架构设计.md"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_document_returns_201_with_metadata(client):
|
||||||
|
response = client.post(
|
||||||
|
"/api/upload",
|
||||||
|
data={"path": "产品文档"},
|
||||||
|
files={"file": ("新文档.md", b"# new content", "text/markdown")},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
body = response.json()
|
||||||
|
assert body["key"] == "产品文档/新文档.md"
|
||||||
|
assert body["name"] == "新文档.md"
|
||||||
|
assert body["size"] == len(b"# new content")
|
||||||
Loading…
Reference in New Issue
Block a user