Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix file not found error with individual recipe export/download. #3579

Merged
merged 8 commits into from
May 20, 2024
8 changes: 6 additions & 2 deletions mealie/core/dependencies/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,13 @@ def validate_recipe_token(token: str | None = None) -> str:
return slug


async def temporary_zip_path() -> AsyncGenerator[Path, None]:
def temporary_zip_path() -> Path:
nephlm marked this conversation as resolved.
Show resolved Hide resolved
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
return app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")


async def unlinking_temporary_zip_path() -> AsyncGenerator[Path, None]:
temp_path = temporary_zip_path()

try:
yield temp_path
Expand Down
4 changes: 2 additions & 2 deletions mealie/routes/groups/controller_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi import Depends, File, Form
from fastapi.datastructures import UploadFile

from mealie.core.dependencies import temporary_zip_path
from mealie.core.dependencies import unlinking_temporary_zip_path
from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.group.group_migration import SupportedMigrations
Expand Down Expand Up @@ -32,7 +32,7 @@ def start_data_migration(
add_migration_tag: bool = Form(False),
migration_type: SupportedMigrations = Form(...),
archive: UploadFile = File(...),
temp_path: Path = Depends(temporary_zip_path),
temp_path: Path = Depends(unlinking_temporary_zip_path),
):
# Save archive to temp_path
with temp_path.open("wb") as buffer:
Expand Down
4 changes: 2 additions & 2 deletions mealie/routes/recipe/bulk_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from fastapi import APIRouter, Depends, HTTPException

from mealie.core.dependencies.dependencies import temporary_zip_path
from mealie.core.dependencies.dependencies import unlinking_temporary_zip_path
from mealie.core.security import create_file_token
from mealie.routes._base import BaseUserController, controller
from mealie.schema.group.group_exports import GroupDataExport
Expand Down Expand Up @@ -44,7 +44,7 @@ def bulk_delete_recipes(self, delete_recipes: DeleteRecipes):
self.service.delete_recipes(delete_recipes.recipes)

@router.post("/export", status_code=202)
def bulk_export_recipes(self, export_recipes: ExportRecipes, temp_path=Depends(temporary_zip_path)):
def bulk_export_recipes(self, export_recipes: ExportRecipes, temp_path=Depends(unlinking_temporary_zip_path)):
self.service.export_recipes(temp_path, export_recipes.recipes)

@router.get("/export/download")
Expand Down
9 changes: 6 additions & 3 deletions mealie/routes/recipe/recipe_crud_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
from fastapi.responses import JSONResponse
from pydantic import UUID4, BaseModel, Field
from slugify import slugify
from starlette.background import BackgroundTask
from starlette.responses import FileResponse

from mealie.core import exceptions
from mealie.core.dependencies import temporary_zip_path
from mealie.core.dependencies import temporary_zip_path, unlinking_temporary_zip_path
from mealie.core.dependencies.dependencies import temporary_dir, validate_recipe_token
from mealie.core.security import create_recipe_slug_token
from mealie.db.models.group.cookbook import CookBook
Expand Down Expand Up @@ -131,7 +132,9 @@
if image_asset.is_file():
myzip.write(image_asset, arcname=image_asset.name)

return FileResponse(temp_path, filename=f"{slug}.zip")
return FileResponse(
temp_path, filename=f"{slug}.zip", background=BackgroundTask(temp_path.unlink, missing_ok=True)
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
)


router = UserAPIRouter(prefix="/recipes", tags=["Recipe: CRUD"], route_class=MealieCrudRoute)
Expand Down Expand Up @@ -219,7 +222,7 @@
return "recipe_scrapers was unable to scrape this URL"

@router.post("/create-from-zip", status_code=201)
def create_recipe_from_zip(self, temp_path=Depends(temporary_zip_path), archive: UploadFile = File(...)):
def create_recipe_from_zip(self, temp_path=Depends(unlinking_temporary_zip_path), archive: UploadFile = File(...)):
"""Create recipe from archive"""
recipe = self.service.create_from_zip(archive, temp_path)
self.publish_event(
Expand Down
31 changes: 31 additions & 0 deletions tests/integration_tests/user_recipe_tests/test_recipe_export_as.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from io import BytesIO
import json
import zipfile

from fastapi.testclient import TestClient

from mealie.core.dependencies.dependencies import temporary_zip_path
from tests.utils import api_routes
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
Expand Down Expand Up @@ -36,6 +41,32 @@ def test_render_jinja_template(api_client: TestClient, unique_user: TestUser) ->
assert f"# {recipe_name}" in response.text


def test_get_recipe_as_zip(api_client: TestClient, unique_user: TestUser) -> None:
# Create Recipe
recipe_name = random_string()
response = api_client.post(api_routes.recipes, json={"name": recipe_name}, headers=unique_user.token)
assert response.status_code == 201
slug = response.json()

# Get zip token
response = api_client.post(api_routes.recipes_slug_exports(slug), headers=unique_user.token)
assert response.status_code == 200
token = response.json()["token"]
assert token

response = api_client.get(api_routes.recipes_slug_exports_zip(slug) + f"?token={token}", headers=unique_user.token)
assert response.status_code == 200

# Verify we clean up.
assert not temporary_zip_path().exists()

# Verify the zip
zip_file = BytesIO(response.content)
with zipfile.ZipFile(zip_file, "r") as zip_fp:
with zip_fp.open(f"{slug}.json") as json_fp:
assert json.loads(json_fp.read())["name"] == recipe_name


# TODO: Allow users to upload templates to their own directory
# def test_upload_template(api_client: TestClient, unique_user: TestUser) -> None:
# assert False
Expand Down
Loading