From 2de3c8514c2bf3700be7ab9d55cb79f1b68b9d60 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Jul 2026 17:21:46 +0000 Subject: [PATCH] no-mistakes(review): Fix orphaned photo files and add upload content-type validation --- app/routers/tickets.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/app/routers/tickets.py b/app/routers/tickets.py index 76c8b4e..9edccd4 100644 --- a/app/routers/tickets.py +++ b/app/routers/tickets.py @@ -224,6 +224,10 @@ async def check_sla( # ── Photo Uploads ──────────────────────────────────────────────────── +ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} +ALLOWED_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} + + @router.post("/{ticket_id}/photos", response_model=list[TicketPhotoOut], status_code=status.HTTP_201_CREATED) async def upload_photos( ticket_id: int, @@ -236,18 +240,32 @@ async def upload_photos( # Verify ticket exists await ticket_service.get_ticket(db, ticket_id) + # Validate all files before any write operations + for file in files: + if file.content_type not in ALLOWED_MIME_TYPES: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unsupported content type '{file.content_type}'. Allowed: image/jpeg, image/png, image/gif, image/webp", + ) + ext = Path(file.filename or "photo.jpg").suffix.lower() if file.filename else ".jpg" + if ext not in ALLOWED_EXTENSIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unsupported file extension '{ext}'. Allowed: .jpg, .jpeg, .png, .gif, .webp", + ) + created_photos: list[TicketPhoto] = [] + file_data: list[tuple[Path, bytes]] = [] for file in files: # Generate unique filename - ext = Path(file.filename or "photo.jpg").suffix if file.filename else ".jpg" + ext = Path(file.filename or "photo.jpg").suffix.lower() if file.filename else ".jpg" unique_name = f"{uuid.uuid4().hex}{ext}" file_path = UPLOADS_DIR / unique_name - # Write file content = await file.read() - file_path.write_bytes(content) + file_data.append((file_path, content)) - # Database record + # Database record (no file write yet) photo = TicketPhoto( ticket_id=ticket_id, photo_url=f"/uploads/{unique_name}", @@ -259,6 +277,11 @@ async def upload_photos( await db.flush() for p in created_photos: await db.refresh(p) + + # Write files to disk only after successful DB flush + for file_path, content in file_data: + file_path.write_bytes(content) + return created_photos