Add limits for content + fix video playback

This commit is contained in:
2025-12-16 02:01:03 +07:00
parent 574140e67d
commit d96f8de568
5 changed files with 166 additions and 44 deletions

View File

@@ -2,8 +2,8 @@
Assignment details and dispute system endpoints.
"""
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import Response, StreamingResponse
from sqlalchemy import select
from sqlalchemy.orm import selectinload
@@ -171,13 +171,14 @@ async def get_assignment_detail(
)
@router.get("/assignments/{assignment_id}/proof-image")
async def get_assignment_proof_image(
@router.get("/assignments/{assignment_id}/proof-media")
async def get_assignment_proof_media(
assignment_id: int,
request: Request,
current_user: CurrentUser,
db: DbSession,
):
"""Stream the proof image for an assignment"""
"""Stream the proof media (image or video) for an assignment with Range support"""
# Get assignment
result = await db.execute(
select(Assignment)
@@ -205,15 +206,59 @@ async def get_assignment_proof_image(
# Check if proof exists
if not assignment.proof_path:
raise HTTPException(status_code=404, detail="No proof image for this assignment")
raise HTTPException(status_code=404, detail="No proof media for this assignment")
# Get file from storage
file_data = await storage_service.get_file(assignment.proof_path, "proofs")
if not file_data:
raise HTTPException(status_code=404, detail="Proof image not found in storage")
raise HTTPException(status_code=404, detail="Proof media not found in storage")
content, content_type = file_data
file_size = len(content)
# Check if it's a video and handle Range requests
is_video = content_type.startswith("video/")
if is_video:
range_header = request.headers.get("range")
if range_header:
# Parse range header
range_match = range_header.replace("bytes=", "").split("-")
start = int(range_match[0]) if range_match[0] else 0
end = int(range_match[1]) if range_match[1] else file_size - 1
# Ensure valid range
if start >= file_size:
raise HTTPException(status_code=416, detail="Range not satisfiable")
end = min(end, file_size - 1)
chunk = content[start:end + 1]
return Response(
content=chunk,
status_code=206,
media_type=content_type,
headers={
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(len(chunk)),
"Cache-Control": "public, max-age=31536000",
}
)
# No range header - return full video with Accept-Ranges
return Response(
content=content,
media_type=content_type,
headers={
"Accept-Ranges": "bytes",
"Content-Length": str(file_size),
"Cache-Control": "public, max-age=31536000",
}
)
# For images, just return the content
return Response(
content=content,
media_type=content_type,
@@ -223,6 +268,18 @@ async def get_assignment_proof_image(
)
# Keep old endpoint for backwards compatibility
@router.get("/assignments/{assignment_id}/proof-image")
async def get_assignment_proof_image(
assignment_id: int,
request: Request,
current_user: CurrentUser,
db: DbSession,
):
"""Deprecated: Use proof-media instead. Redirects to proof-media."""
return await get_assignment_proof_media(assignment_id, request, current_user, db)
@router.post("/assignments/{assignment_id}/dispute", response_model=DisputeResponse)
async def create_dispute(
assignment_id: int,

View File

@@ -23,7 +23,8 @@ class Settings(BaseSettings):
# Uploads
UPLOAD_DIR: str = "uploads"
MAX_UPLOAD_SIZE: int = 15 * 1024 * 1024 # 15 MB
MAX_IMAGE_SIZE: int = 15 * 1024 * 1024 # 15 MB
MAX_VIDEO_SIZE: int = 30 * 1024 * 1024 # 30 MB
ALLOWED_IMAGE_EXTENSIONS: set = {"jpg", "jpeg", "png", "gif", "webp"}
ALLOWED_VIDEO_EXTENSIONS: set = {"mp4", "webm", "mov"}