3ac3ca1d70
- fix(tasks): use RENDER_SCRIPTS_DIR env var for catalog_assets.py path (was computing wrong path via __file__ parents → /render-worker/scripts/ which doesn't exist in container) - fix(models): add AssetLibrary to app/models/__init__.py so alembic autogenerate discovers it - fix(api): remove unused FileResponse import from asset_libraries.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
6.0 KiB
Python
199 lines
6.0 KiB
Python
"""Asset Libraries API — CRUD + .blend upload + catalog refresh."""
|
|
import uuid
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from pydantic import BaseModel
|
|
|
|
from app.database import get_db
|
|
from app.config import settings
|
|
from app.domains.materials.models import AssetLibrary
|
|
from app.utils.auth import require_admin_or_pm
|
|
|
|
router = APIRouter(prefix="/asset-libraries", tags=["asset-libraries"])
|
|
|
|
ASSET_LIB_DIR = "asset-libraries"
|
|
|
|
|
|
def _asset_lib_dir() -> Path:
|
|
d = Path(settings.upload_dir) / ASSET_LIB_DIR
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
return d
|
|
|
|
|
|
# ── Schemas ──────────────────────────────────────────────────────────────────
|
|
|
|
class AssetLibraryOut(BaseModel):
|
|
id: str
|
|
name: str
|
|
description: str | None
|
|
original_filename: str | None
|
|
catalog: dict
|
|
is_active: bool
|
|
created_at: str
|
|
updated_at: str
|
|
|
|
|
|
class AssetLibraryUpdate(BaseModel):
|
|
name: str | None = None
|
|
description: str | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
def _to_out(lib: AssetLibrary) -> dict:
|
|
return {
|
|
"id": str(lib.id),
|
|
"name": lib.name,
|
|
"description": lib.description,
|
|
"original_filename": lib.original_filename,
|
|
"catalog": lib.catalog or {"materials": [], "node_groups": []},
|
|
"is_active": lib.is_active,
|
|
"created_at": lib.created_at.isoformat(),
|
|
"updated_at": lib.updated_at.isoformat(),
|
|
}
|
|
|
|
|
|
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
|
|
|
@router.get("", response_model=list[AssetLibraryOut])
|
|
async def list_asset_libraries(
|
|
db: AsyncSession = Depends(get_db),
|
|
_user=Depends(require_admin_or_pm),
|
|
):
|
|
result = await db.execute(select(AssetLibrary).order_by(AssetLibrary.name))
|
|
return [_to_out(lib) for lib in result.scalars().all()]
|
|
|
|
|
|
@router.post("", response_model=AssetLibraryOut, status_code=status.HTTP_201_CREATED)
|
|
async def create_asset_library(
|
|
name: str = Form(...),
|
|
description: str | None = Form(None),
|
|
blend_file: UploadFile = File(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user=Depends(require_admin_or_pm),
|
|
):
|
|
lib = AssetLibrary(
|
|
name=name,
|
|
description=description,
|
|
original_filename=blend_file.filename,
|
|
catalog={"materials": [], "node_groups": []},
|
|
)
|
|
db.add(lib)
|
|
await db.flush() # get the id
|
|
|
|
# Save .blend file
|
|
dest = _asset_lib_dir() / f"{lib.id}.blend"
|
|
with dest.open("wb") as f:
|
|
shutil.copyfileobj(blend_file.file, f)
|
|
lib.blend_file_path = str(dest)
|
|
|
|
await db.commit()
|
|
await db.refresh(lib)
|
|
|
|
# Queue catalog refresh
|
|
try:
|
|
from app.domains.materials.tasks import refresh_asset_library_catalog
|
|
refresh_asset_library_catalog.delay(str(lib.id))
|
|
except Exception:
|
|
pass # task queuing failure is non-blocking
|
|
|
|
return _to_out(lib)
|
|
|
|
|
|
@router.get("/{lib_id}", response_model=AssetLibraryOut)
|
|
async def get_asset_library(
|
|
lib_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
_user=Depends(require_admin_or_pm),
|
|
):
|
|
lib = await db.get(AssetLibrary, lib_id)
|
|
if not lib:
|
|
raise HTTPException(status_code=404, detail="Asset library not found")
|
|
return _to_out(lib)
|
|
|
|
|
|
@router.patch("/{lib_id}", response_model=AssetLibraryOut)
|
|
async def update_asset_library(
|
|
lib_id: uuid.UUID,
|
|
body: AssetLibraryUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
_user=Depends(require_admin_or_pm),
|
|
):
|
|
lib = await db.get(AssetLibrary, lib_id)
|
|
if not lib:
|
|
raise HTTPException(status_code=404, detail="Asset library not found")
|
|
for field, value in body.model_dump(exclude_none=True).items():
|
|
setattr(lib, field, value)
|
|
await db.commit()
|
|
await db.refresh(lib)
|
|
return _to_out(lib)
|
|
|
|
|
|
@router.post("/{lib_id}/upload-blend", response_model=AssetLibraryOut)
|
|
async def upload_blend(
|
|
lib_id: uuid.UUID,
|
|
blend_file: UploadFile = File(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user=Depends(require_admin_or_pm),
|
|
):
|
|
lib = await db.get(AssetLibrary, lib_id)
|
|
if not lib:
|
|
raise HTTPException(status_code=404, detail="Asset library not found")
|
|
|
|
dest = _asset_lib_dir() / f"{lib.id}.blend"
|
|
with dest.open("wb") as f:
|
|
shutil.copyfileobj(blend_file.file, f)
|
|
lib.blend_file_path = str(dest)
|
|
lib.original_filename = blend_file.filename
|
|
|
|
await db.commit()
|
|
await db.refresh(lib)
|
|
|
|
try:
|
|
from app.domains.materials.tasks import refresh_asset_library_catalog
|
|
refresh_asset_library_catalog.delay(str(lib.id))
|
|
except Exception:
|
|
pass
|
|
|
|
return _to_out(lib)
|
|
|
|
|
|
@router.post("/{lib_id}/refresh-catalog", response_model=AssetLibraryOut)
|
|
async def refresh_catalog(
|
|
lib_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
_user=Depends(require_admin_or_pm),
|
|
):
|
|
lib = await db.get(AssetLibrary, lib_id)
|
|
if not lib:
|
|
raise HTTPException(status_code=404, detail="Asset library not found")
|
|
if not lib.blend_file_path:
|
|
raise HTTPException(status_code=400, detail="No .blend file uploaded yet")
|
|
|
|
from app.domains.materials.tasks import refresh_asset_library_catalog
|
|
refresh_asset_library_catalog.delay(str(lib.id))
|
|
return _to_out(lib)
|
|
|
|
|
|
@router.delete("/{lib_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_asset_library(
|
|
lib_id: uuid.UUID,
|
|
db: AsyncSession = Depends(get_db),
|
|
_user=Depends(require_admin_or_pm),
|
|
):
|
|
lib = await db.get(AssetLibrary, lib_id)
|
|
if not lib:
|
|
raise HTTPException(status_code=404, detail="Asset library not found")
|
|
|
|
# Remove .blend file from disk
|
|
if lib.blend_file_path:
|
|
p = Path(lib.blend_file_path)
|
|
if p.exists():
|
|
p.unlink()
|
|
|
|
await db.delete(lib)
|
|
await db.commit()
|