Fixed #24072 -- Added FileResponse for streaming binary files.
This commit is contained in:
parent
05f702b94c
commit
3d2cae0896
|
@ -197,6 +197,8 @@ class WSGIHandler(base.BaseHandler):
|
||||||
for c in response.cookies.values():
|
for c in response.cookies.values():
|
||||||
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
||||||
start_response(force_str(status), response_headers)
|
start_response(force_str(status), response_headers)
|
||||||
|
if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
|
||||||
|
response = environ['wsgi.file_wrapper'](response.file_to_stream)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
from django.http.cookie import SimpleCookie, parse_cookie
|
from django.http.cookie import SimpleCookie, parse_cookie
|
||||||
from django.http.request import (HttpRequest, QueryDict,
|
from django.http.request import (HttpRequest, QueryDict,
|
||||||
RawPostDataException, UnreadablePostError, build_request_repr)
|
RawPostDataException, UnreadablePostError, build_request_repr)
|
||||||
from django.http.response import (HttpResponse, StreamingHttpResponse,
|
from django.http.response import (
|
||||||
|
HttpResponse, StreamingHttpResponse, FileResponse,
|
||||||
HttpResponseRedirect, HttpResponsePermanentRedirect,
|
HttpResponseRedirect, HttpResponsePermanentRedirect,
|
||||||
HttpResponseNotModified, HttpResponseBadRequest, HttpResponseForbidden,
|
HttpResponseNotModified, HttpResponseBadRequest, HttpResponseForbidden,
|
||||||
HttpResponseNotFound, HttpResponseNotAllowed, HttpResponseGone,
|
HttpResponseNotFound, HttpResponseNotAllowed, HttpResponseGone,
|
||||||
HttpResponseServerError, Http404, BadHeaderError, JsonResponse)
|
HttpResponseServerError, Http404, BadHeaderError, JsonResponse,
|
||||||
|
)
|
||||||
from django.http.utils import fix_location_header, conditional_content_removal
|
from django.http.utils import fix_location_header, conditional_content_removal
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -16,5 +18,5 @@ __all__ = [
|
||||||
'HttpResponseBadRequest', 'HttpResponseForbidden', 'HttpResponseNotFound',
|
'HttpResponseBadRequest', 'HttpResponseForbidden', 'HttpResponseNotFound',
|
||||||
'HttpResponseNotAllowed', 'HttpResponseGone', 'HttpResponseServerError',
|
'HttpResponseNotAllowed', 'HttpResponseGone', 'HttpResponseServerError',
|
||||||
'Http404', 'BadHeaderError', 'fix_location_header', 'JsonResponse',
|
'Http404', 'BadHeaderError', 'fix_location_header', 'JsonResponse',
|
||||||
'conditional_content_removal',
|
'FileResponse', 'conditional_content_removal',
|
||||||
]
|
]
|
||||||
|
|
|
@ -417,6 +417,9 @@ class StreamingHttpResponse(HttpResponseBase):
|
||||||
|
|
||||||
@streaming_content.setter
|
@streaming_content.setter
|
||||||
def streaming_content(self, value):
|
def streaming_content(self, value):
|
||||||
|
self._set_streaming_content(value)
|
||||||
|
|
||||||
|
def _set_streaming_content(self, value):
|
||||||
# Ensure we can never iterate on "value" more than once.
|
# Ensure we can never iterate on "value" more than once.
|
||||||
self._iterator = iter(value)
|
self._iterator = iter(value)
|
||||||
if hasattr(value, 'close'):
|
if hasattr(value, 'close'):
|
||||||
|
@ -429,6 +432,22 @@ class StreamingHttpResponse(HttpResponseBase):
|
||||||
return b''.join(self.streaming_content)
|
return b''.join(self.streaming_content)
|
||||||
|
|
||||||
|
|
||||||
|
class FileResponse(StreamingHttpResponse):
|
||||||
|
"""
|
||||||
|
A streaming HTTP response class optimized for files.
|
||||||
|
"""
|
||||||
|
block_size = 4096
|
||||||
|
|
||||||
|
def _set_streaming_content(self, value):
|
||||||
|
if hasattr(value, 'read'):
|
||||||
|
self.file_to_stream = value
|
||||||
|
filelike = value
|
||||||
|
value = iter(lambda: filelike.read(self.block_size), b'')
|
||||||
|
else:
|
||||||
|
self.file_to_stream = None
|
||||||
|
super(FileResponse, self)._set_streaming_content(value)
|
||||||
|
|
||||||
|
|
||||||
class HttpResponseRedirectBase(HttpResponse):
|
class HttpResponseRedirectBase(HttpResponse):
|
||||||
allowed_schemes = ['http', 'https', 'ftp']
|
allowed_schemes = ['http', 'https', 'ftp']
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import posixpath
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.http import (Http404, HttpResponse, HttpResponseRedirect,
|
from django.http import (Http404, HttpResponse, HttpResponseRedirect,
|
||||||
HttpResponseNotModified, StreamingHttpResponse)
|
HttpResponseNotModified, FileResponse)
|
||||||
from django.template import loader, Template, Context, TemplateDoesNotExist
|
from django.template import loader, Template, Context, TemplateDoesNotExist
|
||||||
from django.utils.http import http_date, parse_http_date
|
from django.utils.http import http_date, parse_http_date
|
||||||
from django.utils.six.moves.urllib.parse import unquote
|
from django.utils.six.moves.urllib.parse import unquote
|
||||||
|
@ -63,8 +63,7 @@ def serve(request, path, document_root=None, show_indexes=False):
|
||||||
return HttpResponseNotModified()
|
return HttpResponseNotModified()
|
||||||
content_type, encoding = mimetypes.guess_type(fullpath)
|
content_type, encoding = mimetypes.guess_type(fullpath)
|
||||||
content_type = content_type or 'application/octet-stream'
|
content_type = content_type or 'application/octet-stream'
|
||||||
response = StreamingHttpResponse(open(fullpath, 'rb'),
|
response = FileResponse(open(fullpath, 'rb'), content_type=content_type)
|
||||||
content_type=content_type)
|
|
||||||
response["Last-Modified"] = http_date(statobj.st_mtime)
|
response["Last-Modified"] = http_date(statobj.st_mtime)
|
||||||
if stat.S_ISREG(statobj.st_mode):
|
if stat.S_ISREG(statobj.st_mode):
|
||||||
response["Content-Length"] = statobj.st_size
|
response["Content-Length"] = statobj.st_size
|
||||||
|
|
|
@ -998,3 +998,21 @@ Attributes
|
||||||
.. attribute:: StreamingHttpResponse.streaming
|
.. attribute:: StreamingHttpResponse.streaming
|
||||||
|
|
||||||
This is always ``True``.
|
This is always ``True``.
|
||||||
|
|
||||||
|
FileResponse objects
|
||||||
|
====================
|
||||||
|
|
||||||
|
.. versionadded:: 1.8
|
||||||
|
|
||||||
|
.. class:: FileResponse
|
||||||
|
|
||||||
|
:class:`FileResponse` is a subclass of :class:`StreamingHttpResponse` optimized
|
||||||
|
for binary files. It uses `wsgi.file_wrapper`_ if provided by the wsgi server,
|
||||||
|
otherwise it streams the file out in small chunks.
|
||||||
|
|
||||||
|
.. _wsgi.file_wrapper: https://www.python.org/dev/peps/pep-3333/#optional-platform-specific-file-handling
|
||||||
|
|
||||||
|
``FileResponse`` expects a file open in binary mode like so::
|
||||||
|
|
||||||
|
>>> from django.http import FileResponse
|
||||||
|
>>> response = FileResponse(open('myfile.png', 'rb'))
|
||||||
|
|
|
@ -559,6 +559,8 @@ Requests and Responses
|
||||||
<django.http.HttpResponse.setdefault>` method allows setting a header unless
|
<django.http.HttpResponse.setdefault>` method allows setting a header unless
|
||||||
it has already been set.
|
it has already been set.
|
||||||
|
|
||||||
|
* You can use the new :class:`~django.http.FileResponse` to stream files.
|
||||||
|
|
||||||
* The :func:`~django.views.decorators.http.condition` decorator for
|
* The :func:`~django.views.decorators.http.condition` decorator for
|
||||||
conditional view processing now supports the ``If-unmodified-since`` header.
|
conditional view processing now supports the ``If-unmodified-since`` header.
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ from unittest import skipIf
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.http import (
|
from django.http import (
|
||||||
HttpRequest, HttpResponse, StreamingHttpResponse, HttpResponsePermanentRedirect,
|
HttpRequest, HttpResponse, StreamingHttpResponse, FileResponse,
|
||||||
HttpResponseRedirect,
|
HttpResponseRedirect, HttpResponsePermanentRedirect,
|
||||||
)
|
)
|
||||||
from django.middleware.clickjacking import XFrameOptionsMiddleware
|
from django.middleware.clickjacking import XFrameOptionsMiddleware
|
||||||
from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware
|
from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware
|
||||||
|
@ -624,6 +624,20 @@ class GZipMiddlewareTest(TestCase):
|
||||||
self.assertEqual(r.get('Content-Encoding'), 'gzip')
|
self.assertEqual(r.get('Content-Encoding'), 'gzip')
|
||||||
self.assertFalse(r.has_header('Content-Length'))
|
self.assertFalse(r.has_header('Content-Length'))
|
||||||
|
|
||||||
|
def test_compress_file_response(self):
|
||||||
|
"""
|
||||||
|
Tests that compression is performed on FileResponse.
|
||||||
|
"""
|
||||||
|
open_file = lambda: open(__file__, 'rb')
|
||||||
|
with open_file() as file1:
|
||||||
|
file_resp = FileResponse(file1)
|
||||||
|
file_resp['Content-Type'] = 'text/html; charset=UTF-8'
|
||||||
|
r = GZipMiddleware().process_response(self.req, file_resp)
|
||||||
|
with open_file() as file2:
|
||||||
|
self.assertEqual(self.decompress(b''.join(r)), file2.read())
|
||||||
|
self.assertEqual(r.get('Content-Encoding'), 'gzip')
|
||||||
|
self.assertIsNot(r.file_to_stream, file1)
|
||||||
|
|
||||||
def test_compress_non_200_response(self):
|
def test_compress_non_200_response(self):
|
||||||
"""
|
"""
|
||||||
Tests that compression is performed on responses with a status other than 200.
|
Tests that compression is performed on responses with a status other than 200.
|
||||||
|
|
|
@ -51,6 +51,28 @@ class WSGITest(TestCase):
|
||||||
bytes(response),
|
bytes(response),
|
||||||
b"Content-Type: text/html; charset=utf-8\r\n\r\nHello World!")
|
b"Content-Type: text/html; charset=utf-8\r\n\r\nHello World!")
|
||||||
|
|
||||||
|
def test_file_wrapper(self):
|
||||||
|
"""
|
||||||
|
Verify that FileResponse uses wsgi.file_wrapper.
|
||||||
|
"""
|
||||||
|
class FileWrapper(object):
|
||||||
|
def __init__(self, filelike, blksize=8192):
|
||||||
|
filelike.close()
|
||||||
|
application = get_wsgi_application()
|
||||||
|
environ = RequestFactory()._base_environ(
|
||||||
|
PATH_INFO='/file/',
|
||||||
|
REQUEST_METHOD='GET',
|
||||||
|
**{'wsgi.file_wrapper': FileWrapper}
|
||||||
|
)
|
||||||
|
response_data = {}
|
||||||
|
|
||||||
|
def start_response(status, headers):
|
||||||
|
response_data['status'] = status
|
||||||
|
response_data['headers'] = headers
|
||||||
|
response = application(environ, start_response)
|
||||||
|
self.assertEqual(response_data['status'], '200 OK')
|
||||||
|
self.assertIsInstance(response, FileWrapper)
|
||||||
|
|
||||||
|
|
||||||
class GetInternalWSGIApplicationTest(unittest.TestCase):
|
class GetInternalWSGIApplicationTest(unittest.TestCase):
|
||||||
@override_settings(WSGI_APPLICATION="wsgi.wsgi.application")
|
@override_settings(WSGI_APPLICATION="wsgi.wsgi.application")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, FileResponse
|
||||||
|
|
||||||
|
|
||||||
def helloworld(request):
|
def helloworld(request):
|
||||||
|
@ -7,4 +7,5 @@ def helloworld(request):
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url("^$", helloworld),
|
url("^$", helloworld),
|
||||||
|
url(r'^file/$', lambda x: FileResponse(open(__file__, 'rb'))),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue