| 1 | diff -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 | ##############
|
|---|
| 21 | diff -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)
|
|---|
| 92 | diff -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)
|
|---|
| 137 | diff -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.")
|
|---|
| 161 | diff -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):
|
|---|
| 210 | diff -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}
|
|---|
| 244 | diff -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 | -
|
|---|
| 381 | diff -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)
|
|---|
| 713 | diff -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 | """
|
|---|
| 736 | diff -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'])
|
|---|
| 780 | diff -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:
|
|---|
| 809 | diff -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 | +
|
|---|
| 866 | diff -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)
|
|---|
| 920 | diff -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
|
|---|
| 948 | diff -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
|
|---|
| 977 | diff -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 |
|
|---|
| 1013 | diff -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()
|
|---|
| 1070 | diff -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"
|
|---|
| 1095 | diff -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/' }),
|
|---|
| 1106 | diff -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):
|
|---|
| 1122 | diff -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 ##################################################################
|
|---|
| 1147 | diff -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()
|
|---|