diff --git a/django_downloadview/response.py b/django_downloadview/response.py new file mode 100644 index 0000000..592f41d --- /dev/null +++ b/django_downloadview/response.py @@ -0,0 +1,52 @@ +"""HttpResponse subclasses.""" +from django.http import HttpResponse + + +class DownloadResponse(HttpResponse): + """File download response.""" + def __init__(self, content, content_type, content_length, basename, + status=200, content_encoding=None, expires=None, + filename=None): + """Constructor. + + It differs a bit from HttpResponse constructor. + + Required arguments: + + * ``content`` is supposed to be an iterable that can read the file. + Consider :py:class:`wsgiref.util.FileWrapper`` as a good candidate. + + * ``content_type`` contains mime-type and charset of the file. + It is used as "Content-Type" header. + + * ``content_length`` is the size, in bytes, of the file. + It is used as "Content-Length" header. + + * ``basename`` is the client-side name of the file ("save as" name). + It is used in "Content-Disposition" header. + + Optional arguments: + + * ``status`` is HTTP status code. + + * ``content_encoding`` is used for "Content-Encoding" header. + + * ``expires`` is a datetime. + It is used to set the "Expires" header. + + * ``filename`` is the server-side name of the file. + It may be used by decorators or middlewares to delegate the actual + streaming to a more efficient server (i.e. Nginx, Lighttpd...). + + """ + super(DownloadResponse, self).__init__(content=content, status=status, + content_type=content_type) + self.filename = filename + self.basename = basename + self['Content-Length'] = content_length + if content_encoding: + self['Content-Encoding'] = content_encoding + self.expires = expires + if expires: + self['Expires'] = expires + self['Content-Disposition'] = 'attachment; filename=%s' % basename diff --git a/django_downloadview/views.py b/django_downloadview/views.py index bd2b4c7..c5419bc 100644 --- a/django_downloadview/views.py +++ b/django_downloadview/views.py @@ -6,11 +6,13 @@ from wsgiref.util import FileWrapper from django.conf import settings from django.core.files import File from django.core.files.storage import DefaultStorage -from django.http import Http404, HttpResponse, HttpResponseNotModified +from django.http import Http404, HttpResponseNotModified from django.views.generic.base import View from django.views.generic.detail import BaseDetailView from django.views.static import was_modified_since +from django_downloadview.response import DownloadResponse + class DownloadMixin(object): """Placeholders and base implementation to create file download views. @@ -27,7 +29,7 @@ class DownloadMixin(object): """ #: Response class to be used in render_to_response(). - response_class = HttpResponse + response_class = DownloadResponse def get_file(self): """Return a django.core.files.File object, which is to be served.""" @@ -130,13 +132,16 @@ class DownloadMixin(object): basename = self.get_basename() encoding = self.get_encoding() wrapper = self.get_file_wrapper() - response = self.response_class(wrapper, content_type=content_type) - if encoding: - response['Content-Encoding'] = encoding - response['Content-Length'] = size - # Do not call fsock.close() as HttpResponse needs it open. - # Garbage collector will close it. - response['Content-Disposition'] = 'attachment; filename=%s' % basename + response = self.response_class(content=wrapper, + content_type=content_type, + content_length=size, + filename=filename, + basename=basename, + content_encoding=encoding, + expires=None) + # Do not close the file as response class may need it open: the wrapper + # is an iterator on the content of the file. + # Garbage collector will close the file. return response @@ -172,7 +177,7 @@ class DownloadView(DownloadMixin, View): self._file = File(open(self.filename)) return self._file except IOError: - raise Http404 + raise Http404() def get(self, request, *args, **kwargs): """Handle GET requests: stream a file."""