Files
rose-ash/artdag/l2/app/routers/assets.py
giles 1a74d811f7
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m33s
Incorporate art-dag-mono repo into artdag/ subfolder
Merges full history from art-dag/mono.git into the monorepo
under the artdag/ directory. Contains: core (DAG engine),
l1 (Celery rendering server), l2 (ActivityPub registry),
common (shared templates/middleware), client (CLI), test (e2e).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

git-subtree-dir: artdag
git-subtree-mainline: 1a179de547
git-subtree-split: 4c2e716558
2026-02-27 09:07:23 +00:00

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")