Ticket #2070: streaming.7092.patch.partial_tests_fix

File streaming.7092.patch.partial_tests_fix, 48.6 KB (added by Faheem Mitha, 16 years ago)

Slightly modified version of streaming.7092.patch with more tests passing.

Line 
1diff -r 8f50398714c1 -r ea52e616a876 django/conf/global_settings.py
2--- a/django/conf/global_settings.py    Fri Feb 08 07:01:23 2008 -0500
3+++ b/django/conf/global_settings.py    Fri Feb 08 15:41:48 2008 -0500
4@@ -257,6 +257,16 @@ DEFAULT_TABLESPACE = ''
5 DEFAULT_TABLESPACE = ''
6 DEFAULT_INDEX_TABLESPACE = ''
7 
8+# The directory to place streamed file uploads. The web server needs write
9+# permissions on this directory.
10+# If this is None, streaming uploads are disabled.
11+FILE_UPLOAD_DIR = None
12+
13+# The minimum size of a POST before file uploads are streamed to disk.
14+# Any less than this number, and the file is uploaded to memory.
15+# Size is in bytes.
16+STREAMING_MIN_POST_SIZE = 512 * (2**10)
17+
18 ##############
19 # MIDDLEWARE #
20 ##############
21diff -r 8f50398714c1 -r ea52e616a876 django/core/handlers/modpython.py
22--- a/django/core/handlers/modpython.py Fri Feb 08 07:01:23 2008 -0500
23+++ b/django/core/handlers/modpython.py Fri Feb 08 15:41:48 2008 -0500
24@@ -52,7 +52,12 @@ class ModPythonRequest(http.HttpRequest)
25     def _load_post_and_files(self):
26         "Populates self._post and self._files"
27         if 'content-type' in self._req.headers_in and self._req.headers_in['content-type'].startswith('multipart'):
28-            self._post, self._files = http.parse_file_upload(self._req.headers_in, self.raw_post_data)
29+            self._raw_post_data = None # raw data is not available for streamed multipart messages
30+            try:
31+                self._post, self._files = http.parse_file_upload(self._req.headers_in, self._req, self)
32+            except:
33+                self._post, self._files = {}, {} # make sure we dont read the input stream again
34+                raise
35         else:
36             self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
37 
38@@ -97,20 +102,21 @@ class ModPythonRequest(http.HttpRequest)
39                 'AUTH_TYPE':         self._req.ap_auth_type,
40                 'CONTENT_LENGTH':    self._req.clength, # This may be wrong
41                 'CONTENT_TYPE':      self._req.content_type, # This may be wrong
42-                'GATEWAY_INTERFACE': 'CGI/1.1',
43-                'PATH_INFO':         self._req.path_info,
44-                'PATH_TRANSLATED':   None, # Not supported
45-                'QUERY_STRING':      self._req.args,
46-                'REMOTE_ADDR':       self._req.connection.remote_ip,
47-                'REMOTE_HOST':       None, # DNS lookups not supported
48-                'REMOTE_IDENT':      self._req.connection.remote_logname,
49-                'REMOTE_USER':       self._req.user,
50-                'REQUEST_METHOD':    self._req.method,
51-                'SCRIPT_NAME':       None, # Not supported
52-                'SERVER_NAME':       self._req.server.server_hostname,
53-                'SERVER_PORT':       self._req.server.port,
54-                'SERVER_PROTOCOL':   self._req.protocol,
55-                'SERVER_SOFTWARE':   'mod_python'
56+                'GATEWAY_INTERFACE':  'CGI/1.1',
57+                'PATH_INFO':          self._req.path_info,
58+                'PATH_TRANSLATED':    None, # Not supported
59+                'QUERY_STRING':       self._req.args,
60+                'REMOTE_ADDR':        self._req.connection.remote_ip,
61+                'REMOTE_HOST':        None, # DNS lookups not supported
62+                'REMOTE_IDENT':       self._req.connection.remote_logname,
63+                'REMOTE_USER':        self._req.user,
64+                'REQUEST_METHOD':     self._req.method,
65+                'SCRIPT_NAME':        None, # Not supported
66+                'SERVER_NAME':        self._req.server.server_hostname,
67+                'SERVER_PORT':        self._req.server.port,
68+                'SERVER_PROTOCOL':    self._req.protocol,
69+                'UPLOAD_PROGRESS_ID': self._get_file_progress_id(),
70+                'SERVER_SOFTWARE':    'mod_python'
71             }
72             for key, value in self._req.headers_in.items():
73                 key = 'HTTP_' + key.upper().replace('-', '_')
74@@ -126,6 +132,17 @@ class ModPythonRequest(http.HttpRequest)
75 
76     def _get_method(self):
77         return self.META['REQUEST_METHOD'].upper()
78+
79+    def _get_file_progress_id(self):
80+        """
81+        Returns the Progress ID of the request,
82+        usually provided if there is a file upload
83+        going on.
84+        Returns ``None`` if no progress ID is specified.
85+        """
86+        return self._get_file_progress_from_args(self._req.headers_in,
87+                                                 self.GET,
88+                                                 self._req.args)
89 
90     GET = property(_get_get, _set_get)
91     POST = property(_get_post, _set_post)
92diff -r 8f50398714c1 -r ea52e616a876 django/core/handlers/wsgi.py
93--- a/django/core/handlers/wsgi.py      Fri Feb 08 07:01:23 2008 -0500
94+++ b/django/core/handlers/wsgi.py      Fri Feb 08 15:41:48 2008 -0500
95@@ -77,6 +77,7 @@ class WSGIRequest(http.HttpRequest):
96         self.environ = environ
97         self.path = force_unicode(environ['PATH_INFO'])
98         self.META = environ
99+        self.META['UPLOAD_PROGRESS_ID'] = self._get_file_progress_id()
100         self.method = environ['REQUEST_METHOD'].upper()
101 
102     def __repr__(self):
103@@ -114,7 +115,14 @@ class WSGIRequest(http.HttpRequest):
104             if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
105                 header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')])
106                 header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '')
107-                self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data)
108+                header_dict['Content-Length'] = self.environ.get('CONTENT_LENGTH', '')
109+                header_dict['X-Progress-ID'] = self.environ.get('HTTP_X_PROGRESS_ID', '')
110+                try:
111+                    self._post, self._files = http.parse_file_upload(header_dict, self.environ['wsgi.input'], self)
112+                except:
113+                    self._post, self._files = {}, {} # make sure we dont read the input stream again
114+                    raise
115+                self._raw_post_data = None # raw data is not available for streamed multipart messages
116             else:
117                 self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
118         else:
119@@ -172,6 +180,17 @@ class WSGIRequest(http.HttpRequest):
120             buf.close()
121             return self._raw_post_data
122 
123+    def _get_file_progress_id(self):
124+        """
125+        Returns the Progress ID of the request,
126+        usually provided if there is a file upload
127+        going on.
128+        Returns ``None`` if no progress ID is specified.
129+        """
130+        return self._get_file_progress_from_args(self.environ,
131+                                                 self.GET,
132+                                                 self.environ.get('QUERY_STRING', ''))
133+
134     GET = property(_get_get, _set_get)
135     POST = property(_get_post, _set_post)
136     COOKIES = property(_get_cookies, _set_cookies)
137diff -r 8f50398714c1 -r ea52e616a876 django/core/validators.py
138--- a/django/core/validators.py Fri Feb 08 07:01:23 2008 -0500
139+++ b/django/core/validators.py Fri Feb 08 15:41:48 2008 -0500
140@@ -177,17 +177,17 @@ def isValidImage(field_data, all_data):
141     from PIL import Image
142     from cStringIO import StringIO
143     try:
144-        content = field_data['content']
145+        filename = field_data['filename']
146     except TypeError:
147         raise ValidationError, _("No file was submitted. Check the encoding type on the form.")
148     try:
149         # load() is the only method that can spot a truncated JPEG,
150         #  but it cannot be called sanely after verify()
151-        trial_image = Image.open(StringIO(content))
152+        trial_image = Image.open(field_data.get('tmpfilename') or StringIO(field_data.get('content','')))
153         trial_image.load()
154         # verify() is the only method that can spot a corrupt PNG,
155         #  but it must be called immediately after the constructor
156-        trial_image = Image.open(StringIO(content))
157+        trial_image = Image.open(field_data.get('tmpfilename') or StringIO(field_data.get('content','')))
158         trial_image.verify()
159     except Exception: # Python Imaging Library doesn't recognize it as an image
160         raise ValidationError, _("Upload a valid image. The file you uploaded was either not an image or a corrupted image.")
161diff -r 8f50398714c1 -r ea52e616a876 django/db/models/base.py
162--- a/django/db/models/base.py  Fri Feb 08 07:01:23 2008 -0500
163+++ b/django/db/models/base.py  Fri Feb 08 15:41:48 2008 -0500
164@@ -12,6 +12,7 @@ from django.dispatch import dispatcher
165 from django.dispatch import dispatcher
166 from django.utils.datastructures import SortedDict
167 from django.utils.functional import curry
168+from django.utils.file import file_move_safe
169 from django.utils.encoding import smart_str, force_unicode, smart_unicode
170 from django.conf import settings
171 from itertools import izip
172@@ -379,12 +380,16 @@ class Model(object):
173     def _get_FIELD_size(self, field):
174         return os.path.getsize(self._get_FIELD_filename(field))
175 
176-    def _save_FIELD_file(self, field, filename, raw_contents, save=True):
177+    def _save_FIELD_file(self, field, filename, raw_field, save=True):
178         directory = field.get_directory_name()
179         try: # Create the date-based directory if it doesn't exist.
180             os.makedirs(os.path.join(settings.MEDIA_ROOT, directory))
181         except OSError: # Directory probably already exists.
182             pass
183+
184+        if filename is None:
185+            filename = raw_field['filename']
186+
187         filename = field.get_filename(filename)
188 
189         # If the filename already exists, keep adding an underscore to the name of
190@@ -401,9 +406,16 @@ class Model(object):
191         setattr(self, field.attname, filename)
192 
193         full_filename = self._get_FIELD_filename(field)
194-        fp = open(full_filename, 'wb')
195-        fp.write(raw_contents)
196-        fp.close()
197+        if raw_field.has_key('tmpfilename'):
198+            raw_field['tmpfile'].close()
199+            file_move_safe(raw_field['tmpfilename'], full_filename)
200+        else:
201+            from django.utils import file_locks
202+            fp = open(full_filename, 'wb')
203+            # exclusive lock
204+            file_locks.lock(fp, file_locks.LOCK_EX)
205+            fp.write(raw_field['content'])
206+            fp.close()
207 
208         # Save the width and/or height, if applicable.
209         if isinstance(field, ImageField) and (field.width_field or field.height_field):
210diff -r 8f50398714c1 -r ea52e616a876 django/db/models/fields/__init__.py
211--- a/django/db/models/fields/__init__.py       Fri Feb 08 07:01:23 2008 -0500
212+++ b/django/db/models/fields/__init__.py       Fri Feb 08 15:41:48 2008 -0500
213@@ -761,7 +761,8 @@ class FileField(Field):
214         setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
215         setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
216         setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
217-        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save))
218+        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save))
219+        setattr(cls, 'move_%s_file' % self.name, lambda instance, raw_field, save=True: instance._save_FIELD_file(self, None, raw_field, save))       
220         dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
221 
222     def delete_file(self, instance):
223@@ -784,9 +785,9 @@ class FileField(Field):
224         if new_data.get(upload_field_name, False):
225             func = getattr(new_object, 'save_%s_file' % self.name)
226             if rel:
227-                func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"], save)
228+                func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0], save)
229             else:
230-                func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"], save)
231+                func(new_data[upload_field_name]["filename"], new_data[upload_field_name], save)
232 
233     def get_directory_name(self):
234         return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
235@@ -799,7 +800,7 @@ class FileField(Field):
236     def save_form_data(self, instance, data):
237         from django.newforms.fields import UploadedFile
238         if data and isinstance(data, UploadedFile):
239-            getattr(instance, "save_%s_file" % self.name)(data.filename, data.content, save=False)
240+            getattr(instance, "save_%s_file" % self.name)(data.filename, data.data, save=False)
241 
242     def formfield(self, **kwargs):
243         defaults = {'form_class': forms.FileField}
244diff -r 8f50398714c1 -r ea52e616a876 django/http/__init__.py
245--- a/django/http/__init__.py   Fri Feb 08 07:01:23 2008 -0500
246+++ b/django/http/__init__.py   Fri Feb 08 15:41:48 2008 -0500
247@@ -1,11 +1,16 @@ import os
248 import os
249+import re
250 from Cookie import SimpleCookie
251 from pprint import pformat
252 from urllib import urlencode
253 from urlparse import urljoin
254+from django.http.utils import str_to_unicode
255+from django.http.multipartparser import MultiPartParser, MultiPartParserError
256 from django.utils.datastructures import MultiValueDict, FileDict
257 from django.utils.encoding import smart_str, iri_to_uri, force_unicode
258 from utils import *
259+
260+upload_id_re = re.compile(r'^[a-fA-F0-9]{32}$') # file progress id Regular expression
261 
262 RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
263 
264@@ -79,7 +84,7 @@ class HttpRequest(object):
265 
266     def is_secure(self):
267         return os.environ.get("HTTPS") == "on"
268-
269+       
270     def _set_encoding(self, val):
271         """
272         Sets the encoding used for GET/POST accesses. If the GET or POST
273@@ -97,38 +102,54 @@ class HttpRequest(object):
274 
275     encoding = property(_get_encoding, _set_encoding)
276 
277-def parse_file_upload(header_dict, post_data):
278-    "Returns a tuple of (POST QueryDict, FILES MultiValueDict)"
279-    import email, email.Message
280-    from cgi import parse_header
281-    raw_message = '\r\n'.join(['%s:%s' % pair for pair in header_dict.items()])
282-    raw_message += '\r\n\r\n' + post_data
283-    msg = email.message_from_string(raw_message)
284-    POST = QueryDict('', mutable=True)
285-    FILES = MultiValueDict()
286-    for submessage in msg.get_payload():
287-        if submessage and isinstance(submessage, email.Message.Message):
288-            name_dict = parse_header(submessage['Content-Disposition'])[1]
289-            # name_dict is something like {'name': 'file', 'filename': 'test.txt'} for file uploads
290-            # or {'name': 'blah'} for POST fields
291-            # We assume all uploaded files have a 'filename' set.
292-            if 'filename' in name_dict:
293-                assert type([]) != type(submessage.get_payload()), "Nested MIME messages are not supported"
294-                if not name_dict['filename'].strip():
295-                    continue
296-                # IE submits the full path, so trim everything but the basename.
297-                # (We can't use os.path.basename because that uses the server's
298-                # directory separator, which may not be the same as the
299-                # client's one.)
300-                filename = name_dict['filename'][name_dict['filename'].rfind("\\")+1:]
301-                FILES.appendlist(name_dict['name'], FileDict({
302-                    'filename': filename,
303-                    'content-type': 'Content-Type' in submessage and submessage['Content-Type'] or None,
304-                    'content': submessage.get_payload(),
305-                }))
306-            else:
307-                POST.appendlist(name_dict['name'], submessage.get_payload())
308-    return POST, FILES
309+    def _get_file_progress(self):
310+        return {}
311+   
312+    def _set_file_progress(self,value):
313+        pass
314+
315+    def _del_file_progress(self):
316+        pass
317+
318+    file_progress = property(_get_file_progress,
319+                             _set_file_progress,
320+                             _del_file_progress)
321+
322+    def _get_file_progress_from_args(self, headers, get, querystring):
323+        """
324+        This parses the request for a file progress_id value.
325+        Note that there are two distinct ways of getting the progress
326+        ID -- header and GET. One is used primarily to attach via JavaScript
327+        to the end of an HTML form action while the other is used for AJAX
328+        communication.
329+
330+        All progress IDs must be valid 32-digit hexadecimal numbers.
331+        """
332+        if 'X-Upload-ID' in headers:
333+            progress_id = headers['X-Upload-ID']
334+        elif 'progress_id' in get:
335+            progress_id = get['progress_id']
336+        else:
337+            return None
338+
339+        if not upload_id_re.match(progress_id):
340+            return None
341+
342+        return progress_id
343+
344+def parse_file_upload(headers, input, request):
345+    from django.conf import settings
346+
347+    # Only stream files to disk if FILE_STREAMING_DIR is set
348+    file_upload_dir = settings.FILE_UPLOAD_DIR
349+    streaming_min_post_size = settings.STREAMING_MIN_POST_SIZE
350+
351+    try:
352+        parser = MultiPartParser(headers, input, request, file_upload_dir, streaming_min_post_size)
353+        return parser.parse()
354+    except MultiPartParserError, e:
355+        return MultiValueDict({ '_file_upload_error': [e.message] }), {}
356+
357 
358 class QueryDict(MultiValueDict):
359     """
360@@ -413,20 +434,3 @@ class HttpResponseServerError(HttpRespon
361 # A backwards compatible alias for HttpRequest.get_host.
362 def get_host(request):
363     return request.get_host()
364-
365-# It's neither necessary nor appropriate to use
366-# django.utils.encoding.smart_unicode for parsing URLs and form inputs. Thus,
367-# this slightly more restricted function.
368-def str_to_unicode(s, encoding):
369-    """
370-    Convert basestring objects to unicode, using the given encoding. Illegaly
371-    encoded input characters are replaced with Unicode "unknown" codepoint
372-    (\ufffd).
373-
374-    Returns any non-basestring objects without change.
375-    """
376-    if isinstance(s, str):
377-        return unicode(s, encoding, 'replace')
378-    else:
379-        return s
380-
381diff -r 8f50398714c1 -r ea52e616a876 django/http/multipartparser.py
382--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
383+++ b/django/http/multipartparser.py    Fri Feb 08 15:41:48 2008 -0500
384@@ -0,0 +1,328 @@
385+"""
386+MultiPart parsing for file uploads.
387+If both a progress id is sent (either through ``X-Progress-ID``
388+header or ``progress_id`` GET) and ``FILE_UPLOAD_DIR`` is set
389+in the settings, then the file progress will be tracked using
390+``request.file_progress``.
391+
392+To use this feature, consider creating a middleware with an appropriate
393+``process_request``::
394+
395+    class FileProgressTrack(object):
396+        def __get__(self, request, HttpRequest):
397+            progress_id = request.META['UPLOAD_PROGRESS_ID']
398+            status = # get progress from progress_id here
399+
400+            return status
401+
402+        def __set__(self, request, new_value):
403+            progress_id = request.META['UPLOAD_PROGRESS_ID']
404+
405+            # set the progress using progress_id here.
406+
407+    # example middleware
408+    class FileProgressExample(object):
409+        def process_request(self, request):
410+            request.__class__.file_progress = FileProgressTrack()
411+
412+
413+
414+"""
415+
416+__all__ = ['MultiPartParserError','MultiPartParser']
417+
418+
419+from django.utils.datastructures import MultiValueDict
420+from django.http.utils import str_to_unicode
421+from django.conf import settings
422+import os
423+
424+try:
425+    from cStringIO import StringIO
426+except ImportError:
427+    from StringIO import StringIO
428+
429+
430+class MultiPartParserError(Exception):
431+    def __init__(self, message):
432+        self.message = message
433+    def __str__(self):
434+        return repr(self.message)
435+
436+class MultiPartParser(object):
437+    """
438+    A rfc2388 multipart/form-data parser.
439+   
440+    parse() reads the input stream in chunk_size chunks and returns a
441+    tuple of (POST MultiValueDict, FILES MultiValueDict). If
442+    file_upload_dir is defined files will be streamed to temporary
443+    files in the specified directory.
444+
445+    The FILES dictionary will have 'filename', 'content-type',
446+    'content' and 'content-length' entries. For streamed files it will
447+    also have 'tmpfilename' and 'tmpfile'. The 'content' entry will
448+    only be read from disk when referenced for streamed files.
449+
450+    If the X-Progress-ID is sent (in one of many formats), then
451+    object.file_progress will be given a dictionary of the progress.
452+    """
453+    def __init__(self, headers, input, request, file_upload_dir=None, streaming_min_post_size=None, chunk_size=1024*64):
454+        try:
455+            content_length = int(headers['Content-Length'])
456+        except:
457+            raise MultiPartParserError('Invalid Content-Length: %s' % headers.get('Content-Length'))
458+
459+        content_type = headers.get('Content-Type')
460+
461+        if not content_type or not content_type.startswith('multipart/'):
462+            raise MultiPartParserError('Invalid Content-Type: %s' % content_type)
463+           
464+        ctype, opts = self.parse_header(content_type)
465+        boundary = opts.get('boundary')
466+        from cgi import valid_boundary
467+        if not boundary or not valid_boundary(boundary):
468+            raise MultiPartParserError('Invalid boundary in multipart form: %s' % boundary)
469+
470+        progress_id = request.META['UPLOAD_PROGRESS_ID']
471+
472+        self._track_progress = file_upload_dir and progress_id # whether or not to track progress
473+        self._boundary = '--' + boundary
474+        self._input = input
475+        self._size = content_length
476+        self._received = 0
477+        self._file_upload_dir = file_upload_dir
478+        self._chunk_size = chunk_size
479+        self._state = 'PREAMBLE'
480+        self._partial = ''
481+        self._post = MultiValueDict()
482+        self._files = MultiValueDict()
483+        self._request = request
484+        self._encoding = request.encoding or settings.DEFAULT_CHARSET
485+
486+        if streaming_min_post_size is not None and content_length < streaming_min_post_size:
487+            self._file_upload_dir = None # disable file streaming for small request
488+        elif self._track_progress:
489+            request.file_progress = {'state': 'starting'}
490+
491+        try:
492+            # Use mx fast string search if available.
493+            from mx.TextTools import FS
494+            self._fs = FS(self._boundary)
495+        except ImportError:
496+            self._fs = None
497+
498+    def parse(self):
499+        try:
500+            self._parse()
501+        finally:
502+            if self._track_progress:
503+                self._request.file_progress = {'state': 'done'}
504+        return self._post, self._files
505+
506+    def _parse(self):
507+        size = self._size
508+
509+        try:
510+            while size > 0:
511+                n = self._read(self._input, min(self._chunk_size, size))
512+                if not n:
513+                    break
514+                size -= n
515+        except:
516+            # consume any remaining data so we dont generate a "Connection Reset" error
517+            size = self._size - self._received
518+            while size > 0:
519+                data = self._input.read(min(self._chunk_size, size))
520+                size -= len(data)
521+            raise
522+
523+    def _find_boundary(self, data, start, stop):
524+        """
525+        Find the next boundary and return the end of current part
526+        and start of next part.
527+        """
528+        if self._fs:
529+            boundary = self._fs.find(data, start, stop)
530+        else:
531+            boundary = data.find(self._boundary, start, stop)
532+        if boundary >= 0:
533+            end = boundary
534+            next = boundary + len(self._boundary)
535+
536+            # backup over CRLF
537+            if end > 0 and data[end-1] == '\n': end -= 1
538+            if end > 0 and data[end-1] == '\r': end -= 1
539+            # skip over --CRLF
540+            if next < stop and data[next] == '-': next += 1
541+            if next < stop and data[next] == '-': next += 1
542+            if next < stop and data[next] == '\r': next += 1
543+            if next < stop and data[next] == '\n': next += 1
544+
545+            return True, end, next
546+        else:
547+            return False, stop, stop
548+
549+    class TemporaryFile(object):
550+        "A temporary file that tries to delete itself when garbage collected."
551+        def __init__(self, dir):
552+            import tempfile
553+            (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir)
554+            self.file = os.fdopen(fd, 'w+b')
555+            self.name = name
556+
557+        def __getattr__(self, name):
558+            a = getattr(self.__dict__['file'], name)
559+            if type(a) != type(0):
560+                setattr(self, name, a)
561+            return a
562+
563+        def __del__(self):
564+            try:
565+                os.unlink(self.name)
566+            except OSError:
567+                pass
568+
569+    class LazyContent(dict):
570+        """
571+        A lazy FILES dictionary entry that reads the contents from
572+        tmpfile only when referenced.
573+        """
574+        def __init__(self, data):
575+            dict.__init__(self, data)
576+
577+        def __getitem__(self, key):
578+            if key == 'content' and not self.has_key(key):
579+                self['tmpfile'].seek(0)
580+                self['content'] = self['tmpfile'].read()
581+            return dict.__getitem__(self, key)
582+
583+    def _read(self, input, size):
584+        data = input.read(size)
585+
586+        if not data:
587+            return 0
588+
589+        read_size = len(data)
590+        self._received += read_size
591+
592+        if self._partial:
593+            data = self._partial + data
594+
595+        start = 0
596+        stop = len(data)
597+
598+        while start < stop:
599+            boundary, end, next = self._find_boundary(data, start, stop)
600+
601+            if not boundary and read_size:
602+                # make sure we dont treat a partial boundary (and its separators) as data
603+                stop -= len(self._boundary) + 16
604+                end = next = stop
605+                if end <= start:
606+                    break # need more data
607+
608+            if self._state == 'PREAMBLE':
609+                # Preamble, just ignore it
610+                self._state = 'HEADER'
611+
612+            elif self._state == 'HEADER':
613+                # Beginning of header, look for end of header and parse it if found.
614+
615+                header_end = data.find('\r\n\r\n', start, stop)
616+                if header_end == -1:
617+                    break # need more data
618+
619+                header = data[start:header_end]
620+
621+                self._fieldname = None
622+                self._filename = None
623+                self._content_type = None
624+
625+                for line in header.split('\r\n'):
626+                    ctype, opts = self.parse_header(line)
627+                    if ctype == 'content-disposition: form-data':
628+                        self._fieldname = opts.get('name')
629+                        self._filename = opts.get('filename')
630+                    elif ctype.startswith('content-type: '):
631+                        self._content_type = ctype[14:]
632+
633+                if self._filename is not None:
634+                    # cleanup filename from IE full paths:
635+                    self._filename = self._filename[self._filename.rfind("\\")+1:].strip()
636+
637+                    if self._filename: # ignore files without filenames
638+                        if self._file_upload_dir:
639+                            try:
640+                                self._file = self.TemporaryFile(dir=self._file_upload_dir)
641+                            except (OSError, IOError), e:
642+                                raise MultiPartParserError("Failed to create temporary file. Error was %s" % e)
643+                        else:
644+                            self._file = StringIO()
645+                    else:
646+                        self._file = None
647+                    self._filesize = 0
648+                    self._state = 'FILE'
649+                else:
650+                    self._field = StringIO()
651+                    self._state = 'FIELD'
652+                next = header_end + 4
653+
654+            elif self._state == 'FIELD':
655+                # In a field, collect data until a boundary is found.
656+
657+                self._field.write(data[start:end])
658+                if boundary:
659+                    if self._fieldname:
660+                        self._post.appendlist(self._fieldname, str_to_unicode(self._field.getvalue(), self._encoding))
661+                    self._field.close()
662+                    self._state = 'HEADER'
663+
664+            elif self._state == 'FILE':
665+                # In a file, collect data until a boundary is found.
666+
667+                if self._file:
668+                    try:
669+                        self._file.write(data[start:end])
670+                    except IOError, e:
671+                        raise MultiPartParserError("Failed to write to temporary file.")
672+                    self._filesize += end-start
673+
674+                    if self._track_progress:
675+                        self._request.file_progress = {'received': self._received,
676+                                                       'size':     self._size,
677+                                                       'state':    'uploading'}
678+
679+                if boundary:
680+                    if self._file:
681+                        if self._file_upload_dir:
682+                            self._file.seek(0)
683+                            file = self.LazyContent({
684+                                'filename': str_to_unicode(self._filename, self._encoding),
685+                                'content-type':  self._content_type,
686+                                # 'content': is read on demand
687+                                'content-length': self._filesize,
688+                                'tmpfilename': self._file.name,
689+                                'tmpfile': self._file
690+                            })
691+                        else:
692+                            file = {
693+                                'filename': str_to_unicode(self._filename, self._encoding),
694+                                'content-type':  self._content_type,
695+                                'content': self._file.getvalue(),
696+                                'content-length': self._filesize
697+                            }
698+                            self._file.close()
699+
700+                        self._files.appendlist(self._fieldname, file)
701+
702+                    self._state = 'HEADER'
703+
704+            start = next
705+
706+        self._partial = data[start:]
707+
708+        return read_size
709+
710+    def parse_header(self, line):
711+        from cgi import parse_header
712+        return parse_header(line)
713diff -r 8f50398714c1 -r ea52e616a876 django/http/utils.py
714--- a/django/http/utils.py      Fri Feb 08 07:01:23 2008 -0500
715+++ b/django/http/utils.py      Fri Feb 08 15:41:48 2008 -0500
716@@ -1,3 +1,19 @@
717+# It's neither necessary nor appropriate to use
718+# django.utils.encoding.smart_unicode for parsing URLs and form inputs. Thus,
719+# this slightly more restricted function.
720+def str_to_unicode(s, encoding):
721+    """
722+    Convert basestring objects to unicode, using the given encoding. Illegaly
723+    encoded input characters are replaced with Unicode "unknown" codepoint
724+    (\ufffd).
725+
726+    Returns any non-basestring objects without change.
727+    """
728+    if isinstance(s, str):
729+        return unicode(s, encoding, 'replace')
730+    else:
731+        return s
732+
733 """
734 Functions that modify an HTTP request or response in some way.
735 """
736diff -r 8f50398714c1 -r ea52e616a876 django/newforms/fields.py
737--- a/django/newforms/fields.py Fri Feb 08 07:01:23 2008 -0500
738+++ b/django/newforms/fields.py Fri Feb 08 15:41:48 2008 -0500
739@@ -415,9 +415,9 @@ except ImportError:
740 
741 class UploadedFile(StrAndUnicode):
742     "A wrapper for files uploaded in a FileField"
743-    def __init__(self, filename, content):
744+    def __init__(self, filename, data):
745         self.filename = filename
746-        self.content = content
747+        self.data = data
748 
749     def __unicode__(self):
750         """
751@@ -444,12 +444,12 @@ class FileField(Field):
752         elif not data and initial:
753             return initial
754         try:
755-            f = UploadedFile(data['filename'], data['content'])
756+            f = UploadedFile(data['filename'], data)
757         except TypeError:
758             raise ValidationError(self.error_messages['invalid'])
759         except KeyError:
760             raise ValidationError(self.error_messages['missing'])
761-        if not f.content:
762+        if not f.data.get('content-length'):
763             raise ValidationError(self.error_messages['empty'])
764         return f
765 
766@@ -473,11 +473,11 @@ class ImageField(FileField):
767         try:
768             # load() is the only method that can spot a truncated JPEG,
769             #  but it cannot be called sanely after verify()
770-            trial_image = Image.open(StringIO(f.content))
771+            trial_image = Image.open(f.data.get('tmpfilename') or StringIO(f.data['content']))
772             trial_image.load()
773             # verify() is the only method that can spot a corrupt PNG,
774             #  but it must be called immediately after the constructor
775-            trial_image = Image.open(StringIO(f.content))
776+            trial_image = Image.open(f.data.get('tmpfilename') or StringIO(f.data['content']))
777             trial_image.verify()
778         except Exception: # Python Imaging Library doesn't recognize it as an image
779             raise ValidationError(self.error_messages['invalid_image'])
780diff -r 8f50398714c1 -r ea52e616a876 django/oldforms/__init__.py
781--- a/django/oldforms/__init__.py       Fri Feb 08 07:01:23 2008 -0500
782+++ b/django/oldforms/__init__.py       Fri Feb 08 15:41:48 2008 -0500
783@@ -681,16 +681,21 @@ class FileUploadField(FormField):
784         self.validator_list = [self.isNonEmptyFile] + validator_list
785 
786     def isNonEmptyFile(self, field_data, all_data):
787-        try:
788-            content = field_data['content']
789-        except TypeError:
790+        if field_data.has_key('_file_upload_error'):
791+            raise validators.CriticalValidationError, field_data['_file_upload_error']
792+        if not field_data.has_key('filename'):
793             raise validators.CriticalValidationError, ugettext("No file was submitted. Check the encoding type on the form.")
794-        if not content:
795+        if not field_data['content-length']:
796             raise validators.CriticalValidationError, ugettext("The submitted file is empty.")
797 
798     def render(self, data):
799         return mark_safe(u'<input type="file" id="%s" class="v%s" name="%s" />' % \
800             (self.get_id(), self.__class__.__name__, self.field_name))
801+
802+    def prepare(self, new_data):
803+        if new_data.has_key('_file_upload_error'):
804+            # pretend we got something in the field to raise a validation error later
805+            new_data[self.field_name] = { '_file_upload_error': new_data['_file_upload_error'] }
806 
807     def html2python(data):
808         if data is None:
809diff -r 8f50398714c1 -r ea52e616a876 django/utils/file.py
810--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
811+++ b/django/utils/file.py      Fri Feb 08 15:41:48 2008 -0500
812@@ -0,0 +1,53 @@
813+import os
814+
815+__all__ = ['file_move_safe']
816+
817+try:
818+    import shutil
819+    file_move = shutil.move
820+except ImportError:
821+    file_move = os.rename
822+
823+def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_overwrite=False):
824+    """
825+    Moves a file from one location to another in the safest way possible.
826+   
827+    First, it tries using shutils.move, which is OS-dependent but doesn't
828+    break with change of filesystems. Then it tries os.rename, which will
829+    break if it encounters a change in filesystems. Lastly, it streams
830+    it manually from one file to another in python.
831+
832+    Without ``allow_overwrite``, if the destination file exists, the
833+    file will raise an IOError.
834+    """
835+
836+    from django.utils import file_locks
837+
838+    if old_file_name == new_file_name:
839+        # No file moving takes place.
840+        return
841+
842+    if not allow_overwrite and os.path.exists(new_file_name):
843+        raise IOError, "Django does not allow overwriting files."
844+
845+    try:
846+        file_move(old_file_name, new_file_name)
847+        return
848+    except OSError: # moving to another filesystem
849+        pass
850+
851+    new_file = open(new_file_name, 'wb')
852+    # exclusive lock
853+    file_locks.lock(new_file, file_locks.LOCK_EX)
854+    old_file = open(old_file_name, 'rb')
855+    current_chunk = None
856+
857+    while current_chunk != '':
858+        current_chunk = old_file.read(chunk_size)
859+        new_file.write(current_chunk)
860+
861+    new_file.close()
862+    old_file.close()
863+
864+    os.remove(old_file_name)
865+
866diff -r 8f50398714c1 -r ea52e616a876 django/utils/file_locks.py
867--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
868+++ b/django/utils/file_locks.py        Fri Feb 08 15:41:48 2008 -0500
869@@ -0,0 +1,50 @@
870+"""
871+Locking portability by Jonathan Feignberg <jdf@pobox.com> in python cookbook
872+
873+Example Usage::
874+
875+    from django.utils import file_locks
876+
877+    f = open('./file', 'wb')
878+
879+    file_locks.lock(f, file_locks.LOCK_EX)
880+    f.write('Django')
881+    f.close()
882+"""
883+
884+
885+import os
886+
887+__all__ = ['LOCK_EX','LOCK_SH','LOCK_NB','lock','unlock']
888+
889+if os.name == 'nt':
890+       import win32con
891+       import win32file
892+       import pywintypes
893+       LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
894+       LOCK_SH = 0
895+       LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
896+       __overlapped = pywintypes.OVERLAPPED()
897+elif os.name == 'posix':
898+       import fcntl
899+       LOCK_EX = fcntl.LOCK_EX
900+       LOCK_SH = fcntl.LOCK_SH
901+       LOCK_NB = fcntl.LOCK_NB
902+else:
903+       raise RuntimeError("Locking only defined for nt and posix platforms")
904+
905+if os.name == 'nt':
906+       def lock(file, flags):
907+               hfile = win32file._get_osfhandle(file.fileno())
908+               win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped)
909+
910+       def unlock(file):
911+               hfile = win32file._get_osfhandle(file.fileno())
912+               win32file.UnlockFileEx(hfile, 0, -0x10000, __overlapped)
913+
914+elif os.name =='posix':
915+       def lock(file, flags):
916+               fcntl.flock(file.fileno(), flags)
917+
918+       def unlock(file):
919+               fcntl.flock(file.fileno(), fcntl.LOCK_UN)
920diff -r 8f50398714c1 -r ea52e616a876 docs/forms.txt
921--- a/docs/forms.txt    Fri Feb 08 07:01:23 2008 -0500
922+++ b/docs/forms.txt    Fri Feb 08 15:41:48 2008 -0500
923@@ -475,6 +475,19 @@ this::
924    new_data = request.POST.copy()
925    new_data.update(request.FILES)
926 
927+Streaming file uploads.
928+-----------------------
929+
930+File uploads will be read into memory by default. This works fine for
931+small to medium sized uploads (from 1MB to 100MB depending on your
932+setup and usage). If you want to support larger uploads you can enable
933+upload streaming where only a small part of the file will be in memory
934+at any time. To do this you need to specify the ``FILE_UPLOAD_DIR``
935+setting (see the settings_ document for more details).
936+
937+See `request object`_ for more details about ``request.FILES`` objects
938+with streaming file uploads enabled.
939+
940 Validators
941 ==========
942 
943@@ -698,3 +711,4 @@ fails. If no message is passed in, a def
944 .. _`generic views`: ../generic_views/
945 .. _`models API`: ../model-api/
946 .. _settings: ../settings/
947+.. _request object: ../request_response/#httprequest-objects
948diff -r 8f50398714c1 -r ea52e616a876 docs/request_response.txt
949--- a/docs/request_response.txt Fri Feb 08 07:01:23 2008 -0500
950+++ b/docs/request_response.txt Fri Feb 08 15:41:48 2008 -0500
951@@ -82,12 +82,24 @@ All attributes except ``session`` should
952 ``FILES``
953     A dictionary-like object containing all uploaded files. Each key in
954     ``FILES`` is the ``name`` from the ``<input type="file" name="" />``. Each
955-    value in ``FILES`` is a standard Python dictionary with the following three
956+    value in ``FILES`` is a standard Python dictionary with the following four
957     keys:
958 
959         * ``filename`` -- The name of the uploaded file, as a Python string.
960         * ``content-type`` -- The content type of the uploaded file.
961         * ``content`` -- The raw content of the uploaded file.
962+        * ``content-length`` -- The length of the content in bytes.
963+
964+    If streaming file uploads are enabled two additional keys
965+    describing the uploaded file will be present:
966+
967+       * ``tmpfilename`` -- The filename for the temporary file.
968+       * ``tmpfile`` -- An open file object for the temporary file.
969+
970+    The temporary file will be removed when the request finishes.
971+
972+    Note that accessing ``content`` when streaming uploads are enabled
973+    will read the whole file into memory which may not be what you want.
974 
975     Note that ``FILES`` will only contain data if the request method was POST
976     and the ``<form>`` that posted to the request had
977diff -r 8f50398714c1 -r ea52e616a876 docs/settings.txt
978--- a/docs/settings.txt Fri Feb 08 07:01:23 2008 -0500
979+++ b/docs/settings.txt Fri Feb 08 15:41:48 2008 -0500
980@@ -521,6 +521,15 @@ these paths should use Unix-style forwar
981 
982 .. _Testing Django Applications: ../testing/
983 
984+FILE_UPLOAD_DIR
985+---------------
986+
987+Default: ``None``
988+
989+Path to a directory where temporary files should be written during
990+file uploads. Leaving this as ``None`` will disable streaming file uploads,
991+and cause all uploaded files to be stored (temporarily) in memory.
992+
993 IGNORABLE_404_ENDS
994 ------------------
995 
996@@ -888,6 +897,16 @@ See the `site framework docs`_.
997 
998 .. _site framework docs: ../sites/
999 
1000+STREAMING_MIN_POST_SIZE
1001+-----------------------
1002+
1003+Default: 524288 (``512*1024``)
1004+
1005+An integer specifying the minimum number of bytes that has to be
1006+received (in a POST) for file upload streaming to take place. Any
1007+request smaller than this will be handled in memory.
1008+Note: ``FILE_UPLOAD_DIR`` has to be defined to enable streaming.
1009+
1010 TEMPLATE_CONTEXT_PROCESSORS
1011 ---------------------------
1012 
1013diff -r 8f50398714c1 -r ea52e616a876 tests/modeltests/model_forms/models.py
1014--- a/tests/modeltests/model_forms/models.py    Fri Feb 08 07:01:23 2008 -0500
1015+++ b/tests/modeltests/model_forms/models.py    Fri Feb 08 15:41:48 2008 -0500
1016@@ -736,7 +736,7 @@ False
1017 
1018 # Upload a file and ensure it all works as expected.
1019 
1020->>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test1.txt', 'content': 'hello world'}})
1021+>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test1.txt', 'content': 'hello world', 'content-length':len('hello world')}})
1022 >>> f.is_valid()
1023 True
1024 >>> type(f.cleaned_data['file'])
1025@@ -763,7 +763,7 @@ u'.../test1.txt'
1026 
1027 # Override the file by uploading a new one.
1028 
1029->>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test2.txt', 'content': 'hello world'}}, instance=instance)
1030+>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test2.txt', 'content': 'hello world', 'content-length':len('hello world')}}, instance=instance)
1031 >>> f.is_valid()
1032 True
1033 >>> instance = f.save()
1034@@ -782,7 +782,7 @@ True
1035 >>> instance.file
1036 ''
1037 
1038->>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test3.txt', 'content': 'hello world'}}, instance=instance)
1039+>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test3.txt', 'content': 'hello world', 'content-length':len('hello world')}}, instance=instance)
1040 >>> f.is_valid()
1041 True
1042 >>> instance = f.save()
1043@@ -802,7 +802,7 @@ u'.../test3.txt'
1044 
1045 >>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png")).read()
1046 
1047->>> f = ImageFileForm(data={'description': u'An image'}, files={'image': {'filename': 'test.png', 'content': image_data}})
1048+>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': {'filename': 'test.png', 'content': image_data}, 'content-length':len(image_data)})
1049 >>> f.is_valid()
1050 True
1051 >>> type(f.cleaned_data['image'])
1052@@ -829,7 +829,7 @@ u'.../test.png'
1053 
1054 # Override the file by uploading a new one.
1055 
1056->>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': {'filename': 'test2.png', 'content': image_data}}, instance=instance)
1057+>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': {'filename': 'test2.png', 'content': image_data}, 'content-length':len(image_data)}, instance=instance)
1058 >>> f.is_valid()
1059 True
1060 >>> instance = f.save()
1061@@ -848,7 +848,7 @@ True
1062 >>> instance.image
1063 ''
1064 
1065->>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': {'filename': 'test3.png', 'content': image_data}}, instance=instance)
1066+>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': {'filename': 'test3.png', 'content': image_data, 'content-length':len(image_data)}}, instance=instance)
1067 >>> f.is_valid()
1068 True
1069 >>> instance = f.save()
1070diff -r 8f50398714c1 -r ea52e616a876 tests/modeltests/test_client/models.py
1071--- a/tests/modeltests/test_client/models.py    Fri Feb 08 07:01:23 2008 -0500
1072+++ b/tests/modeltests/test_client/models.py    Fri Feb 08 15:41:48 2008 -0500
1073@@ -79,6 +79,21 @@ class ClientTest(TestCase):
1074         self.assertEqual(response.status_code, 200)
1075         self.assertEqual(response.template.name, "Book template")
1076         self.assertEqual(response.content, "Blink - Malcolm Gladwell")
1077+
1078+    def test_post_file_view(self):
1079+        "POST this python file to a view"
1080+        import os, tempfile
1081+        from django.conf import settings
1082+        file = __file__.replace('.pyc', '.py')
1083+        for upload_dir, streaming_size in [(None,512*1000), (tempfile.gettempdir(), 1)]:
1084+            settings.FILE_UPLOAD_DIR = upload_dir
1085+            settings.STREAMING_MIN_POST_SIZE = streaming_size
1086+            post_data = { 'name': file, 'file_file': open(file) }
1087+            response = self.client.post('/test_client/post_file_view/', post_data)
1088+            self.failUnless('models.py' in response.context['file']['filename'])
1089+            self.failUnless(len(response.context['file']['content']) == os.path.getsize(file))
1090+            if upload_dir:
1091+                self.failUnless(response.context['file']['tmpfilename'])
1092 
1093     def test_redirect(self):
1094         "GET a URL that redirects elsewhere"
1095diff -r 8f50398714c1 -r ea52e616a876 tests/modeltests/test_client/urls.py
1096--- a/tests/modeltests/test_client/urls.py      Fri Feb 08 07:01:23 2008 -0500
1097+++ b/tests/modeltests/test_client/urls.py      Fri Feb 08 15:41:48 2008 -0500
1098@@ -5,6 +5,7 @@ urlpatterns = patterns('',
1099 urlpatterns = patterns('',
1100     (r'^get_view/$', views.get_view),
1101     (r'^post_view/$', views.post_view),
1102+    (r'^post_file_view/$', views.post_file_view),
1103     (r'^raw_post_view/$', views.raw_post_view),
1104     (r'^redirect_view/$', views.redirect_view),
1105     (r'^permanent_redirect_view/$', redirect_to, { 'url': '/test_client/get_view/' }),
1106diff -r 8f50398714c1 -r ea52e616a876 tests/modeltests/test_client/views.py
1107--- a/tests/modeltests/test_client/views.py     Fri Feb 08 07:01:23 2008 -0500
1108+++ b/tests/modeltests/test_client/views.py     Fri Feb 08 15:41:48 2008 -0500
1109@@ -45,6 +45,12 @@ def raw_post_view(request):
1110         t = Template("GET request.", name="Book GET template")
1111         c = Context()
1112 
1113+    return HttpResponse(t.render(c))
1114+
1115+def post_file_view(request):
1116+    "A view that expects a multipart post and returns a file in the context"
1117+    t = Template('File {{ file.filename }} received', name='POST Template')
1118+    c = Context({'file': request.FILES['file_file']})
1119     return HttpResponse(t.render(c))
1120 
1121 def redirect_view(request):
1122diff -r 8f50398714c1 -r ea52e616a876 tests/regressiontests/forms/fields.py
1123--- a/tests/regressiontests/forms/fields.py     Fri Feb 08 07:01:23 2008 -0500
1124+++ b/tests/regressiontests/forms/fields.py     Fri Feb 08 15:41:48 2008 -0500
1125@@ -788,7 +788,7 @@ Traceback (most recent call last):
1126 ...
1127 ValidationError: [u'No file was submitted. Check the encoding type on the form.']
1128 
1129->>> f.clean({'filename': 'name', 'content': None})
1130+>>> f.clean({'filename': 'name', 'content': None, 'content-length': 0})
1131 Traceback (most recent call last):
1132 ...
1133 ValidationError: [u'The submitted file is empty.']
1134@@ -798,10 +798,10 @@ Traceback (most recent call last):
1135 ...
1136 ValidationError: [u'The submitted file is empty.']
1137 
1138->>> type(f.clean({'filename': 'name', 'content': 'Some File Content'}))
1139+>>> type(f.clean({'filename': 'name', 'content': 'Some File Content', 'content-length': len('Some File Content')}))
1140 <class 'django.newforms.fields.UploadedFile'>
1141 
1142->>> type(f.clean({'filename': 'name', 'content': 'Some File Content'}, 'files/test4.pdf'))
1143+>>> type(f.clean({'filename': 'name', 'content': 'Some File Content', 'content-length': len('Some File Content')}, 'files/test4.pdf'))
1144 <class 'django.newforms.fields.UploadedFile'>
1145 
1146 # URLField ##################################################################
1147diff -r 8f50398714c1 -r ea52e616a876 tests/regressiontests/forms/forms.py
1148--- a/tests/regressiontests/forms/forms.py      Fri Feb 08 07:01:23 2008 -0500
1149+++ b/tests/regressiontests/forms/forms.py      Fri Feb 08 15:41:48 2008 -0500
1150@@ -1410,7 +1410,7 @@ not request.POST.
1151 >>> print f
1152 <tr><th>File1:</th><td><ul class="errorlist"><li>No file was submitted. Check the encoding type on the form.</li></ul><input type="file" name="file1" /></td></tr>
1153 
1154->>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':'some content'}}, auto_id=False)
1155+>>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':'some content', 'content-length': len('some content')}}, auto_id=False)
1156 >>> print f
1157 <tr><th>File1:</th><td><input type="file" name="file1" /></td></tr>
1158 >>> f.is_valid()
Back to Top