Files
languard-servers-manager/backend/core/servers/missions_router.py
Tran G. (Revernomad) Khoa 4aae08420b feat: Phase 2 — Mission rotation management + multi-file upload
- Backend: add terrain field to Arma3MissionManager.list_missions()
- Backend: add missions field to ServerConfig Pydantic model
- Backend: add GET /missions/rotation and PUT /missions/rotation endpoints
- Frontend: Mission type gains terrain field; new MissionRotationEntry type
- Frontend: useServerMissionRotation and useUpdateMissionRotation hooks
- Frontend: useUploadMission updated to accept File[] with sequential upload
- Frontend: MissionList redesigned with Available Missions + Mission Rotation sections
- Frontend: per-file upload progress tracking, terrain badges, difficulty select
- Tests: 5 new tests; fixed existing useUploadMission test for File[] API; 141 pass
2026-04-17 20:33:04 +07:00

155 lines
5.0 KiB
Python

"""Mission management endpoints — list, upload, delete mission files."""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
from pydantic import BaseModel
from sqlalchemy.engine import Connection
from adapters.exceptions import AdapterError
from adapters.registry import GameAdapterRegistry
from core.servers.service import ServerService
from database import get_db
from dependencies import get_current_user, require_admin
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/servers/{server_id}/missions", tags=["missions"])
_MAX_UPLOAD_SIZE = 500 * 1024 * 1024 # 500 MB
class MissionRotationEntry(BaseModel):
name: str
difficulty: str = ""
class MissionRotationUpdate(BaseModel):
missions: list[MissionRotationEntry]
config_version: int
def _ok(data):
return {"success": True, "data": data, "error": None}
def _get_mission_manager(server_id: int, game_type: str):
"""Get MissionManager for the server's game type."""
adapter = GameAdapterRegistry.get(game_type)
if not adapter.has_capability("mission_manager"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"code": "NOT_SUPPORTED", "message": f"Game type '{game_type}' does not support mission management"},
)
return adapter.get_mission_manager(server_id)
@router.get("/rotation")
def get_mission_rotation(
server_id: int,
db: Annotated[Connection, Depends(get_db)],
_user: Annotated[dict, Depends(get_current_user)],
) -> dict:
"""Get the current mission rotation from the server config."""
config = ServerService(db).get_config_section(server_id, "server")
missions = config.get("missions", [])
return _ok({"missions": missions})
@router.put("/rotation")
def update_mission_rotation(
server_id: int,
body: MissionRotationUpdate,
db: Annotated[Connection, Depends(get_db)],
_admin: Annotated[dict, Depends(require_admin)],
) -> dict:
"""Replace the mission rotation in the server config."""
updated = ServerService(db).update_config_section(
server_id=server_id,
section="server",
data={"missions": [e.model_dump() for e in body.missions]},
expected_version=body.config_version,
)
return _ok({"missions": updated.get("missions", [])})
@router.get("")
def list_missions(
server_id: int,
db: Annotated[Connection, Depends(get_db)],
_user: Annotated[dict, Depends(get_current_user)],
) -> dict:
"""List all available mission files on disk."""
server = ServerService(db).get_server(server_id) # raises 404 if not found
mgr = _get_mission_manager(server_id, server["game_type"])
try:
missions = mgr.list_missions()
except AdapterError as exc:
raise HTTPException(status_code=500, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
return _ok({
"server_id": server_id,
"missions": missions,
"total": len(missions),
})
@router.post("", status_code=status.HTTP_201_CREATED)
async def upload_mission(
server_id: int,
db: Annotated[Connection, Depends(get_db)],
_admin: Annotated[dict, Depends(require_admin)],
file: UploadFile = File(...),
) -> dict:
"""
Upload a mission .pbo file.
Max size: 500 MB.
"""
server = ServerService(db).get_server(server_id) # raises 404 if not found
if not file.filename:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"code": "NO_FILENAME", "message": "No filename provided"},
)
content = await file.read()
if len(content) > _MAX_UPLOAD_SIZE:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail={"code": "FILE_TOO_LARGE", "message": f"File too large. Max size is {_MAX_UPLOAD_SIZE // (1024*1024)} MB"},
)
mgr = _get_mission_manager(server_id, server["game_type"])
try:
mission = mgr.upload_mission(file.filename, content)
except AdapterError as exc:
raise HTTPException(status_code=400, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
return _ok(mission)
@router.delete("/{filename}")
def delete_mission(
server_id: int,
filename: str,
db: Annotated[Connection, Depends(get_db)],
_admin: Annotated[dict, Depends(require_admin)],
) -> dict:
"""Delete a mission file by filename."""
server = ServerService(db).get_server(server_id) # raises 404 if not found
mgr = _get_mission_manager(server_id, server["game_type"])
try:
deleted = mgr.delete_mission(filename)
except AdapterError as exc:
raise HTTPException(status_code=400, detail={"code": "ADAPTER_ERROR", "message": str(exc)})
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "NOT_FOUND", "message": f"Mission '{filename}' not found"},
)
return _ok({"message": f"Mission '{filename}' deleted"})