summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Cahoon <chris.cahoon@gmail.com>2009-07-01 02:51:48 +0000
committerChris Cahoon <chris.cahoon@gmail.com>2009-07-01 02:51:48 +0000
commit2d29d6c4f83449fb4ff4d728720a15762c1653b5 (patch)
tree74652f2d35c8a3b7a03ea20f1ec201b7b6d0fa73
parent4df7e8ed9fb77fabc05d9befcf98a6ec8558c42e (diff)
[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
-rw-r--r--django/conf/global_settings.py4
-rw-r--r--django/core/handlers/modpython.py13
-rw-r--r--django/core/handlers/wsgi.py11
-rw-r--r--django/core/servers/basehttp.py7
-rw-r--r--django/http/__init__.py21
-rw-r--r--docs/ref/request-response.txt19
-rw-r--r--tests/regressiontests/sendfile/__init__.py0
-rw-r--r--tests/regressiontests/sendfile/models.py0
-rw-r--r--tests/regressiontests/sendfile/tests.py36
-rw-r--r--tests/regressiontests/sendfile/urls.py7
-rw-r--r--tests/regressiontests/sendfile/views.py7
-rw-r--r--tests/urls.py3
12 files changed, 116 insertions, 12 deletions
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
--- /dev/null
+++ b/tests/regressiontests/sendfile/__init__.py
diff --git a/tests/regressiontests/sendfile/models.py b/tests/regressiontests/sendfile/models.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/sendfile/models.py
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<filename>.*)/$', 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')),
)