- Fix get_activities to use get_activities_paginated - Add get_user_assets, delete_asset, count_users, count_user_activities - Add get_user_activities, get_renderer, update_anchor, delete_anchor - Add record_run and get_run functions - Fix create_asset calls to use dict parameter - Fix update_asset call signature Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
245 lines
6.3 KiB
Python
245 lines
6.3 KiB
Python
"""
|
|
Asset management routes for L2 server.
|
|
|
|
Handles asset registration, listing, and publishing.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, Request, Depends, HTTPException, Form
|
|
from fastapi.responses import HTMLResponse
|
|
from pydantic import BaseModel
|
|
|
|
from artdag_common import render
|
|
from artdag_common.middleware import wants_html, wants_json
|
|
|
|
from ..config import settings
|
|
from ..dependencies import get_templates, require_auth, get_user_from_cookie
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AssetCreate(BaseModel):
|
|
name: str
|
|
content_hash: str
|
|
ipfs_cid: Optional[str] = None
|
|
asset_type: str # image, video, effect, recipe
|
|
tags: List[str] = []
|
|
metadata: dict = {}
|
|
provenance: Optional[dict] = None
|
|
|
|
|
|
class RecordRunRequest(BaseModel):
|
|
run_id: str
|
|
recipe: str
|
|
inputs: List[str]
|
|
output_hash: str
|
|
ipfs_cid: Optional[str] = None
|
|
provenance: Optional[dict] = None
|
|
|
|
|
|
@router.get("")
|
|
async def list_assets(
|
|
request: Request,
|
|
offset: int = 0,
|
|
limit: int = 20,
|
|
asset_type: Optional[str] = None,
|
|
):
|
|
"""List user's assets."""
|
|
import db
|
|
|
|
username = get_user_from_cookie(request)
|
|
if not username:
|
|
if wants_json(request):
|
|
raise HTTPException(401, "Authentication required")
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse(url="/login", status_code=302)
|
|
|
|
assets = await db.get_user_assets(username, offset=offset, limit=limit, asset_type=asset_type)
|
|
has_more = len(assets) >= limit
|
|
|
|
if wants_json(request):
|
|
return {"assets": assets, "offset": offset, "limit": limit, "has_more": has_more}
|
|
|
|
templates = get_templates(request)
|
|
return render(templates, "assets/list.html", request,
|
|
assets=assets,
|
|
user={"username": username},
|
|
offset=offset,
|
|
limit=limit,
|
|
has_more=has_more,
|
|
active_tab="assets",
|
|
)
|
|
|
|
|
|
@router.post("")
|
|
async def create_asset(
|
|
req: AssetCreate,
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
"""Register a new asset."""
|
|
import db
|
|
|
|
asset = await db.create_asset({
|
|
"owner": user["username"],
|
|
"name": req.name,
|
|
"content_hash": req.content_hash,
|
|
"ipfs_cid": req.ipfs_cid,
|
|
"asset_type": req.asset_type,
|
|
"tags": req.tags or [],
|
|
"metadata": req.metadata or {},
|
|
"provenance": req.provenance,
|
|
})
|
|
|
|
if not asset:
|
|
raise HTTPException(400, "Failed to create asset")
|
|
|
|
return {"asset_id": asset.get("name"), "message": "Asset registered"}
|
|
|
|
|
|
@router.get("/{asset_id}")
|
|
async def get_asset(
|
|
asset_id: str,
|
|
request: Request,
|
|
):
|
|
"""Get asset details."""
|
|
import db
|
|
|
|
username = get_user_from_cookie(request)
|
|
|
|
asset = await db.get_asset(asset_id)
|
|
if not asset:
|
|
raise HTTPException(404, "Asset not found")
|
|
|
|
if wants_json(request):
|
|
return asset
|
|
|
|
templates = get_templates(request)
|
|
return render(templates, "assets/detail.html", request,
|
|
asset=asset,
|
|
user={"username": username} if username else None,
|
|
active_tab="assets",
|
|
)
|
|
|
|
|
|
@router.delete("/{asset_id}")
|
|
async def delete_asset(
|
|
asset_id: str,
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
"""Delete an asset."""
|
|
import db
|
|
|
|
asset = await db.get_asset(asset_id)
|
|
if not asset:
|
|
raise HTTPException(404, "Asset not found")
|
|
|
|
if asset.get("owner") != user["username"]:
|
|
raise HTTPException(403, "Not authorized")
|
|
|
|
success = await db.delete_asset(asset_id)
|
|
if not success:
|
|
raise HTTPException(400, "Failed to delete asset")
|
|
|
|
return {"deleted": True}
|
|
|
|
|
|
@router.post("/record-run")
|
|
async def record_run(
|
|
req: RecordRunRequest,
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
"""Record a run completion and register output as asset."""
|
|
import db
|
|
|
|
# Create asset for output
|
|
asset = await db.create_asset({
|
|
"owner": user["username"],
|
|
"name": f"{req.recipe}-{req.run_id[:8]}",
|
|
"content_hash": req.output_hash,
|
|
"ipfs_cid": req.ipfs_cid,
|
|
"asset_type": "render",
|
|
"metadata": {
|
|
"run_id": req.run_id,
|
|
"recipe": req.recipe,
|
|
"inputs": req.inputs,
|
|
},
|
|
"provenance": req.provenance,
|
|
})
|
|
asset_id = asset.get("name") if asset else None
|
|
|
|
# Record run
|
|
await db.record_run(
|
|
run_id=req.run_id,
|
|
username=user["username"],
|
|
recipe=req.recipe,
|
|
inputs=req.inputs or [],
|
|
output_hash=req.output_hash,
|
|
ipfs_cid=req.ipfs_cid,
|
|
asset_id=asset_id,
|
|
)
|
|
|
|
return {
|
|
"run_id": req.run_id,
|
|
"asset_id": asset_id,
|
|
"recorded": True,
|
|
}
|
|
|
|
|
|
@router.get("/by-run-id/{run_id}")
|
|
async def get_asset_by_run_id(run_id: str):
|
|
"""Get asset by run ID (for L1 cache lookup)."""
|
|
import db
|
|
|
|
run = await db.get_run(run_id)
|
|
if not run:
|
|
raise HTTPException(404, "Run not found")
|
|
|
|
return {
|
|
"run_id": run_id,
|
|
"output_hash": run.get("output_hash"),
|
|
"ipfs_cid": run.get("ipfs_cid"),
|
|
"provenance_cid": run.get("provenance_cid"),
|
|
}
|
|
|
|
|
|
@router.post("/{asset_id}/publish")
|
|
async def publish_asset(
|
|
asset_id: str,
|
|
request: Request,
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
"""Publish asset to IPFS."""
|
|
import db
|
|
import ipfs_client
|
|
|
|
asset = await db.get_asset(asset_id)
|
|
if not asset:
|
|
raise HTTPException(404, "Asset not found")
|
|
|
|
if asset.get("owner") != user["username"]:
|
|
raise HTTPException(403, "Not authorized")
|
|
|
|
# Already published?
|
|
if asset.get("ipfs_cid"):
|
|
return {"ipfs_cid": asset["ipfs_cid"], "already_published": True}
|
|
|
|
# Get content from L1
|
|
content_hash = asset.get("content_hash")
|
|
for l1_url in settings.l1_servers:
|
|
try:
|
|
import requests
|
|
resp = requests.get(f"{l1_url}/cache/{content_hash}/raw", timeout=30)
|
|
if resp.status_code == 200:
|
|
# Pin to IPFS
|
|
cid = await ipfs_client.add_bytes(resp.content)
|
|
if cid:
|
|
await db.update_asset(asset_id, {"ipfs_cid": cid})
|
|
return {"ipfs_cid": cid, "published": True}
|
|
except Exception as e:
|
|
logger.warning(f"Failed to fetch from {l1_url}: {e}")
|
|
|
|
raise HTTPException(400, "Failed to publish - content not found on any L1")
|