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() |
---|