SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
4.9 KB · 115 lines python
Raw Blame History
1"""Uploads, downloads (signed or authenticated), manual Python re-run."""23from __future__ import annotations45from fastapi import (6    APIRouter,7    Depends,8    File,9    Form,10    HTTPException,11    Query,12    Request,13    UploadFile,14    status,15)16from fastapi.responses import Response17from pydantic import BaseModel, Field1819from app.core.config import Settings, get_settings20from app.core.ratelimit import MSG_UPLOAD, limiter21from app.core.security import AuthUser, get_current_user, get_optional_user, hash_user_id22from app.services import files as svc23from app.tools import execute_python24from app.tools.registry import ToolContext2526router = APIRouter(tags=["files"])272829@router.post("/files", status_code=201)30async def upload(file: UploadFile = File(...), conversation_id: str | None = Form(None),31                 auth: AuthUser = Depends(get_current_user),32                 settings: Settings = Depends(get_settings)) -> dict:33    limiter.check(f"upload:{auth.id}", settings.RATE_UPLOADS_PER_DAY, 86400, MSG_UPLOAD)34    data = await file.read()35    try:36        rec = await svc.store_upload(auth.id, conversation_id, file.filename or "fichier", data)37    except ValueError as exc:38        raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc39    return {"file_id": rec.id, "filename": rec.filename, "type": rec.type, "size": rec.size_bytes,40            "url": f"/api/v1/files/{rec.id}"}414243@router.get("/files/{file_id}")44async def download(file_id: str, request: Request, sig: str | None = Query(None),45                   download: bool = False,46                   auth: AuthUser | None = Depends(get_optional_user)) -> Response:47    rec = await svc.get_file(file_id)48    if not rec:49        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Fichier introuvable.")50    allowed = (auth is not None and (auth.id == rec.user_id or auth.is_professor)) or \51              (sig is not None and svc.verify_signature(file_id, sig))52    if not allowed:53        raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Accès refusé.")54    try:55        data = svc.read_bytes(rec)56    except FileNotFoundError as exc:57        raise HTTPException(status.HTTP_410_GONE, detail="Fichier expiré.") from exc58    disp = "attachment" if download or rec.type in {"xlsx", "file", "csv", "docx"} else "inline"59    from urllib.parse import quote6061    headers = {"Content-Disposition": f"{disp}; filename*=UTF-8''{quote(rec.filename)}",62               "Cache-Control": "private, max-age=600"}63    return Response(content=data, media_type=rec.mime, headers=headers)646566@router.get("/files/{file_id}/link")67async def signed_link(file_id: str, auth: AuthUser = Depends(get_current_user)) -> dict:68    rec = await svc.get_file(file_id)69    if not rec or (rec.user_id != auth.id and not auth.is_professor):70        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Fichier introuvable.")71    return {"url": f"/api/v1/files/{file_id}?sig={svc.sign(file_id)}&download=1",72            "expires_in": 600}737475class PinReq(BaseModel):76    pinned: bool = True777879@router.post("/files/{file_id}/pin")80async def pin(file_id: str, req: PinReq, auth: AuthUser = Depends(get_current_user)) -> dict:81    if not await svc.set_pinned(file_id, auth.id, req.pinned):82        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Fichier introuvable.")83    return {"ok": True}848586@router.get("/conversations/{conv_id}/files")87async def conversation_files(conv_id: str, auth: AuthUser = Depends(get_current_user)) -> list[dict]:88    rows = await svc.list_for_conversation(conv_id)89    return [{"file_id": r.id, "filename": r.filename, "type": r.type, "kind": r.kind,90             "size": r.size_bytes, "created_at": r.created_at.isoformat(), "pinned": r.pinned,91             "url": f"/api/v1/files/{r.id}"} for r in rows if r.user_id == auth.id]929394class RunReq(BaseModel):95    code: str = Field(..., max_length=100_000)96    conversation_id: str | None = None979899@router.post("/tools/python/run")100async def run_python(req: RunReq, auth: AuthUser = Depends(get_current_user),101                     settings: Settings = Depends(get_settings)) -> dict:102    """Manual re-run of a code block from the UI (same sandbox, same limits)."""103    file_ids: list[str] = []104    if req.conversation_id:105        recs = await svc.list_for_conversation(req.conversation_id)106        file_ids = [r.id for r in recs if r.kind == "upload" and r.user_id == auth.id]107    ctx = ToolContext(user_id=auth.id, user_id_hash=hash_user_id(auth.id),108                      conversation_id=req.conversation_id or "", course_code="",109                      settings=settings, role=auth.role, file_ids=file_ids)110    result = await execute_python.run({"code": req.code, "description": "Ré-exécution",111                                       "heavy": False}, ctx)112    if result.error and not result.payload:113        raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=result.content)114    return result.payload115