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

[BUG] - Cannot Download Recipe(s) #3095

Open
6 tasks done
tumtumsback opened this issue Feb 1, 2024 · 7 comments · May be fixed by #3579
Open
6 tasks done

[BUG] - Cannot Download Recipe(s) #3095

tumtumsback opened this issue Feb 1, 2024 · 7 comments · May be fixed by #3579
Labels
bug: confirmed bug Something isn't working

Comments

@tumtumsback
Copy link

First Check

  • This is not a feature request.
  • I added a very descriptive title to this issue (title field is above this).
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Mealie documentation, with the integrated search.
  • I already read the docs and didn't find an answer.
  • This issue can be replicated on the demo site (https://demo.mealie.io/).

What is the issue you are experiencing?

When I click on 'Download' on a recipe, it does nothing. I wanted to download a Mealie recipe, and give the ZIP to a friend, so that they could import the ZIP into their Mealie, but it just won't download.

Steps to Reproduce

  1. Open recipe
  2. Click ... > Download
  3. Does nothing

Please provide relevant logs

INFO: 192.168.29.230:59323 - "POST /api/recipes/shrimp-fettuccine-alfredo/exports HTTP/1.1" 200 OK
INFO: 192.168.29.230:59323 - "GET /api/recipes/shrimp-fettuccine-alfredo/exports/zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbHVnIjoic2hyaW1wLWZldHR1Y2NpbmUtYWxmcmVkbyIsImV4cCI6MTcwNjgwOTkxNX0.sLcmPW4e1l60K2LSy1gEqEUCvdnFx4I-x7OuhsKTDK0 HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/responses.py", line 323, in call
stat_result = await anyio.to_thread.run_sync(os.stat, self.path)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/anyio/to_thread.py", line 33, in run_sync
return await get_asynclib().run_sync_in_worker_thread(
File "/opt/pysetup/.venv/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 877, in run_sync_in_worker_thread
return await future
File "/opt/pysetup/.venv/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 807, in run
result = context.run(func, *args)
FileNotFoundError: [Errno 2] No such file or directory: '/app/data/.temp/my_zip_archive.zip'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/opt/pysetup/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "/opt/pysetup/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in call
return await self.app(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in call
await super().call(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/applications.py", line 123, in call
await self.middleware_stack(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 186, in call
raise exc
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 164, in call
await self.app(scope, receive, _send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/middleware/gzip.py", line 24, in call
await responder(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/middleware/gzip.py", line 44, in call
await self.app(scope, receive, self.send_with_gzip)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 62, in call
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/routing.py", line 762, in call
await self.middleware_stack(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/routing.py", line 782, in app
await route.handle(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/routing.py", line 297, in handle
await self.app(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/routing.py", line 77, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/routing.py", line 75, in app
await response(scope, receive, send)
File "/opt/pysetup/.venv/lib/python3.10/site-packages/starlette/responses.py", line 326, in call
raise RuntimeError(f"File at path {self.path} does not exist.")
RuntimeError: File at path /app/data/.temp/my_zip_archive.zip does not exist.

Mealie Version

Details
Version: v1.1.0
Build:
Application Mode: Production
Demo Status: Not Demo
API Port: 9000
API Docs: Enabled
Database Type: sqlite
Recipe Scraper Version: 14.53.0

Checks
Secure Site: Yes
Server Side Base URL: Yes
LDAP Ready: No
Email Configured: No
Docker Volumes: error

Deployment

Docker (Linux)

Additional Deployment Details

No response

@tumtumsback tumtumsback added bug Something isn't working triage labels Feb 1, 2024
@Kuchenpirat
Copy link
Collaborator

Can confirm, download seems to be broken

@tumtumsback in the meantime before we issue a fix for that. If your mealie instance is public your friend should be able to scrape your recipe of your mealie with his mealie instance. If not public, i am pretty sure this should work with share links as well

@silentnoodle3
Copy link

Thanks for confirming so quick, you guys are amazing. @Kuchenpirat I can confirm that using a share link from one Mealie instance and importing on another worked - the only thing that did not scrape and come over was the image file. I will watch this issue and use the workaround for now.

@boc-the-git
Copy link
Collaborator

Looked into this briefly.. Definitely don't have the full picture in my head as yet.

The path /app/data/.temp/my_zip_archive.zip is coming from

temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")

Which is almost certainly getting called from here:

@router_exports.get("/{slug}/exports/zip")
def get_recipe_as_zip(self, slug: str, token: str, temp_path=Depends(temporary_zip_path)):
"""Get a Recipe and It's Original Image as a Zip File"""
slug = validate_recipe_token(token)
if slug != slug:
raise HTTPException(status_code=400, detail="Invalid Slug")
recipe: Recipe = self.mixins.get_one(slug)
image_asset = recipe.image_dir.joinpath(RecipeImageTypes.original.value)
with ZipFile(temp_path, "w") as myzip:
myzip.writestr(f"{slug}.json", recipe.json())
if image_asset.is_file():
myzip.write(image_asset, arcname=image_asset.name)
return FileResponse(temp_path, filename=f"{slug}.zip")

I haven't looked into how all the 3rd party libraries operate, though I note the error log seems to be using starlette a lot.

@boc-the-git
Copy link
Collaborator

Possibly of interest.. in a dev container, I get this CORS error as below. I don't get that on my production instance. It's likely not relevant, but just dumping here as an FYI.

image

@boc-the-git
Copy link
Collaborator

boc-the-git commented Feb 4, 2024

Last thing I'll add for now..

mealie    | INFO:     xxx.xx.xxx.xx:0 - "POST /api/recipes/one-pan-baked-butter-chicken/exports HTTP/1.1" 200 OK
mealie    | INFO: 04-Feb-24 21:48:52    temp_path=/app/data/.temp/my_zip_archive.zip, filename=one-pan-baked-butter-chicken.zip
mealie    | INFO:     xxx.xx.xxx.xx:0 - "GET /api/recipes/one-pan-baked-butter-chicken/exports/zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzbHVnIjoib25lLXBhbi1iYWtlZC1idXR0ZXItY2hpY2tlbiIsImV4cCI6MTcwNzA0NTUzMX0.AYcSWKhSYUCTMGkzzsoyWUyOrhJCrQVOT5ubZrC86lk HTTP/1.1" 500 Internal Server Error
<error message seen above continues from here>

This is an output of the variables immediately before this line.. I'm assuming it could be relevant/useful in some way.

return FileResponse(temp_path, filename=f"{slug}.zip")

Need to look into the FileResponse call, and also whether the write methods of the ZipFile class have actually done something.

@nephlm
Copy link

nephlm commented May 1, 2024

I did a bit of debugging on this.

def get_recipe_as_zip(self, slug: str, token: str, temp_path=Depends(temporary_zip_path)):

Part one of the issue is temporary_zip_path which is defined as:

async def temporary_zip_path() -> AsyncGenerator[Path, None]:
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
try:
yield temp_path
finally:
temp_path.unlink(missing_ok=True)

Which means when get_recipe_as_zip goes out of scope, the file that was created is deleted. If you dig into FileResponse you'll see that it doesn't actually return the file before get_recipe_as_zip exits.

The FileResponse constructor doesn't actually read the file, but only sets the path and filename as an instancevar. The FileRespnse instance is returned to the code which called get_recipe_as_zip, which is essentially the starlette router. The router will eventually go on to call FileResponse.__call__(), but by then get_recipe_as_zip has exited and temporary_zip_path has unlinked the zip file.

I did some hacking and can empirically say that the file exists during FileResponse.__init__(), but not during FileResponse.__call__(). I think it's highly likely that in between those two events get_recipe_as_zip() exits which causes the zip file to be unlinked.

We're going to need to find some other way to clean up the zip file. I expect we're going to have to lean on whatever causes data exports to expire, but I just started looking at the code, so I don't know what that is yet.

@nephlm
Copy link

nephlm commented May 8, 2024

Both temporary_zip_path and temporary_file in dependencies.py use this mechanism, but temporary_file doesn't appear to be used by anything.

temporary_zip_file is used by:

  • GET /api/recipes/{slug}/exports/zip
    • mealie/routes/recipe/recipe_crud_routes.py.RecipeExportController.get_recipe_as_zip()
    • This was confirmed and the genesis of this bug.
  • POST /api/groups/migrations
    • mealie/routes/groups/controller_migrations.py.GroupMigrationController.start_data_migration()
  • POST /api/recipes/bulk-actions/export
    • mealie/routes/recipe/bulk_actions.py.RecipeBulkActionController.bulk_export_recipes()
  • POST /api/recipes/create-from-zip
    • mealie/routes/recipe/recipe_crud_routes.py.RecipeController.create_recipe_from_zip()

The other three endpoints use temporary_zip_file in a self contained way so they don't seem to be impacted when the path is unlinked, so the issue seems to be constrained to GET /api/recipes/{slug}/exports/zip.

The easiest thing would be to create a version of temporary_zip_file that doesn't unlink and then pass a background task into the FileResponse that unlinks the file, but that will leave a brief time between when the zip file is created and when FileResponse.__call__() is completed during which the .zip file won't be cleaned up if something killed the system during that moment, but depending on how hard the system is killed, that is always true.

Refs:
tiangolo/fastapi#7148
https://stackoverflow.com/questions/64716495/how-to-delete-the-file-after-a-return-fileresponsefile-path
https://www.starlette.io/background/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug: confirmed bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants