Coverage for opt/mealie/lib/python3.12/site-packages/mealie/routes/recipe/exports.py: 67%

41 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-11-25 17:29 +0000

1from shutil import rmtree 1a

2from zipfile import ZipFile 1a

3 

4from fastapi import ( 1a

5 HTTPException, 

6) 

7from starlette.background import BackgroundTask 1a

8from starlette.responses import FileResponse 1a

9 

10from mealie.core.dependencies import ( 1a

11 get_temporary_path, 

12 get_temporary_zip_path, 

13 validate_recipe_token, 

14) 

15from mealie.core.security import create_recipe_slug_token 1a

16from mealie.routes._base import controller 1a

17from mealie.routes._base.routers import UserAPIRouter 1a

18from mealie.schema.recipe import Recipe, RecipeImageTypes 1a

19from mealie.schema.recipe.request_helpers import ( 1a

20 RecipeZipTokenResponse, 

21) 

22from mealie.services.recipe.template_service import TemplateService 1a

23 

24from ._base import BaseRecipeController, FormatResponse 1a

25 

26router = UserAPIRouter(prefix="/recipes") 1a

27 

28 

29@controller(router) 1a

30class RecipeExportController(BaseRecipeController): 1a

31 # ================================================================================================================== 

32 # Export Operations 

33 

34 @router.get("/exports", response_model=FormatResponse) 1a

35 def get_recipe_formats_and_templates(self): 1a

36 return TemplateService().templates 

37 

38 @router.post("/{slug}/exports", response_model=RecipeZipTokenResponse) 1a

39 def get_recipe_zip_token(self, slug: str): 1a

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)) 

42 

43 @router.get("/{slug}/exports", response_class=FileResponse) 1a

44 def get_recipe_as_format(self, slug: str, template_name: str): 1a

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. 

50 

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)) 

56 

57 @router.get("/{slug}/exports/zip") 1a

58 def get_recipe_as_zip(self, slug: str, token: str): 1a

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) 

62 

63 if validated_slug != slug: 

64 raise HTTPException(status_code=400, detail="Invalid Slug") 

65 

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()) 

70 

71 if image_asset.is_file(): 

72 myzip.write(image_asset, arcname=image_asset.name) 

73 

74 return FileResponse( 

75 temp_path, filename=f"{recipe.slug}.zip", background=BackgroundTask(temp_path.unlink, missing_ok=True) 

76 )