From 85f349a7ab01032980a33469a7afa17627eddc30 Mon Sep 17 00:00:00 2001 From: gilesb Date: Wed, 7 Jan 2026 19:08:03 +0000 Subject: [PATCH] feat: add L1-L2 publish integration endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add POST /cache/{hash}/publish to publish cache items to L2 - Add PATCH /cache/{hash}/republish to sync metadata updates - Validates origin is set before publishing - Tracks publish status in cache metadata (to_l2, asset_name, timestamps) - Forwards auth token to L2 for authentication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- server.py | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/server.py b/server.py index e36da76..1319d4f 100644 --- a/server.py +++ b/server.py @@ -614,6 +614,12 @@ class CacheMetaUpdate(BaseModel): collections: Optional[list[str]] = None +class PublishRequest(BaseModel): + """Request to publish a cache item to L2.""" + asset_name: str + asset_type: str = "image" # image, video, etc. + + @app.get("/cache/{content_hash}/meta") async def get_cache_meta(content_hash: str, username: str = Depends(get_required_user)): """Get metadata for a cached file.""" @@ -670,6 +676,178 @@ async def update_cache_meta(content_hash: str, update: CacheMetaUpdate, username return meta +@app.post("/cache/{content_hash}/publish") +async def publish_cache_to_l2( + content_hash: str, + req: PublishRequest, + request: Request, + username: str = Depends(get_required_user) +): + """ + Publish a cache item to L2 (ActivityPub). + + Requires origin to be set in metadata before publishing. + """ + # Check file exists + cache_path = CACHE_DIR / content_hash + if not cache_path.exists(): + raise HTTPException(404, "Content not found") + + # Check ownership + user_hashes = get_user_cache_hashes(username) + if content_hash not in user_hashes: + raise HTTPException(403, "Access denied") + + # Load metadata + meta = load_cache_meta(content_hash) + + # Check origin is set + origin = meta.get("origin") + if not origin or "type" not in origin: + raise HTTPException(400, "Origin must be set before publishing. Use --origin self or --origin-url ") + + # Get auth token to pass to L2 + token = request.cookies.get("auth_token") + if not token: + # Try from header + auth_header = request.headers.get("Authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header[7:] + + if not token: + raise HTTPException(401, "Authentication token required") + + # Call L2 publish-cache endpoint + try: + resp = http_requests.post( + f"{L2_SERVER}/registry/publish-cache", + headers={"Authorization": f"Bearer {token}"}, + json={ + "content_hash": content_hash, + "asset_name": req.asset_name, + "asset_type": req.asset_type, + "origin": origin, + "description": meta.get("description"), + "tags": meta.get("tags", []), + "metadata": { + "filename": meta.get("filename"), + "folder": meta.get("folder"), + "collections": meta.get("collections", []) + } + }, + timeout=10 + ) + resp.raise_for_status() + l2_result = resp.json() + except http_requests.exceptions.HTTPError as e: + error_detail = "" + try: + error_detail = e.response.json().get("detail", str(e)) + except Exception: + error_detail = str(e) + raise HTTPException(400, f"L2 publish failed: {error_detail}") + except Exception as e: + raise HTTPException(500, f"L2 publish failed: {e}") + + # Update local metadata with publish status + publish_info = { + "to_l2": True, + "asset_name": req.asset_name, + "published_at": datetime.now(timezone.utc).isoformat(), + "last_synced_at": datetime.now(timezone.utc).isoformat() + } + save_cache_meta(content_hash, published=publish_info) + + return { + "published": True, + "asset_name": req.asset_name, + "l2_result": l2_result + } + + +@app.patch("/cache/{content_hash}/republish") +async def republish_cache_to_l2( + content_hash: str, + request: Request, + username: str = Depends(get_required_user) +): + """ + Re-publish (update) a cache item on L2 after metadata changes. + + Only works for already-published items. + """ + # Check file exists + cache_path = CACHE_DIR / content_hash + if not cache_path.exists(): + raise HTTPException(404, "Content not found") + + # Check ownership + user_hashes = get_user_cache_hashes(username) + if content_hash not in user_hashes: + raise HTTPException(403, "Access denied") + + # Load metadata + meta = load_cache_meta(content_hash) + + # Check already published + published = meta.get("published", {}) + if not published.get("to_l2"): + raise HTTPException(400, "Item not published yet. Use publish first.") + + asset_name = published.get("asset_name") + if not asset_name: + raise HTTPException(400, "No asset name found in publish info") + + # Get auth token + token = request.cookies.get("auth_token") + if not token: + auth_header = request.headers.get("Authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header[7:] + + if not token: + raise HTTPException(401, "Authentication token required") + + # Call L2 update endpoint + try: + resp = http_requests.patch( + f"{L2_SERVER}/registry/{asset_name}", + headers={"Authorization": f"Bearer {token}"}, + json={ + "description": meta.get("description"), + "tags": meta.get("tags"), + "origin": meta.get("origin"), + "metadata": { + "filename": meta.get("filename"), + "folder": meta.get("folder"), + "collections": meta.get("collections", []) + } + }, + timeout=10 + ) + resp.raise_for_status() + l2_result = resp.json() + except http_requests.exceptions.HTTPError as e: + error_detail = "" + try: + error_detail = e.response.json().get("detail", str(e)) + except Exception: + error_detail = str(e) + raise HTTPException(400, f"L2 update failed: {error_detail}") + except Exception as e: + raise HTTPException(500, f"L2 update failed: {e}") + + # Update local metadata + published["last_synced_at"] = datetime.now(timezone.utc).isoformat() + save_cache_meta(content_hash, published=published) + + return { + "updated": True, + "asset_name": asset_name, + "l2_result": l2_result + } + + # ============ Folder & Collection Management ============ @app.get("/user/folders")