-1

I am creating a CKAN plugin to allow users to select and download multiple resources (files) from a database server. I created a flask view function with some Python code that should create a zip file with the requested resources, but the creation fails with "no such file or directory" error even though I give the absolute path.

The code is based on this, this, and this post, and to solve the current question I have already checked here and here with not much success.

The code is as follows:

import zipfile
import io

def download_multiple_resources():
    if request.method == "POST":

        memory_file = io.BytesIO()
        with zipfile.ZipFile(memory_file, "w", zipfile.ZIP_STORED) as zf:
            for res in request.form.values():
                zf.write(str(res), 'download.zip')

return send_file(memory_file, mimetype='application/zip', as_attachment=True, attachment_filename='download.zip')

The resources are selected by the user via a form, which posts the request. The form submits the full path of the requested file, so if the user selected to download testfile.txt, in the code above the value of res is something like http://localhost:5000/dataset/6192bb3b-6c4a/resource/dbe39d59-b938/download/testfile.txt. (currently testing on a server created in a docker container on my machine)

Note that copy-pasting the above path in my browser lets me access the file without any issue. However, the zf.write command fails with "no such file or directory" error.

Any idea for what is going wrong?

F. Remonato
  • 248
  • 2
  • 12
  • To anyone downvoting this, you might actually be helpful and write 1) why you downvoted, so I can improve my question if necessary; and 2) maybe your idea on the solution? – F. Remonato Feb 13 '19 at 09:37

2 Answers2

1

Turns out my initial guess was right, and one only needs to use writestr() instead of write(), in addition to reading the file with an http request. Here is the full working code:

import zipfile
import io
import requests
from flask import request, send_file

def download_multiple_resources():
    if request.method == "POST":
        memory_file = io.Bytes.IO()
        with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_STORED) as zf:
            for res in request.form:
                f = requests.get(res, allow_redirects=True)
                zf.writestr('your_chosen_filename', f.content)
        memory_file.seek(0)

    return send_file(memory_file, mimetype='application/zip', as_attachment=True, attachment_filename="download.zip")

Extra note: If you are working on an external server, like me, and use this function to let logged-in users download files, be sure that the credentials of the users are passed to the request.get() call to authorize the download. This in my case has been done using the option headers={'Authorization': user.apiKey}, but this might depend on your framework/application.

F. Remonato
  • 248
  • 2
  • 12
0

You say you want to access the resource by its absolute path, but your example value for res is a URL.

I don't think zipfile.ZipFile.write supports that, since adding a file to a zip archive uses metadata from the filesystem (timestamps, etc). You will need to either access the resource by its local path, or if it's not accessible from the application container, download it to a temporary location first. (The latter case will strip the metadata, of course.)

Christoph Burschka
  • 4,467
  • 3
  • 16
  • 31
  • Thanks for the answer! I don't see the difference between an absolute path and a full URL like that - which is an absolute path on the server - but I am not technical enough to have a better opinion on that. I will try with a local path as you suggest and mark as answer if that works. Downloading a file I have on the server in order to zip it and return it as a download to a user seems a very inefficient approach. – F. Remonato Feb 08 '19 at 10:33