- Add per-mission params to rotation (MissionRotationItem.params); falls back to default_mission_params, then omits entirely if both empty - Add key-value widget to ConfigEditor for default_mission_params field - Add MissionParamsEditor component for editing param key/value/type rows - Bump config schema to 1.1.0 with migration from 1.0.0 - Add normalize_section() to Protocol and ArmaConfigGenerator for read-time backfill of old stored rows - Set Arma3 BasicConfig and ProfileConfig defaults from basic.cfg / Administrator.Arma3Profile - Document 3 known Mods tab bugs in CLAUDE.md for next-session fix
156 lines
5.1 KiB
Python
156 lines
5.1 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, Field
|
|
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 = ""
|
|
params: dict[str, int | float | str | bool] = Field(default_factory=dict)
|
|
|
|
|
|
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"}) |