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