feat: layout hamburger, media browser filters+previews, billing fixes
- Layout: mobile hamburger menu + overlay backdrop + close button; content area always full-width - Media browser: filter chips (default still+turntable); advanced toggle for GLB/STL; thumbnail_url previews for non-image types; video hover-play for turntable - Backend: asset_types multi-filter, thumbnail_url in MediaAssetOut, download proxy endpoint for MinIO/local files - Admin: "Import Existing Media" button → POST /api/admin/import-media-assets - Billing: fix invoice create 500 (MissingGreenlet — use selectinload after commit); PDF download uses axios blob instead of bare <a href> (auth header missing); fix storage.upload() accepting str|Path - SSE task logs: task_logs.py core + router, LiveRenderLog component - CadPreview: fix infinite loop when no gltf_geometry assets; loading screen before ThreeDViewer render - render-worker: add trimesh layer to Dockerfile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,15 +12,17 @@ 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"])
|
||||
router = APIRouter(prefix="/api/media", tags=["media"], redirect_slashes=False)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[MediaAssetOut])
|
||||
@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),
|
||||
@@ -31,11 +33,13 @@ async def list_assets(
|
||||
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
|
||||
|
||||
|
||||
@@ -45,19 +49,45 @@ async def get_asset(asset_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
||||
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.get("/{asset_id}/download")
|
||||
@router.api_route("/{asset_id}/download", methods=["GET", "HEAD"])
|
||||
async def download_asset(asset_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
||||
from fastapi.responses import RedirectResponse
|
||||
"""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")
|
||||
url = service.get_download_url(asset)
|
||||
if url:
|
||||
return RedirectResponse(url)
|
||||
raise HTTPException(404, "File not available")
|
||||
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user