Fix effects router to use proper ownership model

- Upload: Create item_types entry to track user-effect relationship
- List: Query item_types for user's effects instead of scanning filesystem
- Delete: Remove ownership link, only delete files if orphaned (garbage collect)

This matches the ownership model used by recipes and media, where multiple
users can "own" the same cached content through item_types entries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-01-12 19:56:12 +00:00
parent 427de25e13
commit abe89c9177

View File

@@ -200,6 +200,15 @@ async def upload_effect(
# Also store metadata in IPFS for discoverability # Also store metadata in IPFS for discoverability
meta_cid = ipfs_client.add_json(full_meta) meta_cid = ipfs_client.add_json(full_meta)
# Track ownership in item_types
import database
await database.save_item_metadata(
cid=cid,
actor_id=ctx.actor_id,
item_type="effect",
filename=file.filename,
)
# Assign friendly name # Assign friendly name
from ..services.naming_service import get_naming_service from ..services.naming_service import get_naming_service
naming = get_naming_service() naming = get_naming_service()
@@ -314,24 +323,26 @@ async def list_effects(
limit: int = 20, limit: int = 20,
ctx: UserContext = Depends(require_auth), ctx: UserContext = Depends(require_auth),
): ):
"""List uploaded effects with pagination.""" """List user's effects with pagination."""
import database
effects_dir = get_effects_dir() effects_dir = get_effects_dir()
effects = [] effects = []
# Get user's effect CIDs from item_types
user_items = await database.get_user_items(ctx.actor_id, item_type="effect", limit=1000)
effect_cids = [item["cid"] for item in user_items]
# Get naming service for friendly name lookup # Get naming service for friendly name lookup
from ..services.naming_service import get_naming_service from ..services.naming_service import get_naming_service
naming = get_naming_service() naming = get_naming_service()
if effects_dir.exists(): for cid in effect_cids:
for effect_dir in effects_dir.iterdir(): effect_dir = effects_dir / cid
if effect_dir.is_dir():
metadata_path = effect_dir / "metadata.json" metadata_path = effect_dir / "metadata.json"
if metadata_path.exists(): if metadata_path.exists():
try: try:
meta = json.loads(metadata_path.read_text()) meta = json.loads(metadata_path.read_text())
# Add friendly name if available # Add friendly name if available
cid = meta.get("cid")
if cid:
friendly = await naming.get_by_cid(ctx.actor_id, cid) friendly = await naming.get_by_cid(ctx.actor_id, cid)
if friendly: if friendly:
meta["friendly_name"] = friendly["friendly_name"] meta["friendly_name"] = friendly["friendly_name"]
@@ -412,25 +423,29 @@ async def delete_effect(
cid: str, cid: str,
ctx: UserContext = Depends(require_auth), ctx: UserContext = Depends(require_auth),
): ):
"""Delete an effect from local cache (IPFS content is immutable).""" """Remove user's ownership link to an effect."""
import database
# Remove user's ownership link from item_types
await database.delete_item_type(cid, ctx.actor_id, "effect")
# Remove friendly name
await database.delete_friendly_name(ctx.actor_id, cid)
# Check if anyone still owns this effect
remaining_owners = await database.get_item_types(cid)
# Only delete local files if no one owns it anymore
if not remaining_owners:
effects_dir = get_effects_dir() effects_dir = get_effects_dir()
effect_dir = effects_dir / cid effect_dir = effects_dir / cid
if effect_dir.exists():
if not effect_dir.exists():
raise HTTPException(404, f"Effect {cid[:16]}... not found in local cache")
# Check ownership
metadata_path = effect_dir / "metadata.json"
if metadata_path.exists():
meta = json.loads(metadata_path.read_text())
if meta.get("uploader") != ctx.actor_id:
raise HTTPException(403, "Can only delete your own effects")
import shutil import shutil
shutil.rmtree(effect_dir) shutil.rmtree(effect_dir)
# Unpin from IPFS (content remains available if pinned elsewhere) # Unpin from IPFS
ipfs_client.unpin(cid) ipfs_client.unpin(cid)
logger.info(f"Garbage collected effect {cid[:16]}... (no remaining owners)")
logger.info(f"Deleted effect {cid[:16]}... by {ctx.actor_id}") logger.info(f"Removed effect {cid[:16]}... ownership for {ctx.actor_id}")
return {"deleted": True, "note": "Unpinned from local IPFS; content may still exist on other nodes"} return {"deleted": True}