Coverage for opt/mealie/lib/python3.12/site-packages/mealie/routes/recipe/exports.py: 76%
41 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 03:04 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 03:04 +0000
1from shutil import rmtree
2from zipfile import ZipFile
4from fastapi import (
5 HTTPException,
6)
7from starlette.background import BackgroundTask
8from starlette.responses import FileResponse
10from mealie.core.dependencies import (
11 get_temporary_path,
12 get_temporary_zip_path,
13 validate_recipe_token,
14)
15from mealie.core.security import create_recipe_slug_token
16from mealie.routes._base import controller
17from mealie.routes._base.routers import UserAPIRouter
18from mealie.schema.recipe import Recipe, RecipeImageTypes
19from mealie.schema.recipe.request_helpers import (
20 RecipeZipTokenResponse,
21)
22from mealie.services.recipe.template_service import TemplateService
24from ._base import BaseRecipeController, FormatResponse
26router = UserAPIRouter(prefix="/recipes")
29@controller(router)
30class RecipeExportController(BaseRecipeController):
31 # ==================================================================================================================
32 # Export Operations
34 @router.get("/exports", response_model=FormatResponse)
35 def get_recipe_formats_and_templates(self):
36 return TemplateService().templates
38 @router.post("/{slug}/exports", response_model=RecipeZipTokenResponse)
39 def get_recipe_zip_token(self, slug: str):
40 """Generates a recipe zip token to be used to download a recipe as a zip file"""
41 return RecipeZipTokenResponse(token=create_recipe_slug_token(slug))
43 @router.get("/{slug}/exports", response_class=FileResponse)
44 def get_recipe_as_format(self, slug: str, template_name: str):
45 """
46 ## Parameters
47 `template_name`: The name of the template to use to use in the exports listed. Template type will automatically
48 be set on the backend. Because of this, it's important that your templates have unique names. See available
49 names and formats in the /api/recipes/exports endpoint.
51 """
52 with get_temporary_path(auto_unlink=False) as temp_path:
53 recipe = self.mixins.get_one(slug)
54 file = self.service.render_template(recipe, temp_path, template_name)
55 return FileResponse(file, background=BackgroundTask(rmtree, temp_path))
57 @router.get("/{slug}/exports/zip")
58 def get_recipe_as_zip(self, slug: str, token: str):
59 """Get a Recipe and Its Original Image as a Zip File"""
60 with get_temporary_zip_path(auto_unlink=False) as temp_path:
61 validated_slug = validate_recipe_token(token)
63 if validated_slug != slug: 63 ↛ 66line 63 didn't jump to line 66 because the condition on line 63 was always true
64 raise HTTPException(status_code=400, detail="Invalid Slug")
66 recipe: Recipe = self.mixins.get_one(validated_slug)
67 image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
68 with ZipFile(temp_path, "w") as myzip:
69 myzip.writestr(f"{slug}.json", recipe.model_dump_json())
71 if image_asset.is_file():
72 myzip.write(image_asset, arcname=image_asset.name)
74 return FileResponse(
75 temp_path, filename=f"{recipe.slug}.zip", background=BackgroundTask(temp_path.unlink, missing_ok=True)
76 )