Code

Ticket #2131: httpresponsesendfile-no-default-content_bypass-middleware_with-header_with-docs-and-tests.diff

File httpresponsesendfile-no-default-content_bypass-middleware_with-header_with-docs-and-tests.diff, 12.3 KB (added by mrts, 5 years ago)

Added the requested header, as well as docs and tests.

Line 
1Index: django/http/__init__.py
2===================================================================
3--- django/http/__init__.py     (revision 10112)
4+++ django/http/__init__.py     (working copy)
5@@ -393,6 +393,22 @@
6             raise Exception("This %s instance cannot tell its position" % self.__class__)
7         return sum([len(chunk) for chunk in self._container])
8 
9+class HttpResponseSendFile(HttpResponse):
10+    def __init__(self, path_to_file, content_type=None, block_size=8192):
11+        if not content_type:
12+            from mimetypes import guess_type
13+            content_type = guess_type(path_to_file)[0]
14+            if content_type is None:
15+                content_type = "application/octet-stream"
16+        super(HttpResponseSendFile, self).__init__(None,
17+                content_type=content_type)
18+        self.sendfile_filename = path_to_file
19+        self.block_size = block_size
20+        self['Content-Length'] = os.path.getsize(path_to_file)
21+        self['Content-Disposition'] = ('attachment; filename=%s' %
22+                os.path.basename(path_to_file))
23+        self[settings.HTTPRESPONSE_SENDFILE_HEADER] = path_to_file
24+
25 class HttpResponseRedirect(HttpResponse):
26     status_code = 302
27 
28Index: django/conf/global_settings.py
29===================================================================
30--- django/conf/global_settings.py      (revision 10112)
31+++ django/conf/global_settings.py      (working copy)
32@@ -239,6 +239,10 @@
33 # Example: "http://media.lawrence.com"
34 MEDIA_URL = ''
35 
36+# Header to use in HttpResponseSendFile to inform the handler to serve the
37+# file with efficient handler-specific routines.
38+HTTPRESPONSE_SENDFILE_HEADER = 'X-Sendfile'
39+
40 # List of upload handler classes to be applied in order.
41 FILE_UPLOAD_HANDLERS = (
42     'django.core.files.uploadhandler.MemoryFileUploadHandler',
43Index: django/core/servers/basehttp.py
44===================================================================
45--- django/core/servers/basehttp.py     (revision 10112)
46+++ django/core/servers/basehttp.py     (working copy)
47@@ -313,10 +313,9 @@
48         in the event loop to iterate over the data, and to call
49         'self.close()' once the response is finished.
50         """
51-        if not self.result_is_file() and not self.sendfile():
52-            for data in self.result:
53-                self.write(data)
54-            self.finish_content()
55+        for data in self.result:
56+            self.write(data)
57+        self.finish_content()
58         self.close()
59 
60     def get_scheme(self):
61Index: django/core/handlers/wsgi.py
62===================================================================
63--- django/core/handlers/wsgi.py        (revision 10112)
64+++ django/core/handlers/wsgi.py        (working copy)
65@@ -231,30 +231,30 @@
66             self.initLock.release()
67 
68         set_script_prefix(base.get_script_name(environ))
69-        signals.request_started.send(sender=self.__class__)
70-        try:
71-            try:
72-                request = self.request_class(environ)
73-            except UnicodeDecodeError:
74-                response = http.HttpResponseBadRequest()
75-            else:
76-                response = self.get_response(request)
77 
78-                # Apply response middleware
79-                for middleware_method in self._response_middleware:
80-                    response = middleware_method(request, response)
81-                response = self.apply_response_fixes(request, response)
82-        finally:
83-            signals.request_finished.send(sender=self.__class__)
84+        response = self.process_request(environ)
85 
86         try:
87             status_text = STATUS_CODE_TEXT[response.status_code]
88         except KeyError:
89             status_text = 'UNKNOWN STATUS CODE'
90         status = '%s %s' % (response.status_code, status_text)
91+
92         response_headers = [(str(k), str(v)) for k, v in response.items()]
93         for c in response.cookies.values():
94             response_headers.append(('Set-Cookie', str(c.output(header=''))))
95+
96         start_response(status, response_headers)
97+
98+        if isinstance(response, http.HttpResponseSendFile):
99+            filelike = open(response.sendfile_filename, 'rb')
100+            if 'wsgi.file_wrapper' in environ:
101+                return environ['wsgi.file_wrapper'](filelike,
102+                        response.block_size)
103+            else:
104+                # wraps close() as well
105+                from django.core.servers.basehttp import FileWrapper
106+                return FileWrapper(filelike, response.block_size)
107+
108         return response
109 
110Index: django/core/handlers/base.py
111===================================================================
112--- django/core/handlers/base.py        (revision 10112)
113+++ django/core/handlers/base.py        (working copy)
114@@ -63,6 +63,26 @@
115         # as a flag for initialization being complete.
116         self._request_middleware = request_middleware
117 
118+    def process_request(self, request_env):
119+        signals.request_started.send(sender=self.__class__)
120+        try:
121+            try:
122+                request = self.request_class(request_env)
123+            except UnicodeDecodeError:
124+                response = http.HttpResponseBadRequest()
125+            else:
126+                response = self.get_response(request)
127+
128+                # Apply response middleware
129+                if not isinstance(response, http.HttpResponseSendFile):
130+                    for middleware_method in self._response_middleware:
131+                        response = middleware_method(request, response)
132+                    response = self.apply_response_fixes(request, response)
133+        finally:
134+            signals.request_finished.send(sender=self.__class__)
135+
136+        return response
137+
138     def get_response(self, request):
139         "Returns an HttpResponse object for the given HttpRequest"
140         from django.core import exceptions, urlresolvers
141Index: django/core/handlers/modpython.py
142===================================================================
143--- django/core/handlers/modpython.py   (revision 10112)
144+++ django/core/handlers/modpython.py   (working copy)
145@@ -2,7 +2,6 @@
146 from pprint import pformat
147 
148 from django import http
149-from django.core import signals
150 from django.core.handlers.base import BaseHandler
151 from django.core.urlresolvers import set_script_prefix
152 from django.utils import datastructures
153@@ -191,21 +190,8 @@
154             self.load_middleware()
155 
156         set_script_prefix(req.get_options().get('django.root', ''))
157-        signals.request_started.send(sender=self.__class__)
158-        try:
159-            try:
160-                request = self.request_class(req)
161-            except UnicodeDecodeError:
162-                response = http.HttpResponseBadRequest()
163-            else:
164-                response = self.get_response(request)
165 
166-                # Apply response middleware
167-                for middleware_method in self._response_middleware:
168-                    response = middleware_method(request, response)
169-                response = self.apply_response_fixes(request, response)
170-        finally:
171-            signals.request_finished.send(sender=self.__class__)
172+        response = self.process_request(req)
173 
174         # Convert our custom HttpResponse object back into the mod_python req.
175         req.content_type = response['Content-Type']
176@@ -215,12 +201,16 @@
177         for c in response.cookies.values():
178             req.headers_out.add('Set-Cookie', c.output(header=''))
179         req.status = response.status_code
180-        try:
181-            for chunk in response:
182-                req.write(chunk)
183-        finally:
184-            response.close()
185 
186+        if isinstance(response, http.HttpResponseSendFile):
187+            req.sendfile(response.sendfile_filename)
188+        else:
189+            try:
190+                for chunk in response:
191+                    req.write(chunk)
192+            finally:
193+                response.close()
194+
195         return 0 # mod_python.apache.OK
196 
197 def handler(req):
198Index: tests/regressiontests/sendfile/views.py
199===================================================================
200--- tests/regressiontests/sendfile/views.py     (revision 0)
201+++ tests/regressiontests/sendfile/views.py     (revision 0)
202@@ -0,0 +1,7 @@
203+import urllib
204+
205+from django.http import HttpResponseSendFile
206+
207+def serve_file(request, filename):
208+    filename = urllib.unquote(filename)
209+    return HttpResponseSendFile(filename)
210Index: tests/regressiontests/sendfile/__init__.py
211===================================================================
212Index: tests/regressiontests/sendfile/tests.py
213===================================================================
214--- tests/regressiontests/sendfile/tests.py     (revision 0)
215+++ tests/regressiontests/sendfile/tests.py     (revision 0)
216@@ -0,0 +1,36 @@
217+import urllib, os
218+
219+from django.test import TestCase
220+from django.conf import settings
221+from django.core.files import temp as tempfile
222+
223+FILE_SIZE = 2 ** 10
224+CONTENT = 'a' * FILE_SIZE
225+
226+class SendFileTests(TestCase):
227+    def test_sendfile(self):
228+        tdir = tempfile.gettempdir()
229+
230+        file1 = tempfile.NamedTemporaryFile(suffix=".pdf", dir=tdir)
231+        file1.write(CONTENT)
232+        file1.seek(0)
233+
234+        response = self.client.get('/sendfile/serve_file/%s/' %
235+                urllib.quote(file1.name))
236+
237+        file1.close()
238+
239+        self.assertEqual(response.status_code, 200)
240+        self.assertEqual(response[settings.HTTPRESPONSE_SENDFILE_HEADER],
241+                file1.name)
242+        self.assertEqual(response['Content-Disposition'],
243+                'attachment; filename=%s' % os.path.basename(file1.name))
244+        self.assertEqual(response['Content-Length'], str(FILE_SIZE))
245+        self.assertEqual(response['Content-Type'], 'application/pdf')
246+
247+        # *if* the degraded case is to be supported, add this instead:
248+        # self.assertEqual(response.content, CONTENT)
249+        get_content = lambda: response.content
250+        self.assertRaises(TypeError, get_content)
251+
252+        # TODO: test middleware bypass etc
253Index: tests/regressiontests/sendfile/models.py
254===================================================================
255Index: tests/regressiontests/sendfile/urls.py
256===================================================================
257--- tests/regressiontests/sendfile/urls.py      (revision 0)
258+++ tests/regressiontests/sendfile/urls.py      (revision 0)
259@@ -0,0 +1,7 @@
260+from django.conf.urls.defaults import patterns
261+
262+import views
263+
264+urlpatterns = patterns('',
265+    (r'^serve_file/(?P<filename>.*)/$', views.serve_file),
266+)
267Index: tests/urls.py
268===================================================================
269--- tests/urls.py       (revision 10112)
270+++ tests/urls.py       (working copy)
271@@ -20,11 +20,11 @@
272 
273     # test urlconf for middleware tests
274     (r'^middleware/', include('regressiontests.middleware.urls')),
275-   
276+
277     # admin view tests
278     (r'^test_admin/', include('regressiontests.admin_views.urls')),
279     (r'^generic_inline_admin/', include('regressiontests.generic_inline_admin.urls')),
280-   
281+
282     # admin widget tests
283     (r'widget_admin/', include('regressiontests.admin_widgets.urls')),
284 
285@@ -32,4 +32,7 @@
286 
287     # test urlconf for syndication tests
288     (r'^syndication/', include('regressiontests.syndication.urls')),
289+
290+    # HttpResponseSendfile tests
291+    (r'^sendfile/', include('regressiontests.sendfile.urls')),
292 )
293Index: docs/ref/request-response.txt
294===================================================================
295--- docs/ref/request-response.txt       (revision 10112)
296+++ docs/ref/request-response.txt       (working copy)
297@@ -538,10 +538,23 @@
298 HttpResponse subclasses
299 -----------------------
300 
301-Django includes a number of ``HttpResponse`` subclasses that handle different
302-types of HTTP responses. Like ``HttpResponse``, these subclasses live in
303-:mod:`django.http`.
304+Django includes a number of :class:`HttpResponse` subclasses that handle
305+different types of HTTP responses. Like :class:`HttpResponse`, these subclasses
306+live in :mod:`django.http`.
307 
308+.. class:: HttpResponseSendFile
309+
310+    .. versionadded:: 1.1
311+
312+    A special response class for efficient file serving. It informs the HTTP
313+    protocol handler to use platform-specific file serving mechanism (if
314+    available). The constructor takes three arguments -- the file path and,
315+    optionally, the file's content type and block size hint for handlers that
316+    need it.
317+
318+    Note that response middleware will be bypassed if you use
319+    :class:`HttpResponseSendFile`.
320+
321 .. class:: HttpResponseRedirect
322 
323     The constructor takes a single argument -- the path to redirect to. This