21

I need to create a temporary file to send it, I have tried :

# Create a temporary file --> I think it is ok (file not seen)
temporaryfile = NamedTemporaryFile(delete=False, dir=COMPRESSED_ROOT)

# The path to archive --> It's ok
root_dir = "something"

# Create a compressed file --> It bugs
data = open(f.write(make_archive(f.name, 'zip', root_dir))).read()

# Send the file --> Its ok
response = HttpResponse(data, mimetype='application/zip')
response['Content-Disposition'] = 'attachment; filename="%s"' % unicode(downloadedassignment.name + '.zip')
return response

I don't know at all if it is the good approach..

georgexsh
  • 15,984
  • 2
  • 37
  • 62
nlassaux
  • 2,335
  • 2
  • 21
  • 35

2 Answers2

36

I actually just needed to do something similar and I wanted to avoid file I/O entirely, if possible. Here's what I came up with:

import tempfile
import zipfile

with tempfile.SpooledTemporaryFile() as tmp:
    with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as archive:
        archive.writestr('something.txt', 'Some Content Here')

    # Reset file pointer
    tmp.seek(0)

    # Write file data to response
    return HttpResponse(tmp.read(), mimetype='application/x-zip-compressed')

It uses a SpooledTemporaryFile so it will remain in-memory, unless it exceeds the memory limits. Then, I set this tempory file as the stream for ZipFile to use. The filename passed to writestr is just the filename that the file will have inside the archive, it doesn't have anything to do with the server's filesystem. Then, I just need to rewind the file pointer (seek(0)) after ZipFile had done its thing and dump it to the response.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • sth wrong there - tmp.close (from ``with`` ) is after return, isn't it? – Sławomir Lenart Nov 29 '13 at 16:49
  • Yes, but that's necessary. The response needs to read from `tmp`, which requires the stream to still be open. `with` will clean everything up after the return call. – Chris Pratt Dec 02 '13 at 16:29
  • 1
    Python 2.6 has no `__exit__` attribute for `zipfile.ZipFile` so you can't use `with` unless you wrap it in `contextlib.closing`, see [Making Python 2.7 code run with Python 2.6](http://stackoverflow.com/questions/21268470) – Dave Anderson May 07 '14 at 03:25
  • 4
    `mimetype` param of `HttpResponse` changed to `content_type` since Django 1.4 – JWL Feb 03 '15 at 18:54
  • this worked for me. Important note if not using with, you need to close the zipfile before the seek on the temp – postelrich Jun 12 '15 at 15:59
  • 1
    The code in this answer utilizes `SpooledTemporaryFile` in a way that makes it essentially pointless. The ZipFile is constructed, written to memory **or a file**, then immediately read back out _in its entirety_ as a single string. Memory exhaustion avoided for 15ns until memory gets immediately exhausted. One might be able to utilize a WSGI response object that can stream, e.g. by assigning to [WebOb's `Response.body_file`](https://docs.pylonsproject.org/projects/webob/en/stable/api/response.html#webob.response.Response.body_file). – amcgregor Jan 23 '20 at 19:48
  • I think SpooledTemporaryFile is essentially useless for another reason - per the docs https://docs.python.org/3/library/tempfile.html the default max_size (rollover size) is zero. So, it rolls over to disk right away and the usage here is synonymous with using TemporaryFile(). This could be fixed with e.g. `SpooledTemporaryFile(max_size=1024*1024*2)` for 2Mb. (It's possible I'm misunderstanding something in the docs, but it seems like a bit of a 'gotchya'). – Trevor Gross Dec 30 '21 at 06:25
15

First of all, you don't need to create a NamedTemporaryFile to use make_archive; all you want is a unique filename for the make_archive file to create.

.write doesn't return a filename

To focus on that error: You are assuming that the return value of f.write is a filename you can open; just seek to the start of your file and read instead:

f.write(make_archive(f.name, 'zip', root_dir))
f.seek(0)
data = f.read()

Note that you'll also need to clean up the temporary file you created (you set delete=False):

import os
f.close()
os.unlink(f.name)

Alternatively, just omit the delete keyword to have it default to True again and only close your file afterwards, no need to unlink.

That just wrote the archive filename to a new file..

You are just writing the new archive name to your temporary file. You'd be better off just reading the archive directly:

data = open(make_archive(f.name, 'zip', root_dir), 'rb').read()

Note that now your temporary file isn't being written to at all.

Best way to do this

Avoid creating a NamedTemporaryFile altogether: Use tempfile.mkdtemp() instead, to generate a temporary directory in which to put your archive, then clean that up afterwards:

tmpdir = tempfile.mkdtemp()
try:
    tmparchive = os.path.join(tmpdir, 'archive')

    root_dir = "something"

    data = open(make_archive(tmparchive, 'zip', root_dir), 'rb').read()

finally:
    shutil.rmtree(tmpdir)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • It is nearly the perfection, I have the .zip in return but I don't know how I can delete it because I can't delete the file with `shutil.rmtree(tmpdir)` before the return (or I will have an empty file) and the temporary file doesn't destroy itself... – nlassaux Aug 15 '12 at 11:01
  • @Nico401: But you just read the data, so the archive file is no longer needed. Once the `read()` call is complete you no longer need the files on the disk. – Martijn Pieters Aug 15 '12 at 11:02
  • What is the "root_dir" ? Is that the directory in which you want the archive to be created ? – Michael De Keyser Feb 10 '14 at 15:08
  • @MichaelDeKeyser: it is whatever the OP of the question meant it to be. – Martijn Pieters Feb 10 '14 at 15:41
  • @MartijnPieters I meant what does it represent for make_archive but now I get it. It's the directory which content's going to be zipped. Thanks tho. – Michael De Keyser Feb 10 '14 at 15:52
  • @MartijnPieters I am wondering if I can copy files from different directories into the tempdir? How can I achieve that? Thank you. – Jimmy Lin Jun 03 '14 at 04:04
  • @Jimmy: You mean you want to combine files from different directories into the `make_archive()` call? You can't; it is a utility function in `shutil` that only walks one tree at a time. You'd have to recreate the same functionality using `zipfile` or any of the other supported archive formats. See the [source code](http://hg.python.org/cpython/file/b8655be522d4/Lib/shutil.py#l417). – Martijn Pieters Jun 03 '14 at 08:02