From 2d29d6c4f83449fb4ff4d728720a15762c1653b5 Mon Sep 17 00:00:00 2001 From: Chris Cahoon Date: Wed, 1 Jul 2009 02:51:48 +0000 Subject: [PATCH] [soc2009/http-wsgi-improvements] Initial HttpResponseSendFile support, changes pulled from 03/21/09 patch on refs #2131. This does not pass the included regression tests. However, since this feature will be entirely based on these changes, which have already gone through a great number of iterations, I thought it would be sensible to start here. All of the work here is ymasuda, mizatservercave, and mrts (apologies if I missed anyone). I hope to take their work down the final stretch. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/http-wsgi-improvements@11131 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 4 +++ django/core/handlers/modpython.py | 13 +++++--- django/core/handlers/wsgi.py | 11 +++++++ django/core/servers/basehttp.py | 7 ++--- django/http/__init__.py | 21 +++++++++++++ docs/ref/request-response.txt | 19 ++++++++++-- tests/regressiontests/sendfile/__init__.py | 0 tests/regressiontests/sendfile/models.py | 0 tests/regressiontests/sendfile/tests.py | 36 ++++++++++++++++++++++ tests/regressiontests/sendfile/urls.py | 7 +++++ tests/regressiontests/sendfile/views.py | 7 +++++ tests/urls.py | 3 ++ 12 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 tests/regressiontests/sendfile/__init__.py create mode 100644 tests/regressiontests/sendfile/models.py create mode 100644 tests/regressiontests/sendfile/tests.py create mode 100644 tests/regressiontests/sendfile/urls.py create mode 100644 tests/regressiontests/sendfile/views.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 99fc72e468..c00b0b9b96 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -236,6 +236,10 @@ MEDIA_ROOT = '' # Example: "http://media.lawrence.com" MEDIA_URL = '' +# Header to use in HttpResponseSendFile to inform the handler to serve the +# file with efficient handler-specific routines. +HTTPRESPONSE_SENDFILE_HEADER = 'X-Sendfile' + # List of upload handler classes to be applied in order. FILE_UPLOAD_HANDLERS = ( 'django.core.files.uploadhandler.MemoryFileUploadHandler', diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index 95a99e89bc..2517098366 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -200,11 +200,14 @@ class ModPythonHandler(BaseHandler): for c in response.cookies.values(): req.headers_out.add('Set-Cookie', c.output(header='')) req.status = response.status_code - try: - for chunk in response: - req.write(chunk) - finally: - response.close() + if isinstance(response, http.HttpResponseSendFile): + req.sendfile(response.sendfile_filename) + else: + try: + for chunk in response: + req.write(chunk) + finally: + response.close() return 0 # mod_python.apache.OK diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 10a5af7ab0..b79d15fec9 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -241,5 +241,16 @@ class WSGIHandler(base.BaseHandler): for c in response.cookies.values(): response_headers.append(('Set-Cookie', str(c.output(header='')))) start_response(status, response_headers) + + if isinstance(response, http.HttpResponseSendFile): + filelike = open(response.sendfile_filename, 'rb') + if 'wsgi.file_wrapper' in environ: + return environ['wsgi.file_wrapper'](filelike, + response.block_size) + else: + # wraps close() as well + from django.core.servers.basehttp import FileWrapper + return FileWrapper(filelike, response.block_size) + return response diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 6fed0acc59..44090a4516 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -314,10 +314,9 @@ class ServerHandler(object): to iterate over the data, and to call 'self.close()' once the response is finished. """ - if not self.result_is_file() or not self.sendfile(): - for data in self.result: - self.write(data) - self.finish_content() + for data in self.result: + self.write(data) + self.finish_content() self.close() def get_scheme(self): diff --git a/django/http/__init__.py b/django/http/__init__.py index fbdce2e77c..bca89f6133 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -415,6 +415,27 @@ class HttpResponse(object): raise Exception("This %s instance cannot tell its position" % self.__class__) return sum([len(chunk) for chunk in self._container]) +class HttpResponseSendFile(HttpResponse): + def __init__(self, path_to_file, content_type=None, block_size=8192): + if not content_type: + from mimetypes import guess_type + content_type = guess_type(path_to_file)[0] + if content_type is None: + content_type = "application/octet-stream" + super(HttpResponseSendFile, self).__init__(None, + content_type=content_type) + self.sendfile_filename = path_to_file + self.block_size = block_size + self['Content-Length'] = os.path.getsize(path_to_file) + self['Content-Disposition'] = ('attachment; filename=%s' % + os.path.basename(path_to_file)) + self[settings.HTTPRESPONSE_SENDFILE_HEADER] = path_to_file + + def _get_content(self): + return open(self.sendfile_filename) + + content = property(_get_content) + class HttpResponseRedirect(HttpResponse): status_code = 302 diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 8701c76235..bae79520ac 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -560,9 +560,22 @@ Methods HttpResponse subclasses ----------------------- -Django includes a number of ``HttpResponse`` subclasses that handle different -types of HTTP responses. Like ``HttpResponse``, these subclasses live in -:mod:`django.http`. +Django includes a number of :class:`HttpResponse` subclasses that handle +different types of HTTP responses. Like :class:`HttpResponse`, these subclasses +live in :mod:`django.http`. + +.. class:: HttpResponseSendFile + + .. versionadded:: 1.1 + + A special response class for efficient file serving. It informs the HTTP + protocol handler to use platform-specific file serving mechanism (if + available). The constructor takes three arguments -- the file path and, + optionally, the file's content type and block size hint for handlers that + need it. + + Note that response middleware will be bypassed if you use + :class:`HttpResponseSendFile`. .. class:: HttpResponseRedirect diff --git a/tests/regressiontests/sendfile/__init__.py b/tests/regressiontests/sendfile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/sendfile/models.py b/tests/regressiontests/sendfile/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/sendfile/tests.py b/tests/regressiontests/sendfile/tests.py new file mode 100644 index 0000000000..b1f1fdff14 --- /dev/null +++ b/tests/regressiontests/sendfile/tests.py @@ -0,0 +1,36 @@ +import urllib, os + +from django.test import TestCase +from django.conf import settings +from django.core.files import temp as tempfile + +FILE_SIZE = 2 ** 10 +CONTENT = 'a' * FILE_SIZE + +class SendFileTests(TestCase): + def test_sendfile(self): + tdir = tempfile.gettempdir() + + file1 = tempfile.NamedTemporaryFile(suffix=".pdf", dir=tdir) + file1.write(CONTENT) + file1.seek(0) + + response = self.client.get('/sendfile/serve_file/%s/' % + urllib.quote(file1.name)) + + file1.close() + + self.assertEqual(response.status_code, 200) + self.assertEqual(response[settings.HTTPRESPONSE_SENDFILE_HEADER], + file1.name) + self.assertEqual(response['Content-Disposition'], + 'attachment; filename=%s' % os.path.basename(file1.name)) + self.assertEqual(response['Content-Length'], str(FILE_SIZE)) + self.assertEqual(response['Content-Type'], 'application/pdf') + + # *if* the degraded case is to be supported, add this instead: + # self.assertEqual(response.content, CONTENT) + get_content = lambda: response.content + self.assertRaises(TypeError, get_content) + + # TODO: test middleware bypass etc diff --git a/tests/regressiontests/sendfile/urls.py b/tests/regressiontests/sendfile/urls.py new file mode 100644 index 0000000000..db0be4f1e7 --- /dev/null +++ b/tests/regressiontests/sendfile/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import patterns + +import views + +urlpatterns = patterns('', + (r'^serve_file/(?P.*)/$', views.serve_file), +) \ No newline at end of file diff --git a/tests/regressiontests/sendfile/views.py b/tests/regressiontests/sendfile/views.py new file mode 100644 index 0000000000..04a4f50540 --- /dev/null +++ b/tests/regressiontests/sendfile/views.py @@ -0,0 +1,7 @@ +import urllib + +from django.http import HttpResponseSendFile + +def serve_file(request, filename): + filename = urllib.unquote(filename) + return HttpResponseSendFile(filename) \ No newline at end of file diff --git a/tests/urls.py b/tests/urls.py index 6704829231..5c0a04da2b 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -33,6 +33,9 @@ urlpatterns = patterns('', # test urlconf for syndication tests (r'^syndication/', include('regressiontests.syndication.urls')), + # HttpResponseSendfile tests + (r'^sendfile/', include('regressiontests.sendfile.urls')), + # conditional get views (r'condition/', include('regressiontests.conditional_processing.urls')), )