Python 64.6%
TypeScript 33.7%
CSS 0.8%
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