193 lines
5.7 KiB
Python
193 lines
5.7 KiB
Python
"""Asset management router."""
|
|
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.database import get_db
|
|
from app.dependencies import CurrentUser
|
|
from app.models.asset import Asset, AssetType
|
|
from app.models.world import World
|
|
from pydantic import BaseModel, Field
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# --- Pydantic schemas ---
|
|
|
|
class AssetCreate(BaseModel):
|
|
"""Schema for creating an asset reference."""
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
description: Optional[str] = None
|
|
asset_type: AssetType
|
|
world_id: Optional[int] = None
|
|
|
|
|
|
class AssetResponse(BaseModel):
|
|
"""Schema for asset responses."""
|
|
id: int
|
|
name: str
|
|
description: Optional[str]
|
|
asset_type: AssetType
|
|
file_path: str
|
|
file_size: Optional[int]
|
|
mime_type: Optional[str]
|
|
world_id: Optional[int]
|
|
owner_id: int
|
|
is_public: bool
|
|
created_at: str
|
|
updated_at: str
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class AssetUpdate(BaseModel):
|
|
"""Schema for updating an asset."""
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
is_public: Optional[bool] = None
|
|
|
|
|
|
# --- Endpoints ---
|
|
|
|
@router.post("/", response_model=AssetResponse, status_code=201)
|
|
async def create_asset(
|
|
payload: AssetCreate,
|
|
current_user: CurrentUser,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a new asset reference."""
|
|
# Verify world ownership if world_id is provided
|
|
if payload.world_id is not None:
|
|
world = await db.get(World, payload.world_id)
|
|
if world is None:
|
|
raise HTTPException(status_code=404, detail="World not found")
|
|
if world.owner_id != current_user.id:
|
|
raise HTTPException(status_code=403, detail="Not owner of this world")
|
|
|
|
asset = Asset(
|
|
name=payload.name,
|
|
description=payload.description,
|
|
asset_type=payload.asset_type,
|
|
file_path="", # Will be set after upload
|
|
owner_id=current_user.id,
|
|
world_id=payload.world_id,
|
|
is_public=False,
|
|
)
|
|
db.add(asset)
|
|
await db.commit()
|
|
await db.refresh(asset)
|
|
return asset
|
|
|
|
|
|
@router.get("/", response_model=list[AssetResponse])
|
|
async def list_assets(
|
|
current_user: CurrentUser,
|
|
world_id: Optional[int] = Query(None, description="Filter by world"),
|
|
asset_type: Optional[AssetType] = Query(None),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
offset: int = Query(0, ge=0),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List assets with optional filters."""
|
|
stmt = select(Asset).where(Asset.owner_id == current_user.id)
|
|
|
|
if world_id is not None:
|
|
stmt = stmt.where(Asset.world_id == world_id)
|
|
if asset_type is not None:
|
|
stmt = stmt.where(Asset.asset_type == asset_type)
|
|
|
|
stmt = stmt.order_by(Asset.created_at.desc()).limit(limit).offset(offset)
|
|
result = await db.execute(stmt)
|
|
assets = result.scalars().all()
|
|
return assets
|
|
|
|
|
|
@router.get("/{asset_id}", response_model=AssetResponse)
|
|
async def get_asset(
|
|
asset_id: int,
|
|
current_user: CurrentUser,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get a single asset by ID."""
|
|
asset = await db.get(Asset, asset_id)
|
|
if asset is None or asset.owner_id != current_user.id:
|
|
raise HTTPException(status_code=404, detail="Asset not found")
|
|
return asset
|
|
|
|
|
|
@router.patch("/{asset_id}", response_model=AssetResponse)
|
|
async def update_asset(
|
|
asset_id: int,
|
|
payload: AssetUpdate,
|
|
current_user: CurrentUser,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Update an asset."""
|
|
asset = await db.get(Asset, asset_id)
|
|
if asset is None or asset.owner_id != current_user.id:
|
|
raise HTTPException(status_code=404, detail="Asset not found")
|
|
|
|
update_data = payload.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(asset, field, value)
|
|
|
|
await db.commit()
|
|
await db.refresh(asset)
|
|
return asset
|
|
|
|
|
|
@router.delete("/{asset_id}", status_code=204)
|
|
async def delete_asset(
|
|
asset_id: int,
|
|
current_user: CurrentUser,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Delete an asset."""
|
|
asset = await db.get(Asset, asset_id)
|
|
if asset is None or asset.owner_id != current_user.id:
|
|
raise HTTPException(status_code=404, detail="Asset not found")
|
|
|
|
await db.delete(asset)
|
|
await db.commit()
|
|
|
|
|
|
@router.post("/{asset_id}/upload")
|
|
async def upload_asset_file(
|
|
asset_id: int,
|
|
file: UploadFile = File(...),
|
|
current_user: CurrentUser = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Upload a file for an existing asset."""
|
|
asset = await db.get(Asset, asset_id)
|
|
if asset is None or asset.owner_id != current_user.id:
|
|
raise HTTPException(status_code=404, detail="Asset not found")
|
|
|
|
# Sanitize filename to prevent path traversal
|
|
safe_filename = file.filename.replace("/", "_").replace("\\", "_") if file.filename else f"asset_{asset_id}"
|
|
file_path = f"uploads/{asset_id}_{safe_filename}"
|
|
|
|
# Validate file size against configured maximum
|
|
max_bytes = settings.MAX_UPLOAD_SIZE_MB * 1024 * 1024
|
|
content = await file.read()
|
|
file_size = len(content)
|
|
if file_size > max_bytes:
|
|
raise HTTPException(
|
|
status_code=413,
|
|
detail=f"File size {file_size} exceeds maximum allowed size {max_bytes} bytes",
|
|
)
|
|
|
|
# TODO: Implement actual file write logic (S3, local filesystem, etc.)
|
|
asset.file_path = file_path
|
|
asset.mime_type = file.content_type
|
|
asset.file_size = file_size
|
|
|
|
await db.commit()
|
|
await db.refresh(asset)
|
|
|
|
return {"message": "File upload endpoint", "asset_id": asset_id, "file_path": file_path}
|