jazzband / django-downloadview

Serve files with Django.
https://django-downloadview.readthedocs.io
Other
374 stars 57 forks source link

VirtualDownloadView with BytesIteratorIO set response header Content-Length to zero #123

Closed bmampaey closed 4 years ago

bmampaey commented 8 years ago

Similarly to the example given in the documentation for VirtualDownloadView for Stream generated content, i tried to make a view that returns a zipfile from a generator. Since zipfiles are binary, I use the BytesIteratorIO instead of the TextIteratorIO to wrap my generator.

The problem is that in that case, the response header Content-Length is set to zero, which causes the browser to not download the file.

Here is an example to demonstrate the problem, with both TextIteratorIO and BytesIteratorIO

from django_downloadview import VirtualDownloadView, VirtualFile, TextIteratorIO, BytesIteratorIO

def text():
    yield u'Hello '
    yield u'world!'
    yield u'\n'

def binary():
    yield b'Hello '
    yield b'world!'
    yield b'\n'

class TextVirtualView(VirtualDownloadView):
    '''Virtual download view that returns text'''
    attachment = True

    def get_file(self):
        """Return wrapper on ``TextIteratorIO`` object."""
        file_obj = TextIteratorIO(text())
        return VirtualFile(file_obj, name='hello-world.txt')

class BinaryVirtualView(VirtualDownloadView):
    '''Virtual download view that returns binary'''
    attachment = True

    def get_file(self):
        """Return wrapper on ``BytesIteratorIO`` object."""
        file_obj = BytesIteratorIO(binary())
        return VirtualFile(file_obj, name='hello-world.bin')

The urls set up (for clarity):

from django.conf.urls import url
from error.views import TextVirtualView, BinaryVirtualView
urlpatterns = [
    url(r'^text_view/$', TextVirtualView.as_view()),
    url(r'^binary_view/$', BinaryVirtualView.as_view()),
]

And here is the result from wget on both views:

bentomac:tmp benjmam$ wget -S http://localhost:8000/text_view/
--2016-06-18 13:20:43--  http://localhost:8000/text_view/
Resolving localhost (localhost)... 127.0.0.1, ::1
Connecting to localhost (localhost)|127.0.0.1|:8000... connected.
HTTP request sent, awaiting response...
  HTTP/1.0 200 OK
  Date: Sat, 18 Jun 2016 11:20:43 GMT
  Server: WSGIServer/0.1 Python/2.7.10
  X-Frame-Options: SAMEORIGIN
  Content-Type: text/plain; charset=utf-8
  Content-Disposition: attachment; filename="hello-world.txt"
Length: unspecified [text/plain]
Saving to: 'index.html'

index.html                    [ <=>                                 ]      13  --.-KB/s   in 0s

2016-06-18 13:20:43 (3.10 MB/s) - 'index.html' saved [13]

bentomac:tmp benjmam$ wget -S http://localhost:8000/binary_view/
--2016-06-18 13:20:56--  http://localhost:8000/binary_view/
Resolving localhost (localhost)... 127.0.0.1, ::1
Connecting to localhost (localhost)|127.0.0.1|:8000... connected.
HTTP request sent, awaiting response...
  HTTP/1.0 200 OK
  Date: Sat, 18 Jun 2016 11:20:56 GMT
  Server: WSGIServer/0.1 Python/2.7.10
  Content-Length: 0  <================= LOOK HERE ============================
  Content-Type: application/octet-stream; charset=utf-8
  X-Frame-Options: SAMEORIGIN
  Content-Disposition: attachment; filename="hello-world.bin"
Length: 0 [application/octet-stream]
Saving to: 'index.html.1'

index.html.1                  [ <=>                                 ]       0  --.-KB/s   in 0s

2016-06-18 13:20:56 (0.00 B/s) - 'index.html.1' saved [0/0]

PS: I used django version 1.9.7and django_downloadview 1.9

bmampaey commented 8 years ago

I believe the problem comes from this piece of code in VirtualFile

    def _get_size(self):
        try:
            return self._size
        except AttributeError:
            try:
                self._size = self.file.size
            except AttributeError:
                self._size = len(self.file.getvalue()) <======= LOOK HERE ===================
        return self._size

TextIteratorIO does not have the getvalue method, so it raises an AttributeError that is caught in DownloadResponse's method default_headers:

            try:
                headers['Content-Length'] = self.file.size
            except (AttributeError, NotImplementedError):
                pass  # Generated files.

But BytesIteratorIO inherits from io.BytesIO that does implement getvalue, and so it returns 0.