2

I have a Flask view like the below which generates several CSV files and puts them in a zip archive to send to the user.

@route('/download/<int:some_value>')
def download(self, some_value):
    """Return a ZIP archive with several CSVs in"""
    # ensure the thing exists
    at = (MyModel.SomeModel
                 .query
                 .filter((MyModel.SomeModel
                                 .some_primary_key) == some_value)
                 .first_or_404())
    # what queries do we need to run?
    queries = cascade_export(at)
    # prepare a zip
    out = BytesIO()
    with zipfile.ZipFile(out, 'w') as zf:
        # run each query
        for tn, q in queries.items():
            # make the query
            conn = db.engine.connect()
            r = conn.execute(q.query, **q.params)
            conn.close()
            # map the col names
            cols = [c.name for c in r.cursor.description]
            col_map = {
                c.name: c.key
                for c in q.model.__table__.columns
            }
            col_order = [col_map[c] for c in cols]
            # put it into a csv in memory
            f = StringIO()
            writer = csv.DictWriter(f, fieldnames=col_order)
            for row in r:
                writer.writerow({
                    k: v
                    for k, v in zip(col_order, row)
                })
            # write it into the zip
            f.seek(0)
            zf.writestr('{0}.csv'.format(q.model.__name__), f.read())

    out.seek(0)

    fn = 'export-{0}-{1}.zip'.format(
        at.some_name,
        datetime.datetime.now().strftime('%d-%m-%Y-%H-%M-%S')
    )
    return send_file(out,
                     attachment_filename=fn,
                     as_attachment=True,
                     cache_timeout=0)

In my test, where r is a response object, both the following pass:

assert r.status_code == 200
zf = zipfile.ZipFile(io.BytesIO(r.data))
assert zf.testzip() is None

However, when I try and open the file in ubuntu, I get the following:

Archive:  ../downloads/export-Something-20-04-2018-11-59-04.zip
warning [../downloads/export-Something-20-04-2018-11-59-04.zip]:  300 extra bytes at beginning or within zipfile
  (attempting to process anyway)
error [../downloads/export-Something-20-04-2018-11-59-04.zip]:  start of central directory not found;
  zipfile corrupt.
  (please check that you have transferred or created the zipfile in the
  appropriate BINARY mode and that you have compiled UnZip properly)

Any thoughts/help on this much appreciated.

Dom Weldon
  • 1,728
  • 1
  • 12
  • 24
  • 1
    Try writing in binary mode: `with zipfile.ZipFile(out, 'wb') as zf:` – Colin Ricardo Apr 20 '18 at 11:11
  • That gives me `RuntimeError: ZipFile requires mode 'r', 'w', 'x', or 'a'` – Dom Weldon Apr 20 '18 at 11:12
  • Are you using `requests` on the client? – jdehesa Apr 20 '18 at 11:20
  • @jdehesa - I'm using the flask test client (`with app.test_client() as client:`) and python seems able to open that file fine, but when I run it in my browser and download the file it's corrupted. – Dom Weldon Apr 20 '18 at 11:22
  • If I download the file in my browser, and then try and open it, I get `zipfile.BadZipFile: Bad magic number for central directory` – Dom Weldon Apr 20 '18 at 11:32
  • I don't think this should help, but just in case you could try passing a `mimetype` to `send_file` with something like `application/zip, application/octet-stream'` ([see here](https://stackoverflow.com/questions/6977544/rar-zip-files-mime-type)). – jdehesa Apr 20 '18 at 11:40
  • Having run a few more tests, I think this is a front-end error, to do with the way the file is passed from axios and saved. Seems the zip works fine, apologies for the confusion! – Dom Weldon Apr 20 '18 at 11:49

1 Answers1

1

The problem was not with Python, but rather with the way that the file was being downloaded. As is clear, if the python test is passing, the zip file is probably legit! If you're in the unlikely situation of finding yourself with the same error, ensure your HTTP library is expecting the right kind of response, foraxios, this means setting responseType to 'bytearray'.

Dom Weldon
  • 1,728
  • 1
  • 12
  • 24