@@ -62,38 +80,58 @@
diff --git a/frontend/components/global/BaseDialog.vue b/frontend/components/global/BaseDialog.vue
index 73ecf66ca..468f2df20 100644
--- a/frontend/components/global/BaseDialog.vue
+++ b/frontend/components/global/BaseDialog.vue
@@ -59,7 +59,6 @@
{
return this.requests.post(routes.recipesRecipeSlugImage(slug), { url });
}
+ deleteImage(slug: string) {
+ return this.requests.delete(routes.recipesRecipeSlugImage(slug));
+ }
+
async testCreateOneUrl(url: string, useOpenAI = false) {
return await this.requests.post(routes.recipesTestScrapeUrl, { url, useOpenAI });
}
diff --git a/mealie/lang/messages/en-US.json b/mealie/lang/messages/en-US.json
index 1f6abf614..cf38657e0 100644
--- a/mealie/lang/messages/en-US.json
+++ b/mealie/lang/messages/en-US.json
@@ -5,6 +5,7 @@
"recipe": {
"unique-name-error": "Recipe names must be unique",
"recipe-created": "Recipe Created",
+ "recipe-image-deleted": "Recipe image deleted",
"recipe-defaults": {
"ingredient-note": "1 Cup Flour",
"step-text": "Recipe steps as well as other fields in the recipe page support markdown syntax.\n\n**Add a link**\n\n[My Link](https://demo.mealie.io)\n"
diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py
index 354efa413..a9982553d 100644
--- a/mealie/repos/repository_recipes.py
+++ b/mealie/repos/repository_recipes.py
@@ -154,6 +154,11 @@ class RepositoryRecipes(HouseholdRepositoryGeneric[Recipe, RecipeModel]):
return entry.image
+ def delete_image(self, slug: str, _: str | None = None):
+ entry: RecipeModel = self._query_one(match_value=slug)
+ entry.image = None
+ self.session.commit()
+
def count_uncategorized(self, count=True, override_schema=None):
return self._count_attribute(
attribute_name=RecipeModel.recipe_category,
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index 5232bf339..a65ac305f 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -46,7 +46,7 @@ from mealie.schema.recipe.request_helpers import (
)
from mealie.schema.response import PaginationBase, PaginationQuery
from mealie.schema.response.pagination import RecipeSearchQuery
-from mealie.schema.response.responses import ErrorResponse
+from mealie.schema.response.responses import ErrorResponse, SuccessResponse
from mealie.services import urls
from mealie.services.event_bus_service.event_types import (
EventOperation,
@@ -543,6 +543,15 @@ class RecipeController(BaseRecipeController):
self.handle_exceptions(e)
return None
+ @router.delete("/{slug}/image", tags=["Recipe: Images and Assets"])
+ def delete_recipe_image(self, slug: str):
+ try:
+ self.service.delete_recipe_image(slug)
+ return SuccessResponse.respond(message=self.t("recipe.recipe-image-deleted"))
+ except Exception as e:
+ self.handle_exceptions(e)
+ return None
+
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
def upload_recipe_asset(
self,
diff --git a/mealie/services/recipe/recipe_data_service.py b/mealie/services/recipe/recipe_data_service.py
index f92270aff..be4611bfa 100644
--- a/mealie/services/recipe/recipe_data_service.py
+++ b/mealie/services/recipe/recipe_data_service.py
@@ -8,6 +8,7 @@ from pydantic import UUID4
from mealie.pkgs import img, safehttp
from mealie.pkgs.safehttp.transport import AsyncSafeTransport
from mealie.schema.recipe.recipe import Recipe
+from mealie.schema.recipe.recipe_image_types import RecipeImageTypes
from mealie.services._base_service import BaseService
from mealie.services.scraper.user_agents_manager import get_user_agents_manager
@@ -104,6 +105,14 @@ class RecipeDataService(BaseService):
return image_path
+ def delete_image(self, image_dir: Path | None = None):
+ if not image_dir:
+ image_dir = self.dir_image
+
+ for img_type in RecipeImageTypes:
+ image_path = image_dir.joinpath(img_type.value)
+ image_path.unlink(missing_ok=True)
+
async def scrape_image(self, image_url: str | dict[str, str] | list[str]) -> None:
self.logger.info(f"Image URL: {image_url}")
user_agent = get_user_agents_manager().user_agents[0]
diff --git a/mealie/services/recipe/recipe_service.py b/mealie/services/recipe/recipe_service.py
index 381793eb3..43782725f 100644
--- a/mealie/services/recipe/recipe_service.py
+++ b/mealie/services/recipe/recipe_service.py
@@ -418,6 +418,17 @@ class RecipeService(RecipeServiceBase):
return self.group_recipes.update_image(slug, extension)
+ def delete_recipe_image(self, slug: str) -> None:
+ recipe = self.get_one(slug)
+ if not self.can_update(recipe):
+ raise exceptions.PermissionDenied("You do not have permission to edit this recipe.")
+
+ data_service = RecipeDataService(recipe.id)
+ data_service.delete_image()
+
+ self.group_recipes.delete_image(slug)
+ return None
+
def patch_one(self, slug_or_id: str | UUID, patch_data: Recipe) -> Recipe:
recipe: Recipe = self._pre_update_check(slug_or_id, patch_data)
diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_owner.py b/tests/integration_tests/user_recipe_tests/test_recipe_owner.py
index 3bfe0191a..0a355dac6 100644
--- a/tests/integration_tests/user_recipe_tests/test_recipe_owner.py
+++ b/tests/integration_tests/user_recipe_tests/test_recipe_owner.py
@@ -6,6 +6,7 @@ from fastapi.testclient import TestClient
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.recipe.recipe import Recipe
from mealie.schema.recipe.recipe_settings import RecipeSettings
+from mealie.services.recipe.recipe_service import RecipeDataService
from tests import data
from tests.utils import api_routes
from tests.utils.factories import random_string
@@ -211,3 +212,50 @@ def test_user_can_update_recipe_image(api_client: TestClient, unique_user: TestU
response = api_client.get(api_routes.recipes_slug(recipe_id), headers=unique_user.token)
recipe_respons = response.json()
assert recipe_respons["image"] is not None
+
+ service = RecipeDataService(recipe_json.id)
+ assert service.dir_image.exists() and any(f.is_file() for f in service.dir_image.iterdir())
+
+
+def test_user_can_delete_recipe_image(api_client: TestClient, unique_user: TestUser):
+ data_payload = {"extension": "jpg"}
+ file_payload = {"image": data.images_test_image_1.read_bytes()}
+
+ household = unique_user.repos.households.get_one(unique_user.household_id)
+ assert household and household.preferences
+ household.preferences.private_household = True
+ household.preferences.lock_recipe_edits_from_other_households = True
+ unique_user.repos.household_preferences.update(household.id, household.preferences)
+
+ response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
+ assert response.status_code == 201
+ recipe_json = unique_user.repos.recipes.get_one(response.json())
+ assert recipe_json and recipe_json.id
+ assert recipe_json.image is None
+ recipe_id = str(recipe_json.id)
+
+ response = api_client.put(
+ api_routes.recipes_slug_image(recipe_json.slug),
+ data=data_payload,
+ files=file_payload,
+ headers=unique_user.token,
+ )
+ assert response.status_code == 200
+
+ response = api_client.get(api_routes.recipes_slug(recipe_id), headers=unique_user.token)
+ recipe_respons = response.json()
+ assert recipe_respons["image"] is not None
+
+ service = RecipeDataService(recipe_json.id)
+ assert service.dir_image.exists() and any(f.is_file() for f in service.dir_image.iterdir())
+
+ response = api_client.delete(
+ api_routes.recipes_slug_image(recipe_json.slug),
+ headers=unique_user.token,
+ )
+ assert response.status_code == 200
+
+ response = api_client.get(api_routes.recipes_slug(recipe_id), headers=unique_user.token)
+ recipe_respons = response.json()
+ assert recipe_respons["image"] is None
+ assert not service.dir_image.exists() or not any(f.is_file() for f in service.dir_image.iterdir())