[Django]-Serving dynamically generated ZIP archives in Django

49đź‘Ť

âś…

The solution is as follows.

Use Python module zipfile to create zip archive, but as the file specify StringIO object (ZipFile constructor requires file-like object). Add files you want to compress. Then in your Django application return the content of StringIO object in HttpResponse with mimetype set to application/x-zip-compressed (or at least application/octet-stream). If you want, you can set content-disposition header, but this should not be really required.

But beware, creating zip archives on each request is bad idea and this may kill your server (not counting timeouts if the archives are large). Performance-wise approach is to cache generated output somewhere in filesystem and regenerate it only if source files have changed. Even better idea is to prepare archives in advance (eg. by cron job) and have your web server serving them as usual statics.

👤zgoda

46đź‘Ť

Here’s a Django view to do this:

import os
import zipfile
import StringIO

from django.http import HttpResponse


def getfiles(request):
    # Files (local path) to put in the .zip
    # FIXME: Change this (get paths from DB etc)
    filenames = ["/tmp/file1.txt", "/tmp/file2.txt"]

    # Folder name in ZIP archive which contains the above files
    # E.g [thearchive.zip]/somefiles/file2.txt
    # FIXME: Set this to something better
    zip_subdir = "somefiles"
    zip_filename = "%s.zip" % zip_subdir

    # Open StringIO to grab in-memory ZIP contents
    s = StringIO.StringIO()

    # The zip compressor
    zf = zipfile.ZipFile(s, "w")

    for fpath in filenames:
        # Calculate path for file in zip
        fdir, fname = os.path.split(fpath)
        zip_path = os.path.join(zip_subdir, fname)

        # Add file, at correct path
        zf.write(fpath, zip_path)

    # Must close zip for all contents to be written
    zf.close()

    # Grab ZIP file from in-memory, make response with correct MIME-type
    resp = HttpResponse(s.getvalue(), mimetype = "application/x-zip-compressed")
    # ..and correct content-disposition
    resp['Content-Disposition'] = 'attachment; filename=%s' % zip_filename

    return resp
👤dbr

36đź‘Ť

Many answers here suggest to use a StringIO or BytesIO buffer. However this is not needed as HttpResponse is already a file-like object:

response = HttpResponse(content_type='application/zip')
zip_file = zipfile.ZipFile(response, 'w')
for filename in filenames:
    zip_file.write(filename)
response['Content-Disposition'] = 'attachment; filename={}'.format(zipfile_name)
return response

Note that you should not call zip_file.close() as the open "file" is response and we definitely don’t want to close it.

11đź‘Ť

I used Django 2.0 and Python 3.6.

import zipfile
import os
from io import BytesIO

def download_zip_file(request):
    filelist = ["path/to/file-11.txt", "path/to/file-22.txt"]

    byte_data = BytesIO()
    zip_file = zipfile.ZipFile(byte_data, "w")

    for file in filelist:
        filename = os.path.basename(os.path.normpath(file))
        zip_file.write(file, filename)
    zip_file.close()

    response = HttpResponse(byte_data.getvalue(), content_type='application/zip')
    response['Content-Disposition'] = 'attachment; filename=files.zip'

    # Print list files in zip_file
    zip_file.printdir()

    return response
👤Pasha M

8đź‘Ť

For python3 i use the io.ByteIO since StringIO is deprecated to achieve this. Hope it helps.

import io

def my_downloadable_zip(request):
    zip_io = io.BytesIO()
    with zipfile.ZipFile(zip_io, mode='w', compression=zipfile.ZIP_DEFLATED) as backup_zip:
        backup_zip.write('file_name_loc_to_zip') # u can also make use of list of filename location
                                                 # and do some iteration over it
     response = HttpResponse(zip_io.getvalue(), content_type='application/x-zip-compressed')
     response['Content-Disposition'] = 'attachment; filename=%s' % 'your_zipfilename' + ".zip"
     response['Content-Length'] = zip_io.tell()
     return response
👤pitaside

6đź‘Ť

Django doesn’t directly handle the generation of dynamic content (specifically Zip files). That work would be done by Python’s standard library. You can take a look at how to dynamically create a Zip file in Python here.

If you’re worried about it slowing down your server you can cache the requests if you expect to have many of the same requests. You can use Django’s cache framework to help you with that.

Overall, zipping files can be CPU intensive but Django shouldn’t be any slower than another Python web framework.

👤Cristian

5đź‘Ť

Shameless plug: you can use django-zipview for the same purpose.

After a pip install django-zipview:

from zipview.views import BaseZipView

from reviews import Review


class CommentsArchiveView(BaseZipView):
    """Download at once all comments for a review."""

    def get_files(self):
        document_key = self.kwargs.get('document_key')
        reviews = Review.objects \
            .filter(document__document_key=document_key) \
            .exclude(comments__isnull=True)

        return [review.comments.file for review in reviews if review.comments.name]
👤Thibault J

2đź‘Ť

I suggest to use separate model for storing those temp zip files. You can create zip on-fly, save to model with filefield and finally send url to user.

Advantages:

  • Serving static zip files with django media mechanism (like usual uploads).
  • Ability to cleanup stale zip files by regular cron script execution (which can use date field from zip file model).
👤carefulweb

0đź‘Ť

A lot of contributions were made to the topic already, but since I came across this thread when I first researched this problem, I thought I’d add my own two cents.

Integrating your own zip creation is probably not as robust and optimized as web-server-level solutions. At the same time, we’re using Nginx and it doesn’t come with a module out of the box.

You can, however, compile Nginx with the mod_zip module (see here for a docker image with the latest stable Nginx version, and an alpine base making it smaller than the default Nginx image). This adds the zip stream capabilities.

Then Django just needs to serve a list of files to zip, all done!
It is a little more reusable to use a library for this file list response, and django-zip-stream offers just that.

Sadly it never really worked for me, so I started a fork with fixes and improvements.

You can use it in a few lines:

def download_view(request, name=""):
    from django_zip_stream.responses import FolderZipResponse
    path = settings.STATIC_ROOT
    path = os.path.join(path, name)

    return FolderZipResponse(path)

You need a way to have Nginx serve all files that you want to archive, but that’s it.

👤yspreen

-1đź‘Ť

Can’t you just write a link to a “zip server” or whatnot? Why does the zip archive itself need to be served from Django? A 90’s era CGI script to generate a zip and spit it to stdout is really all that’s required here, at least as far as I can see.

👤Andy Ross

Leave a comment