Kaydet (Commit) 3d2cae08 authored tarafından Collin Anderson's avatar Collin Anderson Kaydeden (comit) Tim Graham

Fixed #24072 -- Added FileResponse for streaming binary files.

üst 05f702b9
...@@ -197,6 +197,8 @@ class WSGIHandler(base.BaseHandler): ...@@ -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
......
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__ = [ ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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): ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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")
......
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): ...@@ -7,4 +7,5 @@ def helloworld(request):
urlpatterns = [ urlpatterns = [
url("^$", helloworld), url("^$", helloworld),
url(r'^file/$', lambda x: FileResponse(open(__file__, 'rb'))),
] ]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment