Nicolas Cantu 088eab84b7 Platform docs, services, ia_dev submodule, smart_ide project config
- Add ia_dev submodule (projects/smart_ide on forge 4nk)
- Document APIs, orchestrator, gateway, local-office, rollout
- Add systemd/scripts layout; relocate setup scripts
- Remove obsolete nginx/enso-only docs from this repo scope
2026-04-03 16:07:58 +02:00

157 lines
5.1 KiB
Python

"""Document routes: upload, get file, list, delete, commands. Auth and rate limit on each."""
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse
from app.api.schemas import CommandsRequest, commands_to_dicts
from app.auth import require_api_key
from app.config import get_max_upload_bytes
from app.engine.commands import apply_commands
from app.limiter import limiter, rate_limit_string
from app.storage import file_storage
from app.storage.file_storage import delete_document_file, read_document, write_document
from app.storage.metadata import (
delete_document_metadata,
generate_document_id,
get_document,
insert_document,
list_documents,
update_document_size,
)
logger = logging.getLogger(__name__)
router = APIRouter()
rate = rate_limit_string()
ALLOWED_MIME = {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
}
def _check_owner(meta: dict | None, api_key_id: str, document_id: str) -> None:
if meta is None:
raise HTTPException(status_code=404, detail="Document not found")
if meta.get("api_key_id") != api_key_id:
raise HTTPException(status_code=404, detail="Document not found")
@router.post("", status_code=201, response_model=dict)
@limiter.limit(rate)
async def upload_document(
request: Request,
api_key_id: Annotated[str, Depends(require_api_key)],
file: Annotated[UploadFile, File()],
) -> dict:
"""Upload an Office file. Returns document_id."""
content = await file.read()
if len(content) > get_max_upload_bytes():
raise HTTPException(
status_code=413,
detail="File too large",
)
mime = (file.content_type or "").strip().split(";")[0]
if mime not in ALLOWED_MIME:
raise HTTPException(
status_code=400,
detail=f"Unsupported file type. Allowed: {sorted(ALLOWED_MIME)}",
)
name = file.filename or "document"
document_id = generate_document_id()
write_document(document_id, content)
insert_document(
document_id=document_id,
api_key_id=api_key_id,
name=name,
mime_type=mime,
size=len(content),
)
logger.info("Uploaded document %s for key %s", document_id, api_key_id)
return {"document_id": document_id, "name": name, "mime_type": mime, "size": len(content)}
@router.get("")
@limiter.limit(rate)
async def list_docs(
request: Request,
api_key_id: Annotated[str, Depends(require_api_key)],
) -> list:
"""List documents for the authenticated API key."""
return list_documents(api_key_id)
@router.get("/{document_id}")
@limiter.limit(rate)
async def get_metadata(
request: Request,
document_id: str,
api_key_id: Annotated[str, Depends(require_api_key)],
) -> dict:
"""Get document metadata."""
meta = get_document(document_id)
_check_owner(meta, api_key_id, document_id)
return meta
@router.get("/{document_id}/file")
@limiter.limit(rate)
async def download_file(
request: Request,
document_id: str,
api_key_id: Annotated[str, Depends(require_api_key)],
):
"""Download document file."""
meta = get_document(document_id)
_check_owner(meta, api_key_id, document_id)
path = file_storage.get_document_path(document_id)
if not path.is_file():
raise HTTPException(status_code=404, detail="Document file not found")
return FileResponse(
path,
media_type=meta.get("mime_type", "application/octet-stream"),
filename=meta.get("name", "document"),
)
@router.post("/{document_id}/commands")
@limiter.limit(rate)
async def apply_document_commands(
request: Request,
document_id: str,
body: CommandsRequest,
api_key_id: Annotated[str, Depends(require_api_key)],
) -> dict:
"""Apply commands to document. Supported for docx: replaceText, insertParagraph."""
meta = get_document(document_id)
_check_owner(meta, api_key_id, document_id)
content = read_document(document_id)
mime = meta.get("mime_type", "")
try:
new_content = apply_commands(content, mime, commands_to_dicts(body.commands))
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
write_document(document_id, new_content)
update_document_size(document_id, len(new_content))
logger.info("Applied %d commands to document %s", len(body.commands), document_id)
return {"document_id": document_id, "size": len(new_content)}
@router.delete("/{document_id}", status_code=204)
@limiter.limit(rate)
async def delete_doc(
request: Request,
document_id: str,
api_key_id: Annotated[str, Depends(require_api_key)],
) -> None:
"""Delete document and its file."""
meta = get_document(document_id)
_check_owner(meta, api_key_id, document_id)
deleted = delete_document_metadata(document_id)
delete_document_file(document_id)
if not deleted:
raise HTTPException(status_code=404, detail="Document not found")