feat: add L1-L2 publish integration endpoints
- 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 <noreply@anthropic.com>
This commit is contained in:
178
server.py
178
server.py
@@ -614,6 +614,12 @@ class CacheMetaUpdate(BaseModel):
|
|||||||
collections: Optional[list[str]] = None
|
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")
|
@app.get("/cache/{content_hash}/meta")
|
||||||
async def get_cache_meta(content_hash: str, username: str = Depends(get_required_user)):
|
async def get_cache_meta(content_hash: str, username: str = Depends(get_required_user)):
|
||||||
"""Get metadata for a cached file."""
|
"""Get metadata for a cached file."""
|
||||||
@@ -670,6 +676,178 @@ async def update_cache_meta(content_hash: str, update: CacheMetaUpdate, username
|
|||||||
return meta
|
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 <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 ============
|
# ============ Folder & Collection Management ============
|
||||||
|
|
||||||
@app.get("/user/folders")
|
@app.get("/user/folders")
|
||||||
|
|||||||
Reference in New Issue
Block a user