Moved to S3

This commit is contained in:
2025-12-16 01:25:21 +07:00
parent c7966656d8
commit 87ecd9756c
15 changed files with 446 additions and 56 deletions

View File

@@ -3,6 +3,7 @@ Assignment details and dispute system endpoints.
"""
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response
from sqlalchemy import select
from sqlalchemy.orm import selectinload
@@ -17,6 +18,7 @@ from app.schemas import (
MessageResponse, ChallengeResponse, GameShort, ReturnedAssignmentResponse,
)
from app.schemas.user import UserPublic
from app.services.storage import storage_service
router = APIRouter(tags=["assignments"])
@@ -133,10 +135,7 @@ async def get_assignment_detail(
can_dispute = time_since_completion < timedelta(hours=DISPUTE_WINDOW_HOURS)
# Build proof URLs
proof_image_url = None
if assignment.proof_path:
# Extract filename from path
proof_image_url = f"/uploads/proofs/{assignment.proof_path.split('/')[-1]}"
proof_image_url = storage_service.get_url(assignment.proof_path, "proofs")
return AssignmentDetailResponse(
id=assignment.id,
@@ -153,7 +152,7 @@ async def get_assignment_detail(
game=GameShort(
id=game.id,
title=game.title,
cover_url=f"/uploads/covers/{game.cover_path.split('/')[-1]}" if game.cover_path else None,
cover_url=storage_service.get_url(game.cover_path, "covers"),
),
is_generated=challenge.is_generated,
created_at=challenge.created_at,
@@ -172,6 +171,58 @@ async def get_assignment_detail(
)
@router.get("/assignments/{assignment_id}/proof-image")
async def get_assignment_proof_image(
assignment_id: int,
current_user: CurrentUser,
db: DbSession,
):
"""Stream the proof image for an assignment"""
# Get assignment
result = await db.execute(
select(Assignment)
.options(
selectinload(Assignment.challenge).selectinload(Challenge.game),
)
.where(Assignment.id == assignment_id)
)
assignment = result.scalar_one_or_none()
if not assignment:
raise HTTPException(status_code=404, detail="Assignment not found")
# Check user is participant of the marathon
marathon_id = assignment.challenge.game.marathon_id
result = await db.execute(
select(Participant).where(
Participant.user_id == current_user.id,
Participant.marathon_id == marathon_id,
)
)
participant = result.scalar_one_or_none()
if not participant:
raise HTTPException(status_code=403, detail="You are not a participant of this marathon")
# Check if proof exists
if not assignment.proof_path:
raise HTTPException(status_code=404, detail="No proof image 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")
content, content_type = file_data
return Response(
content=content,
media_type=content_type,
headers={
"Cache-Control": "public, max-age=31536000",
}
)
@router.post("/assignments/{assignment_id}/dispute", response_model=DisputeResponse)
async def create_dispute(
assignment_id: int,
@@ -421,7 +472,7 @@ async def get_returned_assignments(
game=GameShort(
id=a.challenge.game.id,
title=a.challenge.game.title,
cover_url=f"/uploads/covers/{a.challenge.game.cover_path.split('/')[-1]}" if a.challenge.game.cover_path else None,
cover_url=storage_service.get_url(a.challenge.game.cover_path, "covers"),
),
is_generated=a.challenge.is_generated,
created_at=a.challenge.created_at,

View File

@@ -11,8 +11,6 @@ from app.models import (
SwapRequest as SwapRequestModel, SwapRequestStatus, User,
)
from fastapi import UploadFile, File, Form
from pathlib import Path
import uuid
from app.schemas import (
EventCreate, EventResponse, ActiveEventResponse, EventEffects,
@@ -24,6 +22,7 @@ from app.core.config import settings
from app.schemas.event import EVENT_INFO, COMMON_ENEMY_BONUSES
from app.schemas.user import UserPublic
from app.services.events import event_service
from app.services.storage import storage_service
router = APIRouter(tags=["events"])
@@ -937,13 +936,13 @@ def assignment_to_response(assignment: Assignment) -> AssignmentResponse:
game=GameShort(
id=game.id,
title=game.title,
cover_url=f"/uploads/covers/{game.cover_path.split('/')[-1]}" if game.cover_path else None,
cover_url=storage_service.get_url(game.cover_path, "covers"),
),
is_generated=challenge.is_generated,
created_at=challenge.created_at,
),
status=assignment.status,
proof_url=f"/uploads/proofs/{assignment.proof_path.split('/')[-1]}" if assignment.proof_path else assignment.proof_url,
proof_url=storage_service.get_url(assignment.proof_path, "proofs") if assignment.proof_path else assignment.proof_url,
proof_comment=assignment.proof_comment,
points_earned=assignment.points_earned,
streak_at_completion=assignment.streak_at_completion,
@@ -1065,14 +1064,16 @@ async def complete_event_assignment(
detail=f"Invalid file type. Allowed: {settings.ALLOWED_EXTENSIONS}",
)
filename = f"{assignment_id}_{uuid.uuid4().hex}.{ext}"
filepath = Path(settings.UPLOAD_DIR) / "proofs" / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
# Upload file to storage
filename = storage_service.generate_filename(assignment_id, proof_file.filename)
file_path = await storage_service.upload_file(
content=contents,
folder="proofs",
filename=filename,
content_type=proof_file.content_type or "application/octet-stream",
)
with open(filepath, "wb") as f:
f.write(contents)
assignment.proof_path = str(filepath)
assignment.proof_path = file_path
else:
assignment.proof_url = proof_url

View File

@@ -1,8 +1,6 @@
from fastapi import APIRouter, HTTPException, status, UploadFile, File, Query
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
import uuid
from pathlib import Path
from app.api.deps import (
DbSession, CurrentUser,
@@ -11,6 +9,7 @@ from app.api.deps import (
from app.core.config import settings
from app.models import Marathon, MarathonStatus, GameProposalMode, Game, GameStatus, Challenge, Activity, ActivityType
from app.schemas import GameCreate, GameUpdate, GameResponse, MessageResponse, UserPublic
from app.services.storage import storage_service
router = APIRouter(tags=["games"])
@@ -35,7 +34,7 @@ def game_to_response(game: Game, challenges_count: int = 0) -> GameResponse:
return GameResponse(
id=game.id,
title=game.title,
cover_url=f"/uploads/covers/{game.cover_path.split('/')[-1]}" if game.cover_path else None,
cover_url=storage_service.get_url(game.cover_path, "covers"),
download_url=game.download_url,
genre=game.genre,
status=game.status,
@@ -354,15 +353,20 @@ async def upload_cover(
detail=f"Invalid file type. Allowed: {settings.ALLOWED_IMAGE_EXTENSIONS}",
)
# Save file
filename = f"{game_id}_{uuid.uuid4().hex}.{ext}"
filepath = Path(settings.UPLOAD_DIR) / "covers" / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
# Delete old cover if exists
if game.cover_path:
await storage_service.delete_file(game.cover_path)
with open(filepath, "wb") as f:
f.write(contents)
# Upload file
filename = storage_service.generate_filename(game_id, file.filename)
file_path = await storage_service.upload_file(
content=contents,
folder="covers",
filename=filename,
content_type=file.content_type or "image/jpeg",
)
game.cover_path = str(filepath)
game.cover_path = file_path
await db.commit()
return await get_game(game_id, current_user, db)

View File

@@ -1,12 +1,11 @@
from fastapi import APIRouter, HTTPException, status, UploadFile, File
from sqlalchemy import select
import uuid
from pathlib import Path
from app.api.deps import DbSession, CurrentUser
from app.core.config import settings
from app.models import User
from app.schemas import UserPublic, UserUpdate, TelegramLink, MessageResponse
from app.services.storage import storage_service
router = APIRouter(prefix="/users", tags=["users"])
@@ -64,16 +63,21 @@ async def upload_avatar(
detail=f"Invalid file type. Allowed: {settings.ALLOWED_IMAGE_EXTENSIONS}",
)
# Save file
filename = f"{current_user.id}_{uuid.uuid4().hex}.{ext}"
filepath = Path(settings.UPLOAD_DIR) / "avatars" / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
# Delete old avatar if exists
if current_user.avatar_path:
await storage_service.delete_file(current_user.avatar_path)
with open(filepath, "wb") as f:
f.write(contents)
# Upload file
filename = storage_service.generate_filename(current_user.id, file.filename)
file_path = await storage_service.upload_file(
content=contents,
folder="avatars",
filename=filename,
content_type=file.content_type or "image/jpeg",
)
# Update user
current_user.avatar_path = str(filepath)
current_user.avatar_path = file_path
await db.commit()
await db.refresh(current_user)

View File

@@ -3,8 +3,6 @@ from datetime import datetime
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
import uuid
from pathlib import Path
from app.api.deps import DbSession, CurrentUser
from app.core.config import settings
@@ -19,6 +17,7 @@ from app.schemas import (
)
from app.services.points import PointsService
from app.services.events import event_service
from app.services.storage import storage_service
router = APIRouter(tags=["wheel"])
@@ -195,7 +194,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
game=GameResponse(
id=game.id,
title=game.title,
cover_url=f"/uploads/covers/{game.cover_path.split('/')[-1]}" if game.cover_path else None,
cover_url=storage_service.get_url(game.cover_path, "covers"),
download_url=game.download_url,
genre=game.genre,
added_by=None,
@@ -250,7 +249,7 @@ async def get_current_assignment(marathon_id: int, current_user: CurrentUser, db
created_at=challenge.created_at,
),
status=assignment.status,
proof_url=f"/uploads/proofs/{assignment.proof_path.split('/')[-1]}" if assignment.proof_path else assignment.proof_url,
proof_url=storage_service.get_url(assignment.proof_path, "proofs") if assignment.proof_path else assignment.proof_url,
proof_comment=assignment.proof_comment,
points_earned=assignment.points_earned,
streak_at_completion=assignment.streak_at_completion,
@@ -313,14 +312,16 @@ async def complete_assignment(
detail=f"Invalid file type. Allowed: {settings.ALLOWED_EXTENSIONS}",
)
filename = f"{assignment_id}_{uuid.uuid4().hex}.{ext}"
filepath = Path(settings.UPLOAD_DIR) / "proofs" / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
# Upload file to storage
filename = storage_service.generate_filename(assignment_id, proof_file.filename)
file_path = await storage_service.upload_file(
content=contents,
folder="proofs",
filename=filename,
content_type=proof_file.content_type or "application/octet-stream",
)
with open(filepath, "wb") as f:
f.write(contents)
assignment.proof_path = str(filepath)
assignment.proof_path = file_path
else:
assignment.proof_url = proof_url
@@ -571,7 +572,7 @@ async def get_my_history(
created_at=a.challenge.created_at,
),
status=a.status,
proof_url=f"/uploads/proofs/{a.proof_path.split('/')[-1]}" if a.proof_path else a.proof_url,
proof_url=storage_service.get_url(a.proof_path, "proofs") if a.proof_path else a.proof_url,
proof_comment=a.proof_comment,
points_earned=a.points_earned,
streak_at_completion=a.streak_at_completion,