Ticket #7894: file_wrapper_response3.diff

File file_wrapper_response3.diff, 11.4 KB (added by graham.carlyle@…, 7 years ago)

Fixed incorrect content length when only supplying an offset, added tests for wsgi handler, patched serve generic function to use

  • django/http/__init__.py

     
    424424    def __init__(self, *args, **kwargs):
    425425        HttpResponse.__init__(self, *args, **kwargs)
    426426
     427
     428class FileWrapper(object):
     429    def __init__(self, file_path, block_size, byte_count, offset):
     430        self.__dict__.update(locals())
     431
     432    def __iter__(self):
     433        self.file = open(self.file_path, 'rb')
     434        self.close = self.file.close
     435        if self.offset:
     436            self.file.seek(self.offset)
     437        self.bytes_left = self.byte_count
     438        return self
     439       
     440    def next(self):
     441        if self.bytes_left > 0:
     442            data_size = min(self.block_size, self.bytes_left)
     443            data = self.file.read(data_size)
     444            if data:
     445                self.bytes_left -= data_size
     446                return data
     447        raise StopIteration
     448
     449
     450class HttpResponseFileWrapper(HttpResponse):
     451    def __init__(self, file_path, block_size=8192, length=None, offset=0, **kwargs):
     452        content_length = length or (os.path.getsize(file_path) - offset)
     453        HttpResponse.__init__(self, content=FileWrapper(file_path, block_size, content_length, offset), **kwargs)
     454        self.file_path = file_path
     455        self.block_size = block_size
     456        self.length = length
     457        self.offset = offset
     458        self['Content-Length'] = content_length
     459
     460
    427461# A backwards compatible alias for HttpRequest.get_host.
    428462def get_host(request):
    429463    return request.get_host()
  • django/core/servers/basehttp.py

     
    312312        in the event loop to iterate over the data, and to call
    313313        'self.close()' once the response is finished.
    314314        """
    315         if not self.result_is_file() and not self.sendfile():
     315        if not self.result_is_file() or not self.sendfile():
    316316            for data in self.result:
    317317                self.write(data)
    318318            self.finish_content()
  • django/core/handlers/wsgi.py

     
    11from threading import Lock
     2import os
    23from pprint import pformat
    34try:
    45    from cStringIO import StringIO
     
    194195    initLock = Lock()
    195196    request_class = WSGIRequest
    196197
     198    def wsgi_adaptor_honours_content_length(self, environ):
     199        return 'mod_wsgi.callable_object' in environ
     200   
    197201    def __call__(self, environ, start_response):
    198202        from django.conf import settings
    199203
     
    232236        for c in response.cookies.values():
    233237            response_headers.append(('Set-Cookie', str(c.output(header=''))))
    234238        start_response(status, response_headers)
    235         return response
     239        if (isinstance(response, http.HttpResponseFileWrapper) and
     240            'wsgi.file_wrapper'in environ and
     241            (response.length is None or
     242             self.wsgi_adaptor_honours_content_length(environ) or
     243             response.length == os.path.getsize(response.file_path) - response.offset)):
     244            f = open(response.file_path, 'rb')
     245            if response.offset:
     246                f.seek(response.offset)
     247            return environ['wsgi.file_wrapper'](f, response.block_size)
     248        else:
     249            return response
    236250
  • django/core/handlers/modpython.py

     
    199199            req.headers_out.add('Set-Cookie', c.output(header=''))
    200200        req.status = response.status_code
    201201        try:
    202             for chunk in response:
    203                 req.write(chunk)
     202            if isinstance(response, http.HttpResponseFileWrapper):
     203                if response.length is None:
     204                    length = -1
     205                else:
     206                    length = response.length
     207                req.sendfile(response.file_path, response.offset, length)
     208            else:
     209                for chunk in response:
     210                    req.write(chunk)
    204211        finally:
    205212            response.close()
    206 
    207213        return 0 # mod_python.apache.OK
    208214
    209215def handler(req):
  • django/views/static.py

     
    1212from email.Utils import parsedate_tz, mktime_tz
    1313
    1414from django.template import loader
    15 from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified
     15from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified, HttpResponseFileWrapper
    1616from django.template import Template, Context, TemplateDoesNotExist
    1717from django.utils.http import http_date
    1818
     
    6060                              statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]):
    6161        return HttpResponseNotModified()
    6262    mimetype = mimetypes.guess_type(fullpath)[0] or 'application/octet-stream'
    63     contents = open(fullpath, 'rb').read()
    64     response = HttpResponse(contents, mimetype=mimetype)
     63    response = HttpResponseFileWrapper(fullpath, mimetype=mimetype)
    6564    response["Last-Modified"] = http_date(statobj[stat.ST_MTIME])
    66     response["Content-Length"] = len(contents)
    6765    return response
    6866
    6967DEFAULT_DIRECTORY_INDEX_TEMPLATE = """
  • tests/regressiontests/handlers/tests.py

     
     1"""
     2#############################################
     3# WSGI handler with HttpResponseFileWrapper #
     4#############################################
     5
     6>>> h = wsgi.WSGIHandler()
     7
     8Patch handler to shortcut dispatch process and provide a mocked response
     9
     10>>> import os
     11>>> test_file_path = os.path.join(os.path.dirname(__file__), 'test_file.txt')
     12>>> r = http.HttpResponseFileWrapper(test_file_path)
     13>>> h.get_response = lambda self: r
     14
     15Mock the environ and start_response
     16
     17>>> environ = {'REQUEST_METHOD': 'GET'}
     18>>> start_response = lambda status, headers: None
     19
     20No wsgi.file_wrapper in the environ defaults to returning the response
     21
     22>>> r is h(environ, start_response)
     23True
     24
     25A wsgi.file_wrapper in the environ with no length or offset makes the handler call the file_wrapper with the open file and suggested block_size
     26
     27>>> file_wrapper = lambda f, block_size: (f, block_size)
     28>>> environ = {'REQUEST_METHOD': 'GET', 'wsgi.file_wrapper': file_wrapper}
     29
     30>>> f, block_size = h(environ, start_response)
     31>>> f
     32<open file '.../tests/regressiontests/handlers/test_file.txt', mode 'rb' at ...>
     33>>> f.tell() == 0
     34True
     35>>> block_size == r.block_size
     36True
     37>>> f.close()
     38
     39An response with an offset causes a file seek before the handler returns
     40
     41>>> r = http.HttpResponseFileWrapper(test_file_path, offset=4)
     42>>> h.get_response = lambda self: r
     43
     44>>> f, block_size = h(environ, start_response)
     45>>> f.tell() == 4
     46True
     47>>> f.close()
     48
     49A response with a length that isn't the remaining file length in a wsgi adaptor that isn't recognised as supporting this returns the response
     50
     51>>> r = http.HttpResponseFileWrapper(test_file_path, length=10)
     52>>> h.get_response = lambda self: r
     53
     54>>> r is h(environ, start_response)
     55True
     56
     57If the length is the remaining file length then all adaptors should support this
     58
     59>>> r = http.HttpResponseFileWrapper(test_file_path, block_size=8192, length=26)
     60>>> h.get_response = lambda self: r
     61>>> h(environ, start_response)
     62(<open file '.../tests/regressiontests/handlers/test_file.txt', mode 'rb' at ...>, 8192)
     63
     64>>> r = http.HttpResponseFileWrapper(test_file_path, block_size=8192, offset=1, length=25)
     65>>> h.get_response = lambda self: r
     66>>> h(environ, start_response)
     67(<open file '.../tests/regressiontests/handlers/test_file.txt', mode 'rb' at ...>, 8192)
     68
     69If the wsgi adaptor is recognised as honouring the response's Content-Length header then the file_wrapper is used even if the length isn't the remaining file length
     70
     71>>> environ = {'REQUEST_METHOD': 'GET', 'wsgi.file_wrapper': file_wrapper, 'mod_wsgi.callable_object': 'foo'}
     72>>> r = http.HttpResponseFileWrapper(test_file_path, block_size=8192, length=5)
     73>>> h.get_response = lambda self: r
     74>>> h(environ, start_response)
     75(<open file '.../tests/regressiontests/handlers/test_file.txt', mode 'rb' at ...>, 8192)
     76
     77"""
     78
     79from django.core.handlers import wsgi
     80from django import http
     81
     82if __name__ == "__main__":
     83    import doctest
     84    doctest.testmod()
  • tests/regressiontests/handlers/test_file.txt

     
     1123456789 Heres some data!
     2 No newline at end of file
  • tests/regressiontests/httpwrappers/tests.py

     
    426426Traceback (most recent call last):
    427427...
    428428UnicodeEncodeError: ..., HTTP response headers must be in US-ASCII format
    429  
     429
     430
     431###########################
     432# HttpResponseFileWrapper #
     433###########################
     434
     435Get the path of the test file
     436
     437>>> import os
     438>>> test_file_path = os.path.join(os.path.dirname(__file__), 'test_file.txt')
     439
     440Create a response with the file path
     441
     442>>> r = HttpResponseFileWrapper(test_file_path, mimetype='text/plain')
     443
     444The response's content defaults to an iterator over the file data
     445
     446>>> ''.join(list(r))
     447'123456789 Heres some data!'
     448
     449The Content-Length header is set to size of the file
     450
     451>>> r['Content-Length']
     452'26'
     453
     454An offset and length can be provided to limit the data read from the file
     455
     456>>> r = HttpResponseFileWrapper(test_file_path, offset=2, length=5)
     457>>> ''.join(list(r))
     458'34567'
     459>>> r['Content-Length']
     460'5'
     461>>> r = HttpResponseFileWrapper(test_file_path, offset=2)
     462>>> r['Content-Length']
     463'24'
     464
     465A block_size can also be passed in as a suggestion for the block size to read
     466
     467>>> r = HttpResponseFileWrapper(test_file_path, offset=2, length=5, block_size=4024)
     468
     469The handler can choose to deliver the file's data using alternative means by using the response's properties rather than treating it as an iterator
     470
     471>>> r.file_path == test_file_path
     472True
     473
     474>>> r.offset
     4752
     476>>> r.length
     4775
     478>>> r.block_size
     4794024
     480
    430481"""
    431482
    432 from django.http import QueryDict, HttpResponse
     483from django.http import QueryDict, HttpResponse, HttpResponseFileWrapper
    433484
    434485if __name__ == "__main__":
    435486    import doctest
  • tests/regressiontests/httpwrappers/test_file.txt

     
     1123456789 Heres some data!
     2 No newline at end of file
Back to Top