Spaces:
Runtime error
Runtime error
| """ | |
| Uploads API Routes | |
| ================== | |
| Presigned S3 uploads for public form submissions. | |
| """ | |
| import os | |
| from datetime import datetime | |
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from sqlalchemy.orm import Session | |
| from ..core.db import get_db | |
| from ..core.security import get_current_user | |
| from ..models import Form, FormQuestion, FormUpload, PublicForm, QuestionType, User | |
| from ..schemas.uploads import ( | |
| UploadCreateRequest, | |
| UploadCreateResponse, | |
| UploadCompleteRequest, | |
| UploadCompleteResponse | |
| ) | |
| from ..services.s3_service import s3_service | |
| router = APIRouter(prefix="/api", tags=["uploads"]) | |
| async def create_public_upload( | |
| token: str, | |
| payload: UploadCreateRequest, | |
| db: Session = Depends(get_db) | |
| ): | |
| """Create a presigned upload URL for a public form submission.""" | |
| public_form = db.query(PublicForm).filter( | |
| PublicForm.share_token == token, | |
| PublicForm.is_public == True | |
| ).first() | |
| if not public_form: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Form not found or no longer accepting responses" | |
| ) | |
| if public_form.expires_at and public_form.expires_at < datetime.utcnow(): | |
| raise HTTPException( | |
| status_code=status.HTTP_410_GONE, | |
| detail="This form has expired" | |
| ) | |
| form = db.query(Form).filter(Form.id == public_form.form_id).first() | |
| if not form: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Form not found" | |
| ) | |
| question = db.query(FormQuestion).filter( | |
| FormQuestion.id == payload.question_id, | |
| FormQuestion.form_id == form.id | |
| ).first() | |
| if not question: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Question not found" | |
| ) | |
| if question.question_type != QuestionType.FILE_UPLOAD: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Question does not accept file uploads" | |
| ) | |
| settings = question.settings or {} | |
| max_size = settings.get("max_file_size") | |
| if max_size and payload.size_bytes and payload.size_bytes > max_size: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="File exceeds maximum allowed size" | |
| ) | |
| allowed_types = settings.get("file_types") or [] | |
| if allowed_types: | |
| ext = os.path.splitext(payload.filename)[1].lower() | |
| allowed = {t.lower() for t in allowed_types} | |
| if ext and ext not in allowed: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="File type not allowed" | |
| ) | |
| upload = FormUpload( | |
| user_id=public_form.user_id, | |
| form_id=form.id, | |
| form_question_id=question.id, | |
| original_filename=payload.filename, | |
| content_type=payload.content_type, | |
| size_bytes=payload.size_bytes, | |
| status="pending", | |
| s3_key="" | |
| ) | |
| db.add(upload) | |
| db.flush() | |
| s3_key = s3_service.build_key(form.id, form.title, upload.id, payload.filename) | |
| upload.s3_key = s3_key | |
| db.commit() | |
| try: | |
| upload_url = s3_service.generate_presigned_upload_url( | |
| key=s3_key, | |
| content_type=payload.content_type | |
| ) | |
| except Exception: | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="S3 upload is not configured" | |
| ) | |
| return UploadCreateResponse( | |
| upload_id=upload.id, | |
| upload_url=upload_url, | |
| s3_key=s3_key, | |
| expires_in=900, | |
| original_filename=payload.filename | |
| ) | |
| async def complete_public_upload( | |
| token: str, | |
| upload_id: int, | |
| payload: UploadCompleteRequest, | |
| db: Session = Depends(get_db) | |
| ): | |
| """Mark a public upload as completed.""" | |
| public_form = db.query(PublicForm).filter( | |
| PublicForm.share_token == token, | |
| PublicForm.is_public == True | |
| ).first() | |
| if not public_form: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Form not found" | |
| ) | |
| upload = db.query(FormUpload).filter( | |
| FormUpload.id == upload_id, | |
| FormUpload.form_id == public_form.form_id | |
| ).first() | |
| if not upload: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Upload not found" | |
| ) | |
| if payload.content_type: | |
| upload.content_type = payload.content_type | |
| if payload.size_bytes is not None: | |
| upload.size_bytes = payload.size_bytes | |
| upload.status = "uploaded" | |
| upload.uploaded_at = datetime.utcnow() | |
| db.commit() | |
| return UploadCompleteResponse(success=True) | |
| async def download_upload( | |
| form_id: int, | |
| upload_id: int, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """Generate a presigned download URL for a file upload (owner only).""" | |
| form = db.query(Form).filter( | |
| Form.id == form_id, | |
| Form.user_id == current_user.id | |
| ).first() | |
| if not form: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Form not found" | |
| ) | |
| upload = db.query(FormUpload).filter( | |
| FormUpload.id == upload_id, | |
| FormUpload.form_id == form_id | |
| ).first() | |
| if not upload: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Upload not found" | |
| ) | |
| try: | |
| download_url = s3_service.generate_presigned_download_url( | |
| key=upload.s3_key, | |
| filename=upload.original_filename | |
| ) | |
| except Exception: | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="S3 download is not configured" | |
| ) | |
| return {"download_url": download_url} | |