"""MediaAsset router — /api/media.""" import io import uuid import zipfile from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.domains.media.models import MediaAssetType from app.domains.media.schemas import MediaAssetOut from app.domains.media import service router = APIRouter(prefix="/api/media", tags=["media"], redirect_slashes=False) @router.get("", response_model=list[MediaAssetOut]) @router.get("/", response_model=list[MediaAssetOut], include_in_schema=False) async def list_assets( product_id: uuid.UUID | None = None, order_line_id: uuid.UUID | None = None, cad_file_id: uuid.UUID | None = None, asset_type: MediaAssetType | None = None, asset_types: list[MediaAssetType] = Query(default=[]), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=500), db: AsyncSession = Depends(get_db), ): assets = await service.list_media_assets( db, product_id=product_id, order_line_id=order_line_id, cad_file_id=cad_file_id, asset_type=asset_type, asset_types=asset_types if asset_types else None, skip=skip, limit=limit, ) for a in assets: a.download_url = service.get_download_url(a) a.thumbnail_url = service.get_thumbnail_url(a) return assets @router.get("/{asset_id}", response_model=MediaAssetOut) async def get_asset(asset_id: uuid.UUID, db: AsyncSession = Depends(get_db)): asset = await service.get_media_asset(db, asset_id) if not asset: raise HTTPException(404, "Asset not found") asset.download_url = service.get_download_url(asset) asset.thumbnail_url = service.get_thumbnail_url(asset) return asset @router.api_route("/{asset_id}/download", methods=["GET", "HEAD"]) async def download_asset(asset_id: uuid.UUID, db: AsyncSession = Depends(get_db)): """Proxy file content directly — avoids internal MinIO hostname issues.""" from fastapi.responses import FileResponse, Response from pathlib import Path asset = await service.get_media_asset(db, asset_id) if not asset: raise HTTPException(404, "Asset not found") key = asset.storage_key mime = asset.mime_type or "application/octet-stream" # Local file path (absolute or relative to UPLOAD_DIR) candidate = Path(key) if not candidate.is_absolute(): from app.config import settings candidate = Path(settings.UPLOAD_DIR) / key if candidate.exists(): ext = candidate.suffix.lstrip(".") fname = f"{asset.asset_type.value}_{asset_id}.{ext or 'bin'}" return FileResponse(str(candidate), media_type=mime, filename=fname) # Fall back to MinIO try: from app.core.storage import get_storage data = get_storage().download_bytes(key) ext = key.rsplit(".", 1)[-1] if "." in key else "bin" fname = f"{asset.asset_type.value}_{asset_id}.{ext}" return Response( content=data, media_type=mime, headers={"Content-Disposition": f"attachment; filename={fname}"}, ) except Exception: raise HTTPException(404, "File not available") @router.post("/zip") async def zip_download( asset_ids: list[uuid.UUID], db: AsyncSession = Depends(get_db), ): assets = [] for aid in asset_ids: a = await service.get_media_asset(db, aid) if a: assets.append(a) if not assets: raise HTTPException(404, "No assets found") def generate(): buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: from app.core.storage import get_storage storage = get_storage() for a in assets: ext = (a.mime_type or "").split("/")[-1] or "bin" fname = f"{a.asset_type.value}_{a.id}.{ext}" try: data = storage.download_bytes(a.storage_key) zf.writestr(fname, data) except Exception: pass yield buf.getvalue() return StreamingResponse( generate(), media_type="application/zip", headers={"Content-Disposition": "attachment; filename=media-export.zip"}, ) @router.delete("/{asset_id}") async def archive_asset(asset_id: uuid.UUID, db: AsyncSession = Depends(get_db)): asset = await service.archive_media_asset(db, asset_id) if not asset: raise HTTPException(404, "Asset not found") return {"ok": True}