Code

Ticket #2070: 5722.diff

File 5722.diff, 26.4 KB (added by simonbun <simonbun@…>, 7 years ago)

Updated patch against r5722

Line 
1Index: django/http/__init__.py
2===================================================================
3--- django/http/__init__.py     (revision 5722)
4+++ django/http/__init__.py     (working copy)
5@@ -4,7 +4,11 @@
6 from urllib import urlencode
7 from django.utils.datastructures import MultiValueDict
8 from django.utils.encoding import smart_str, iri_to_uri, force_unicode
9+from django.http.multipartparser import MultiPartParser, MultiPartParserError
10+import re
11 
12+upload_id_re = re.compile(r'^[a-fA-F0-9]{32}$') # file progress id Regular expression
13+
14 RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
15 
16 try:
17@@ -64,37 +68,55 @@
18 
19     encoding = property(_get_encoding, _set_encoding)
20 
21-def parse_file_upload(header_dict, post_data):
22-    "Returns a tuple of (POST QueryDict, FILES MultiValueDict)"
23-    import email, email.Message
24-    from cgi import parse_header
25-    raw_message = '\r\n'.join(['%s:%s' % pair for pair in header_dict.items()])
26-    raw_message += '\r\n\r\n' + post_data
27-    msg = email.message_from_string(raw_message)
28-    POST = QueryDict('', mutable=True)
29-    FILES = MultiValueDict()
30-    for submessage in msg.get_payload():
31-        if submessage and isinstance(submessage, email.Message.Message):
32-            name_dict = parse_header(submessage['Content-Disposition'])[1]
33-            # name_dict is something like {'name': 'file', 'filename': 'test.txt'} for file uploads
34-            # or {'name': 'blah'} for POST fields
35-            # We assume all uploaded files have a 'filename' set.
36-            if 'filename' in name_dict:
37-                assert type([]) != type(submessage.get_payload()), "Nested MIME messages are not supported"
38-                if not name_dict['filename'].strip():
39-                    continue
40-                # IE submits the full path, so trim everything but the basename.
41-                # (We can't use os.path.basename because it expects Linux paths.)
42-                filename = name_dict['filename'][name_dict['filename'].rfind("\\")+1:]
43-                FILES.appendlist(name_dict['name'], {
44-                    'filename': filename,
45-                    'content-type': 'Content-Type' in submessage and submessage['Content-Type'] or None,
46-                    'content': submessage.get_payload(),
47-                })
48-            else:
49-                POST.appendlist(name_dict['name'], submessage.get_payload())
50-    return POST, FILES
51+    def _get_file_progress(self):
52+        return {}
53 
54+    def _set_file_progress(self,value):
55+        pass
56+
57+    def _del_file_progress(self):
58+        pass
59+
60+    file_progress = property(_get_file_progress,
61+                             _set_file_progress,
62+                            _del_file_progress)
63+
64+    def _get_file_progress_from_args(self, headers, get, querystring):
65+        """
66+       This parses the request for a file progress_id value.
67+       Note that there are two distinct ways of getting the progress
68+       ID -- header and GET. One is used primarily to attach via JavaScript
69+       to the end of an HTML form action while the other is used for AJAX
70+       communication.
71+       
72+       All progress IDs must be valid 32-digit hexadecimal numbers.
73+       """
74+       if 'X-Upload-ID' in headers:
75+           progress_id = headers['X-Upload-ID']
76+       elif 'progress_id' in get:
77+           progress_id = get['progress_id']
78+       else:
79+           return None
80+       
81+       if not upload_id_re.match(progress_id):
82+           return None
83+
84+       return progress_id
85+
86+def parse_file_upload(headers, input, request):
87+    from django.conf import settings
88+
89+    # Only stream files to disk if FILE_STREAMING_DIR is set
90+    file_upload_dir = settings.FILE_UPLOAD_DIR
91+    streaming_min_post_size = settings.STREAMING_MIN_POST_SIZE
92+   
93+    try:
94+        parser = MultiPartParser(headers, input, request, file_upload_dir, streaming_min_post_size)
95+       return parser.parse()
96+    except MultiPartParserError, e:
97+        return MultiValueDict({ '_file_upload_error': [e.message] }), {}
98+
99+
100 class QueryDict(MultiValueDict):
101     """
102     A specialized MultiValueDict that takes a query string when initialized.
103Index: django/oldforms/__init__.py
104===================================================================
105--- django/oldforms/__init__.py (revision 5722)
106+++ django/oldforms/__init__.py (working copy)
107@@ -676,16 +676,21 @@
108         self.validator_list = [self.isNonEmptyFile] + validator_list
109 
110     def isNonEmptyFile(self, field_data, all_data):
111-        try:
112-            content = field_data['content']
113-        except TypeError:
114+        if field_data.has_key('_file_upload_error'):
115+           raise validators.CriticalValidationError, field_data['_file_upload_error']
116+       if not field_data.has_key('filename'):
117             raise validators.CriticalValidationError, ugettext("No file was submitted. Check the encoding type on the form.")
118-        if not content:
119+        if not field_data['content-length']:
120             raise validators.CriticalValidationError, ugettext("The submitted file is empty.")
121 
122     def render(self, data):
123         return u'<input type="file" id="%s" class="v%s" name="%s" />' % \
124             (self.get_id(), self.__class__.__name__, self.field_name)
125+   
126+    def prepare(self, new_data):
127+        if new_data.has_key('_file_upload_error'):
128+           # pretend we got something in the field to raise a validation error later
129+           new_data[self.field_name] = { '_file_upload_error': new_data['_file_upload_error'] }
130 
131     def html2python(data):
132         if data is None:
133Index: django/db/models/base.py
134===================================================================
135--- django/db/models/base.py    (revision 5722)
136+++ django/db/models/base.py    (working copy)
137@@ -13,6 +13,7 @@
138 from django.utils.datastructures import SortedDict
139 from django.utils.functional import curry
140 from django.utils.encoding import smart_str, force_unicode
141+from django.utils.file import file_move_safe
142 from django.conf import settings
143 from itertools import izip
144 import types
145@@ -365,12 +366,16 @@
146     def _get_FIELD_size(self, field):
147         return os.path.getsize(self._get_FIELD_filename(field))
148 
149-    def _save_FIELD_file(self, field, filename, raw_contents, save=True):
150+    def _save_FIELD_file(self, field, filename, raw_field, save=True):
151         directory = field.get_directory_name()
152         try: # Create the date-based directory if it doesn't exist.
153             os.makedirs(os.path.join(settings.MEDIA_ROOT, directory))
154         except OSError: # Directory probably already exists.
155             pass
156+
157+        if filename is None:
158+            filename = raw_field['filename']
159+
160         filename = field.get_filename(filename)
161 
162         # If the filename already exists, keep adding an underscore to the name of
163@@ -387,9 +392,16 @@
164         setattr(self, field.attname, filename)
165 
166         full_filename = self._get_FIELD_filename(field)
167-        fp = open(full_filename, 'wb')
168-        fp.write(raw_contents)
169-        fp.close()
170+        if raw_field.has_key('tmpfilename'):
171+            raw_field['tmpfile'].close()
172+            file_move_safe(raw_field['tmpfilename'], full_filename)
173+        else:
174+            from django.utils import file_locks
175+            fp = open(full_filename, 'wb')
176+            # exclusive lock
177+            file_locks.lock(fp, file_locks.LOCK_EX)
178+            fp.write(raw_field['content'])
179+            fp.close()
180 
181         # Save the width and/or height, if applicable.
182         if isinstance(field, ImageField) and (field.width_field or field.height_field):
183Index: django/db/models/fields/__init__.py
184===================================================================
185--- django/db/models/fields/__init__.py (revision 5722)
186+++ django/db/models/fields/__init__.py (working copy)
187@@ -707,7 +707,8 @@
188         setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
189         setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
190         setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
191-        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save))
192+        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save))
193+        setattr(cls, 'move_%s_file' % self.name, lambda instance, raw_field, save=True: instance._save_FIELD_file(self, None, raw_field, save))       
194         dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
195 
196     def delete_file(self, instance):
197@@ -730,9 +731,9 @@
198         if new_data.get(upload_field_name, False):
199             func = getattr(new_object, 'save_%s_file' % self.name)
200             if rel:
201-                func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"], save)
202+                func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0], save)
203             else:
204-                func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"], save)
205+                func(new_data[upload_field_name]["filename"], new_data[upload_field_name], save)
206 
207     def get_directory_name(self):
208         return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
209Index: django/conf/global_settings.py
210===================================================================
211--- django/conf/global_settings.py      (revision 5722)
212+++ django/conf/global_settings.py      (working copy)
213@@ -247,6 +247,16 @@
214 from django import get_version
215 URL_VALIDATOR_USER_AGENT = "Django/%s (http://www.djangoproject.com)" % get_version()
216 
217+# The directory to place streamed file uploads. The web server needs write
218+# permissions on this directory.
219+# If this is None, streaming uploads are disabled.
220+FILE_UPLOAD_DIR = None
221+
222+# The minimum size of a POST before file uploads are streamed to disk.
223+# Any less than this number, and the file is uploaded to memory.
224+# Size is in bytes.
225+STREAMING_MIN_POST_SIZE = 512 * (2**10)
226+
227 ##############
228 # MIDDLEWARE #
229 ##############
230Index: django/core/handlers/wsgi.py
231===================================================================
232--- django/core/handlers/wsgi.py        (revision 5722)
233+++ django/core/handlers/wsgi.py        (working copy)
234@@ -76,6 +76,7 @@
235         self.environ = environ
236         self.path = force_unicode(environ['PATH_INFO'])
237         self.META = environ
238+        self.META['UPLOAD_PROGRESS_ID'] = self._get_file_progress_id()
239         self.method = environ['REQUEST_METHOD'].upper()
240 
241     def __repr__(self):
242@@ -112,7 +113,14 @@
243             if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
244                 header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')])
245                 header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '')
246-                self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data)
247+                header_dict['Content-Length'] = self.environ.get('CONTENT_LENGTH', '')
248+                header_dict['X-Progress-ID'] = self.environ.get('HTTP_X_PROGRESS_ID', '')
249+                try:
250+                    self._post, self._files = http.parse_file_upload(header_dict, self.environ['wsgi.input'], self)
251+                except:
252+                    self._post, self._files = {}, {} # make sure we dont read the input stream again
253+                    raise
254+                self._raw_post_data = None # raw data is not available for streamed multipart messages
255             else:
256                 self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
257         else:
258@@ -168,6 +176,17 @@
259             buf.close()
260             return self._raw_post_data
261 
262+    def _get_file_progress_id(self):
263+        """
264+        Returns the Progress ID of the request,
265+        usually provided if there is a file upload
266+        going on.
267+        Returns ``None`` if no progress ID is specified.
268+        """
269+        return self._get_file_progress_from_args(self.environ,
270+                                                 self.GET,
271+                                                 self.environ.get('QUERY_STRING', ''))
272+
273     GET = property(_get_get, _set_get)
274     POST = property(_get_post, _set_post)
275     COOKIES = property(_get_cookies, _set_cookies)
276Index: django/core/handlers/modpython.py
277===================================================================
278--- django/core/handlers/modpython.py   (revision 5722)
279+++ django/core/handlers/modpython.py   (working copy)
280@@ -48,7 +48,12 @@
281     def _load_post_and_files(self):
282         "Populates self._post and self._files"
283         if 'content-type' in self._req.headers_in and self._req.headers_in['content-type'].startswith('multipart'):
284-            self._post, self._files = http.parse_file_upload(self._req.headers_in, self.raw_post_data)
285+            self._raw_post_data = None # raw data is not available for streamed multipart messages
286+            try:
287+                self._post, self._files = http.parse_file_upload(self._req.headers_in, self._req, self)
288+            except:
289+                self._post, self._files = {}, {} # make sure we dont read the input stream again
290+                raise
291         else:
292             self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
293 
294@@ -93,20 +98,21 @@
295                 'AUTH_TYPE':         self._req.ap_auth_type,
296                 'CONTENT_LENGTH':    self._req.clength, # This may be wrong
297                 'CONTENT_TYPE':      self._req.content_type, # This may be wrong
298-                'GATEWAY_INTERFACE': 'CGI/1.1',
299-                'PATH_INFO':         self._req.path_info,
300-                'PATH_TRANSLATED':   None, # Not supported
301-                'QUERY_STRING':      self._req.args,
302-                'REMOTE_ADDR':       self._req.connection.remote_ip,
303-                'REMOTE_HOST':       None, # DNS lookups not supported
304-                'REMOTE_IDENT':      self._req.connection.remote_logname,
305-                'REMOTE_USER':       self._req.user,
306-                'REQUEST_METHOD':    self._req.method,
307-                'SCRIPT_NAME':       None, # Not supported
308-                'SERVER_NAME':       self._req.server.server_hostname,
309-                'SERVER_PORT':       self._req.server.port,
310-                'SERVER_PROTOCOL':   self._req.protocol,
311-                'SERVER_SOFTWARE':   'mod_python'
312+                'GATEWAY_INTERFACE':  'CGI/1.1',
313+                'PATH_INFO':          self._req.path_info,
314+                'PATH_TRANSLATED':    None, # Not supported
315+                'QUERY_STRING':       self._req.args,
316+                'REMOTE_ADDR':        self._req.connection.remote_ip,
317+                'REMOTE_HOST':        None, # DNS lookups not supported
318+                'REMOTE_IDENT':       self._req.connection.remote_logname,
319+                'REMOTE_USER':        self._req.user,
320+                'REQUEST_METHOD':     self._req.method,
321+                'SCRIPT_NAME':        None, # Not supported
322+                'SERVER_NAME':        self._req.server.server_hostname,
323+                'SERVER_PORT':        self._req.server.port,
324+                'SERVER_PROTOCOL':    self._req.protocol,
325+                'UPLOAD_PROGRESS_ID': self._get_file_progress_id(),
326+                'SERVER_SOFTWARE':    'mod_python'
327             }
328             for key, value in self._req.headers_in.items():
329                 key = 'HTTP_' + key.upper().replace('-', '_')
330@@ -123,6 +129,17 @@
331     def _get_method(self):
332         return self.META['REQUEST_METHOD'].upper()
333 
334+    def _get_file_progress_id(self):
335+        """
336+        Returns the Progress ID of the request,
337+        usually provided if there is a file upload
338+        going on.
339+        Returns ``None`` if no progress ID is specified.
340+        """
341+        return self._get_file_progress_from_args(self._req.headers_in,
342+                                                 self.GET,
343+                                                 self._req.args)
344+
345     GET = property(_get_get, _set_get)
346     POST = property(_get_post, _set_post)
347     COOKIES = property(_get_cookies, _set_cookies)
348Index: django/newforms/fields.py
349===================================================================
350--- django/newforms/fields.py   (revision 5722)
351+++ django/newforms/fields.py   (working copy)
352@@ -110,6 +110,8 @@
353         super(CharField, self).clean(value)
354         if value in EMPTY_VALUES:
355             return u''
356+       if isinstance(value, dict):
357+           return value
358         value = smart_unicode(value)
359         value_length = len(value)
360         if self.max_length is not None and value_length > self.max_length:
361Index: django/contrib/admin/urls.py
362===================================================================
363--- django/contrib/admin/urls.py        (revision 5722)
364+++ django/contrib/admin/urls.py        (working copy)
365@@ -10,6 +10,7 @@
366     ('^$', 'django.contrib.admin.views.main.index'),
367     ('^r/(\d+)/(.*)/$', 'django.views.defaults.shortcut'),
368     ('^jsi18n/$', i18n_view, {'packages': 'django.conf'}),
369+    ('^upload_progress/$', 'django.contrib.admin.views.main.upload_progress'),
370     ('^logout/$', 'django.contrib.auth.views.logout'),
371     ('^password_change/$', 'django.contrib.auth.views.password_change'),
372     ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'),
373Index: django/contrib/admin/views/main.py
374===================================================================
375--- django/contrib/admin/views/main.py  (revision 5722)
376+++ django/contrib/admin/views/main.py  (working copy)
377@@ -9,7 +9,7 @@
378 from django.shortcuts import get_object_or_404, render_to_response
379 from django.db import models
380 from django.db.models.query import handle_legacy_orderlist, QuerySet
381-from django.http import Http404, HttpResponse, HttpResponseRedirect
382+from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseServerError
383 from django.utils.html import escape
384 from django.utils.text import capfirst, get_text_list
385 from django.utils.encoding import force_unicode, smart_str
386@@ -88,6 +88,8 @@
387 def get_javascript_imports(opts, auto_populated_fields, field_sets):
388 # Put in any necessary JavaScript imports.
389     js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
390+    if opts.has_field_type(models.FileField) and settings.FILE_UPLOAD_DIR:
391+        js.append('js/UploadProgress.js')
392     if auto_populated_fields:
393         js.append('js/urlify.js')
394     if opts.has_field_type(models.DateTimeField) or opts.has_field_type(models.TimeField) or opts.has_field_type(models.DateField):
395@@ -789,3 +791,19 @@
396                                'admin/%s/change_list.html' % app_label,
397                                'admin/change_list.html'], context_instance=c)
398 change_list = staff_member_required(never_cache(change_list))
399+
400+def upload_progress(request):
401+    """
402+    Given this request, returns a JSON
403+    object that has information on a file upload progress.
404+    If there is no file upload in progress, returns an
405+    empty dictionary, '{}'.
406+    """
407+    from django.utils import simplejson
408+
409+    content = simplejson.dumps(request.file_progress)
410+
411+    if content.strip() == '{}':
412+        return HttpResponseServerError('')
413+    else:
414+        return HttpResponse(content=content, mimetype='text/plain')
415Index: django/contrib/admin/templates/admin/change_form.html
416===================================================================
417--- django/contrib/admin/templates/admin/change_form.html       (revision 5722)
418+++ django/contrib/admin/templates/admin/change_form.html       (working copy)
419@@ -65,6 +65,18 @@
420    {% auto_populated_field_script auto_populated_fields change %}
421    </script>
422 {% endif %}
423+
424+{% if has_file_field %}
425+<div id="progress_wrap" style="position: absolute; background: white; z-index: 9040; display: none; visibility: hidden; width: 420px; height: 50px padding: 10px; border: solid 1px #ddd;">
426+   <a href="#" onclick="close_progress();return false" title="Close Progress Bar"
427+      style="color: #c00; font-size: 1.5em; font-weight: bold; float: right; padding: 0; position: relative; top: -2px; left: -2px;">X</a>
428+   <h1>Upload progress</h1>
429+
430+   <div id="progress_bar" style="top: 0; left: 0; width: 0; z-index: 9049; height: 4px;" class="submit-row"></div>
431+   <div id="progress_text" style="color: black;">0%</div>
432 </div>
433+{% endif %}
434+
435+</div>
436 </form></div>
437 {% endblock %}
438Index: tests/modeltests/test_client/views.py
439===================================================================
440--- tests/modeltests/test_client/views.py       (revision 5722)
441+++ tests/modeltests/test_client/views.py       (working copy)
442@@ -46,6 +46,12 @@
443 
444     return HttpResponse(t.render(c))
445 
446+def post_file_view(request):
447+    "A view that expects a multipart post and returns a file in the context"
448+    t = Template('File {{ file.filename }} received', name='POST Template')
449+    c = Context({'file': request.FILES['file_file']})
450+    return HttpResponse(t.render(c))
451+
452 def redirect_view(request):
453     "A view that redirects all requests to the GET view"
454     return HttpResponseRedirect('/test_client/get_view/')
455Index: tests/modeltests/test_client/models.py
456===================================================================
457--- tests/modeltests/test_client/models.py      (revision 5722)
458+++ tests/modeltests/test_client/models.py      (working copy)
459@@ -4,7 +4,7 @@
460 
461 The test client is a class that can act like a simple
462 browser for testing purposes.
463
464+
465 It allows the user to compose GET and POST requests, and
466 obtain the response that the server gave to those requests.
467 The server Response objects are annotated with the details
468@@ -80,6 +80,20 @@
469         self.assertEqual(response.template.name, "Book template")
470         self.assertEqual(response.content, "Blink - Malcolm Gladwell")
471 
472+    def test_post_file_view(self):
473+        "POST this python file to a view"
474+        import os, tempfile
475+        from django.conf import settings
476+        file = __file__.replace('.pyc', '.py')
477+        for upload_dir in [None, tempfile.gettempdir()]:
478+            settings.FILE_UPLOAD_DIR = upload_dir
479+            post_data = { 'name': file, 'file': open(file) }
480+            response = self.client.post('/test_client/post_file_view/', post_data)
481+            self.failUnless('models.py' in response.context['file']['filename'])
482+            self.failUnless(len(response.context['file']['content']) == os.path.getsize(file))
483+            if upload_dir:
484+                self.failUnless(response.context['file']['tmpfilename'])
485+
486     def test_redirect(self):
487         "GET a URL that redirects elsewhere"
488         response = self.client.get('/test_client/redirect_view/')
489Index: tests/modeltests/test_client/urls.py
490===================================================================
491--- tests/modeltests/test_client/urls.py        (revision 5722)
492+++ tests/modeltests/test_client/urls.py        (working copy)
493@@ -5,6 +5,7 @@
494 urlpatterns = patterns('',
495     (r'^get_view/$', views.get_view),
496     (r'^post_view/$', views.post_view),
497+    (r'^post_file_view/$', views.post_file_view),
498     (r'^raw_post_view/$', views.raw_post_view),
499     (r'^redirect_view/$', views.redirect_view),
500     (r'^permanent_redirect_view/$', redirect_to, { 'url': '/test_client/get_view/' }),
501Index: docs/request_response.txt
502===================================================================
503--- docs/request_response.txt   (revision 5722)
504+++ docs/request_response.txt   (working copy)
505@@ -72,13 +72,25 @@
506 ``FILES``
507     A dictionary-like object containing all uploaded files. Each key in
508     ``FILES`` is the ``name`` from the ``<input type="file" name="" />``. Each
509-    value in ``FILES`` is a standard Python dictionary with the following three
510+    value in ``FILES`` is a standard Python dictionary with the following four
511     keys:
512 
513         * ``filename`` -- The name of the uploaded file, as a Python string.
514         * ``content-type`` -- The content type of the uploaded file.
515         * ``content`` -- The raw content of the uploaded file.
516+        * ``content-length`` -- The length of the content in bytes.
517 
518+    If streaming file uploads are enabled two additional keys
519+    describing the uploaded file will be present:
520+
521+       * ``tmpfilename`` -- The filename for the temporary file.
522+       * ``tmpfile`` -- An open file object for the temporary file.
523+
524+    The temporary file will be removed when the request finishes.
525+
526+    Note that accessing ``content`` when streaming uploads are enabled
527+    will read the whole file into memory which may not be what you want.
528+
529     Note that ``FILES`` will only contain data if the request method was POST
530     and the ``<form>`` that posted to the request had
531     ``enctype="multipart/form-data"``. Otherwise, ``FILES`` will be a blank
532Index: docs/settings.txt
533===================================================================
534--- docs/settings.txt   (revision 5722)
535+++ docs/settings.txt   (working copy)
536@@ -472,6 +472,15 @@
537 
538 .. _Testing Django Applications: ../testing/
539 
540+FILE_UPLOAD_DIR
541+---------------
542+
543+Default: ``None``
544+
545+Path to a directory where temporary files should be written during
546+file uploads. Leaving this as ``None`` will disable streaming file uploads,
547+and cause all uploaded files to be stored (temporarily) in memory.
548+
549 IGNORABLE_404_ENDS
550 ------------------
551 
552@@ -788,6 +797,16 @@
553 
554 .. _site framework docs: ../sites/
555 
556+STREAMING_MIN_POST_SIZE
557+-----------------------
558+
559+Default: 524288 (``512*1024``)
560+
561+An integer specifying the minimum number of bytes that has to be
562+received (in a POST) for file upload streaming to take place. Any
563+request smaller than this will be handled in memory.
564+Note: ``FILE_UPLOAD_DIR`` has to be defined to enable streaming.
565+
566 TEMPLATE_CONTEXT_PROCESSORS
567 ---------------------------
568 
569Index: docs/forms.txt
570===================================================================
571--- docs/forms.txt      (revision 5722)
572+++ docs/forms.txt      (working copy)
573@@ -475,6 +475,19 @@
574    new_data = request.POST.copy()
575    new_data.update(request.FILES)
576 
577+Streaming file uploads.
578+-----------------------
579+
580+File uploads will be read into memory by default. This works fine for
581+small to medium sized uploads (from 1MB to 100MB depending on your
582+setup and usage). If you want to support larger uploads you can enable
583+upload streaming where only a small part of the file will be in memory
584+at any time. To do this you need to specify the ``FILE_UPLOAD_DIR``
585+setting (see the settings_ document for more details).
586+
587+See `request object`_ for more details about ``request.FILES`` objects
588+with streaming file uploads enabled.
589+
590 Validators
591 ==========
592 
593@@ -698,3 +711,4 @@
594 .. _`generic views`: ../generic_views/
595 .. _`models API`: ../model-api/
596 .. _settings: ../settings/
597+.. _request object: ../request_response/#httprequest-objects