Fix recipe ownership tracking via item_types table
- Upload now creates item_types entry linking user to recipe - List queries item_types for user's recipes (not all cached) - Delete removes item_types entry (not the file) - File only deleted when no users own it (garbage collection) This allows multiple users to "own" the same recipe CID. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,45 +96,42 @@ class RecipeService:
|
|||||||
|
|
||||||
async def list_recipes(self, actor_id: Optional[str] = None, offset: int = 0, limit: int = 20) -> List[Recipe]:
|
async def list_recipes(self, actor_id: Optional[str] = None, offset: int = 0, limit: int = 20) -> List[Recipe]:
|
||||||
"""
|
"""
|
||||||
List available recipes for a user.
|
List recipes owned by a user.
|
||||||
|
|
||||||
L1 data is isolated per-user - only shows recipes owned by actor_id.
|
Queries item_types table for user's recipe links.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import database
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
recipes = []
|
recipes = []
|
||||||
|
|
||||||
if hasattr(self.cache, 'list_by_type'):
|
if not actor_id:
|
||||||
items = self.cache.list_by_type('recipe')
|
logger.warning("list_recipes called without actor_id")
|
||||||
logger.info(f"Found {len(items)} recipe CIDs in cache: {items[:5]}...")
|
return []
|
||||||
for cid in items:
|
|
||||||
logger.debug(f"Attempting to get recipe {cid[:16]}...")
|
# Get user's recipe CIDs from item_types
|
||||||
recipe = await self.get_recipe(cid)
|
user_items = await database.get_user_items(actor_id, item_type="recipe", limit=1000)
|
||||||
if recipe and not recipe.get("error"):
|
recipe_cids = [item["cid"] for item in user_items]
|
||||||
# Don't trust owner from recipe content - could be spoofed
|
logger.info(f"Found {len(recipe_cids)} recipe CIDs for user {actor_id}")
|
||||||
# For L1, recipes in cache are visible to all authenticated users
|
|
||||||
# (ownership is tracked via naming service, not recipe content)
|
for cid in recipe_cids:
|
||||||
recipes.append(recipe)
|
recipe = await self.get_recipe(cid)
|
||||||
logger.info(f"Recipe {cid[:16]}... included")
|
if recipe and not recipe.get("error"):
|
||||||
elif recipe and recipe.get("error"):
|
recipes.append(recipe)
|
||||||
logger.warning(f"Recipe {cid[:16]}... has error: {recipe.get('error')}")
|
elif recipe and recipe.get("error"):
|
||||||
else:
|
logger.warning(f"Recipe {cid[:16]}... has error: {recipe.get('error')}")
|
||||||
logger.warning(f"Recipe {cid[:16]}... returned None")
|
|
||||||
else:
|
|
||||||
logger.warning("Cache does not have list_by_type method")
|
|
||||||
|
|
||||||
# Add friendly names
|
# Add friendly names
|
||||||
if actor_id:
|
from .naming_service import get_naming_service
|
||||||
from .naming_service import get_naming_service
|
naming = get_naming_service()
|
||||||
naming = get_naming_service()
|
for recipe in recipes:
|
||||||
for recipe in recipes:
|
recipe_id = recipe.get("recipe_id")
|
||||||
recipe_id = recipe.get("recipe_id")
|
if recipe_id:
|
||||||
if recipe_id:
|
friendly = await naming.get_by_cid(actor_id, recipe_id)
|
||||||
friendly = await naming.get_by_cid(actor_id, recipe_id)
|
if friendly:
|
||||||
if friendly:
|
recipe["friendly_name"] = friendly["friendly_name"]
|
||||||
recipe["friendly_name"] = friendly["friendly_name"]
|
recipe["base_name"] = friendly["base_name"]
|
||||||
recipe["base_name"] = friendly["base_name"]
|
|
||||||
|
|
||||||
# Sort by name
|
# Sort by name
|
||||||
recipes.sort(key=lambda r: r.get("name", ""))
|
recipes.sort(key=lambda r: r.get("name", ""))
|
||||||
@@ -172,11 +169,23 @@ class RecipeService:
|
|||||||
cached, ipfs_cid = self.cache.put(tmp_path, node_type="recipe", move=True)
|
cached, ipfs_cid = self.cache.put(tmp_path, node_type="recipe", move=True)
|
||||||
recipe_id = ipfs_cid or cached.cid # Prefer IPFS CID
|
recipe_id = ipfs_cid or cached.cid # Prefer IPFS CID
|
||||||
|
|
||||||
# Assign friendly name
|
# Track ownership in item_types and assign friendly name
|
||||||
if uploader:
|
if uploader:
|
||||||
|
import database
|
||||||
|
display_name = name or compiled.name or "unnamed-recipe"
|
||||||
|
|
||||||
|
# Create item_types entry (ownership link)
|
||||||
|
await database.save_item_metadata(
|
||||||
|
cid=recipe_id,
|
||||||
|
actor_id=uploader,
|
||||||
|
item_type="recipe",
|
||||||
|
description=description,
|
||||||
|
filename=f"{display_name}.sexp",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assign friendly name
|
||||||
from .naming_service import get_naming_service
|
from .naming_service import get_naming_service
|
||||||
naming = get_naming_service()
|
naming = get_naming_service()
|
||||||
display_name = name or compiled.name or "unnamed-recipe"
|
|
||||||
await naming.assign_name(
|
await naming.assign_name(
|
||||||
cid=recipe_id,
|
cid=recipe_id,
|
||||||
actor_id=uploader,
|
actor_id=uploader,
|
||||||
@@ -191,29 +200,27 @@ class RecipeService:
|
|||||||
|
|
||||||
async def delete_recipe(self, recipe_id: str, actor_id: str = None) -> Tuple[bool, Optional[str]]:
|
async def delete_recipe(self, recipe_id: str, actor_id: str = None) -> Tuple[bool, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Delete a recipe.
|
Remove user's ownership link to a recipe.
|
||||||
|
|
||||||
Note: This only removes from local cache. IPFS copies persist.
|
This removes the item_types entry linking the user to the recipe.
|
||||||
|
The cached file is only deleted if no other users own it.
|
||||||
Returns (success, error_message).
|
Returns (success, error_message).
|
||||||
"""
|
"""
|
||||||
recipe = await self.get_recipe(recipe_id)
|
import database
|
||||||
if not recipe:
|
|
||||||
return False, "Recipe not found"
|
|
||||||
|
|
||||||
# Note: We don't check ownership from recipe content as it could be spoofed.
|
if not actor_id:
|
||||||
# For L1, any authenticated user can delete recipes in the cache.
|
return False, "actor_id required"
|
||||||
# (Ownership tracking should use naming service or cache metadata, not recipe content)
|
|
||||||
|
|
||||||
# Delete from cache
|
# Remove user's ownership link
|
||||||
try:
|
try:
|
||||||
if hasattr(self.cache, 'delete_by_cid'):
|
await database.delete_item_type(recipe_id, actor_id, "recipe")
|
||||||
success, msg = self.cache.delete_by_cid(recipe_id)
|
|
||||||
if not success:
|
# Also remove friendly name
|
||||||
return False, msg
|
await database.delete_friendly_name(actor_id, recipe_id)
|
||||||
else:
|
|
||||||
path = self.cache.get_by_cid(recipe_id)
|
# Try to garbage collect if no one owns it anymore
|
||||||
if path and path.exists():
|
# (delete_cache_item only deletes if no item_types remain)
|
||||||
path.unlink()
|
await database.delete_cache_item(recipe_id)
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user