Ticket #2070: 5343_cleaned_streaming_file_upload.diff
File 5343_cleaned_streaming_file_upload.diff, 37.4 KB (added by , 17 years ago) |
---|
-
django/http/multipartparser.py
1 """ 2 MultiPart parsing for file uploads. 3 If both a progress id is sent (either through ``X-Progress-ID`` 4 header or ``progress_id`` GET) and ``FILE_UPLOAD_DIR`` is set 5 in the settings, then the file progress will be tracked using 6 ``request.file_progress``. 7 8 To use this feature, consider creating a middleware with an appropriate 9 ``process_request``:: 10 11 class FileProgressTrack(object): 12 def __get__(self, request, HttpRequest): 13 progress_id = request.META['UPLOAD_PROGRESS_ID'] 14 status = # get progress from progress_id here 15 16 return status 17 18 def __set__(self, request, new_value): 19 progress_id = request.META['UPLOAD_PROGRESS_ID'] 20 21 # set the progress using progress_id here. 22 23 # example middleware 24 class FileProgressExample(object): 25 def process_request(self, request): 26 request.__class__.file_progress = FileProgressTrack() 27 28 29 30 """ 31 32 __all__ = ['MultiPartParserError','MultiPartParser'] 33 34 35 from django.utils.datastructures import MultiValueDict 36 import os 37 38 try: 39 from cStringIO import StringIO 40 except ImportError: 41 from StringIO import StringIO 42 43 44 class MultiPartParserError(Exception): 45 def __init__(self, message): 46 self.message = message 47 def __str__(self): 48 return repr(self.message) 49 50 class MultiPartParser(object): 51 """ 52 A rfc2388 multipart/form-data parser. 53 54 parse() reads the input stream in chunk_size chunks and returns a 55 tuple of (POST MultiValueDict, FILES MultiValueDict). If 56 file_upload_dir is defined files will be streamed to temporary 57 files in the specified directory. 58 59 The FILES dictionary will have 'filename', 'content-type', 60 'content' and 'content-length' entries. For streamed files it will 61 also have 'tmpfilename' and 'tmpfile'. The 'content' entry will 62 only be read from disk when referenced for streamed files. 63 64 If the X-Progress-ID is sent (in one of many formats), then 65 object.file_progress will be given a dictionary of the progress. 66 """ 67 def __init__(self, headers, input, request, file_upload_dir=None, streaming_min_post_size=None, chunk_size=1024*64): 68 try: 69 content_length = int(headers['Content-Length']) 70 except: 71 raise MultiPartParserError('Invalid Content-Length: %s' % headers.get('Content-Length')) 72 73 content_type = headers.get('Content-Type') 74 75 if not content_type or not content_type.startswith('multipart/'): 76 raise MultiPartParserError('Invalid Content-Type: %s' % content_type) 77 78 ctype, opts = self.parse_header(content_type) 79 boundary = opts.get('boundary') 80 from cgi import valid_boundary 81 if not boundary or not valid_boundary(boundary): 82 raise MultiPartParserError('Invalid boundary in multipart form: %s' % boundary) 83 84 progress_id = request.META['UPLOAD_PROGRESS_ID'] 85 86 self._track_progress = file_upload_dir and progress_id # whether or not to track progress 87 self._boundary = '--' + boundary 88 self._input = input 89 self._size = content_length 90 self._received = 0 91 self._file_upload_dir = file_upload_dir 92 self._chunk_size = chunk_size 93 self._state = 'PREAMBLE' 94 self._partial = '' 95 self._post = MultiValueDict() 96 self._files = MultiValueDict() 97 self._request = request 98 99 if streaming_min_post_size is not None and content_length < streaming_min_post_size: 100 self._file_upload_dir = None # disable file streaming for small request 101 elif self._track_progress: 102 request.file_progress = {'state': 'starting'} 103 104 try: 105 # Use mx fast string search if available. 106 from mx.TextTools import FS 107 self._fs = FS(self._boundary) 108 except ImportError: 109 self._fs = None 110 111 def parse(self): 112 try: 113 self._parse() 114 finally: 115 if self._track_progress: 116 self._request.file_progress = {'state': 'done'} 117 return self._post, self._files 118 119 def _parse(self): 120 size = self._size 121 122 try: 123 while size > 0: 124 n = self._read(self._input, min(self._chunk_size, size)) 125 if not n: 126 break 127 size -= n 128 except: 129 # consume any remaining data so we dont generate a "Connection Reset" error 130 size = self._size - self._received 131 while size > 0: 132 data = self._input.read(min(self._chunk_size, size)) 133 size -= len(data) 134 raise 135 136 def _find_boundary(self, data, start, stop): 137 """ 138 Find the next boundary and return the end of current part 139 and start of next part. 140 """ 141 if self._fs: 142 boundary = self._fs.find(data, start, stop) 143 else: 144 boundary = data.find(self._boundary, start, stop) 145 if boundary >= 0: 146 end = boundary 147 next = boundary + len(self._boundary) 148 149 # backup over CRLF 150 if end > 0 and data[end-1] == '\n': end -= 1 151 if end > 0 and data[end-1] == '\r': end -= 1 152 # skip over --CRLF 153 if next < stop and data[next] == '-': next += 1 154 if next < stop and data[next] == '-': next += 1 155 if next < stop and data[next] == '\r': next += 1 156 if next < stop and data[next] == '\n': next += 1 157 158 return True, end, next 159 else: 160 return False, stop, stop 161 162 class TemporaryFile(object): 163 "A temporary file that tries to delete itself when garbage collected." 164 def __init__(self, dir): 165 import tempfile 166 (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir) 167 self.file = os.fdopen(fd, 'w+b') 168 self.name = name 169 170 def __getattr__(self, name): 171 a = getattr(self.__dict__['file'], name) 172 if type(a) != type(0): 173 setattr(self, name, a) 174 return a 175 176 def __del__(self): 177 try: 178 os.unlink(self.name) 179 except OSError: 180 pass 181 182 class LazyContent(dict): 183 """ 184 A lazy FILES dictionary entry that reads the contents from 185 tmpfile only when referenced. 186 """ 187 def __init__(self, data): 188 dict.__init__(self, data) 189 190 def __getitem__(self, key): 191 if key == 'content' and not self.has_key(key): 192 self['tmpfile'].seek(0) 193 self['content'] = self['tmpfile'].read() 194 return dict.__getitem__(self, key) 195 196 def _read(self, input, size): 197 data = input.read(size) 198 199 if not data: 200 return 0 201 202 read_size = len(data) 203 self._received += read_size 204 205 if self._partial: 206 data = self._partial + data 207 208 start = 0 209 stop = len(data) 210 211 while start < stop: 212 boundary, end, next = self._find_boundary(data, start, stop) 213 214 if not boundary and read_size: 215 # make sure we dont treat a partial boundary (and its separators) as data 216 stop -= len(self._boundary) + 16 217 end = next = stop 218 if end <= start: 219 break # need more data 220 221 if self._state == 'PREAMBLE': 222 # Preamble, just ignore it 223 self._state = 'HEADER' 224 225 elif self._state == 'HEADER': 226 # Beginning of header, look for end of header and parse it if found. 227 228 header_end = data.find('\r\n\r\n', start, stop) 229 if header_end == -1: 230 break # need more data 231 232 header = data[start:header_end] 233 234 self._fieldname = None 235 self._filename = None 236 self._content_type = None 237 238 for line in header.split('\r\n'): 239 ctype, opts = self.parse_header(line) 240 if ctype == 'content-disposition: form-data': 241 self._fieldname = opts.get('name') 242 self._filename = opts.get('filename') 243 elif ctype.startswith('content-type: '): 244 self._content_type = ctype[14:] 245 246 if self._filename is not None: 247 # cleanup filename from IE full paths: 248 self._filename = self._filename[self._filename.rfind("\\")+1:].strip() 249 250 if self._filename: # ignore files without filenames 251 if self._file_upload_dir: 252 try: 253 self._file = self.TemporaryFile(dir=self._file_upload_dir) 254 except OSError, IOError: 255 raise MultiPartParserError("Failed to create temporary file. Error was %s" % e) 256 else: 257 self._file = StringIO() 258 else: 259 self._file = None 260 self._filesize = 0 261 self._state = 'FILE' 262 else: 263 self._field = StringIO() 264 self._state = 'FIELD' 265 next = header_end + 4 266 267 elif self._state == 'FIELD': 268 # In a field, collect data until a boundary is found. 269 270 self._field.write(data[start:end]) 271 if boundary: 272 if self._fieldname: 273 self._post.appendlist(self._fieldname, self._field.getvalue()) 274 self._field.close() 275 self._state = 'HEADER' 276 277 elif self._state == 'FILE': 278 # In a file, collect data until a boundary is found. 279 280 if self._file: 281 try: 282 self._file.write(data[start:end]) 283 except IOError, e: 284 raise MultiPartParserError("Failed to write to temporary file.") 285 self._filesize += end-start 286 287 if self._track_progress: 288 self._request.file_progress = {'received': self._received, 289 'size': self._size, 290 'state': 'uploading'} 291 292 if boundary: 293 if self._file: 294 if self._file_upload_dir: 295 self._file.seek(0) 296 file = self.LazyContent({ 297 'filename': self._filename, 298 'content-type': self._content_type, 299 # 'content': is read on demand 300 'content-length': self._filesize, 301 'tmpfilename': self._file.name, 302 'tmpfile': self._file 303 }) 304 else: 305 file = { 306 'filename': self._filename, 307 'content-type': self._content_type, 308 'content': self._file.getvalue(), 309 'content-length': self._filesize 310 } 311 self._file.close() 312 313 self._files.appendlist(self._fieldname, file) 314 315 self._state = 'HEADER' 316 317 start = next 318 319 self._partial = data[start:] 320 321 return read_size 322 323 def parse_header(self, line): 324 from cgi import parse_header 325 return parse_header(line) -
django/http/__init__.py
3 3 from pprint import pformat 4 4 from urllib import urlencode, quote 5 5 from django.utils.datastructures import MultiValueDict 6 from django.http.multipartparser import MultiPartParser, MultiPartParserError 7 import re 6 8 9 upload_id_re = re.compile(r'^[a-fA-F0-9]{32}$') # file progress id Regular expression 10 7 11 RESERVED_CHARS="!*'();:@&=+$,/?%#[]" 8 12 9 13 try: … … 42 46 def is_secure(self): 43 47 return os.environ.get("HTTPS") == "on" 44 48 45 def parse_file_upload(header_dict, post_data): 46 "Returns a tuple of (POST MultiValueDict, FILES MultiValueDict)" 47 import email, email.Message 48 from cgi import parse_header 49 raw_message = '\r\n'.join(['%s:%s' % pair for pair in header_dict.items()]) 50 raw_message += '\r\n\r\n' + post_data 51 msg = email.message_from_string(raw_message) 52 POST = MultiValueDict() 53 FILES = MultiValueDict() 54 for submessage in msg.get_payload(): 55 if submessage and isinstance(submessage, email.Message.Message): 56 name_dict = parse_header(submessage['Content-Disposition'])[1] 57 # name_dict is something like {'name': 'file', 'filename': 'test.txt'} for file uploads 58 # or {'name': 'blah'} for POST fields 59 # We assume all uploaded files have a 'filename' set. 60 if 'filename' in name_dict: 61 assert type([]) != type(submessage.get_payload()), "Nested MIME messages are not supported" 62 if not name_dict['filename'].strip(): 63 continue 64 # IE submits the full path, so trim everything but the basename. 65 # (We can't use os.path.basename because it expects Linux paths.) 66 filename = name_dict['filename'][name_dict['filename'].rfind("\\")+1:] 67 FILES.appendlist(name_dict['name'], { 68 'filename': filename, 69 'content-type': 'Content-Type' in submessage and submessage['Content-Type'] or None, 70 'content': submessage.get_payload(), 71 }) 72 else: 73 POST.appendlist(name_dict['name'], submessage.get_payload()) 74 return POST, FILES 49 def _get_file_progress(self): 50 return {} 75 51 52 def _set_file_progress(self,value): 53 pass 54 55 def _del_file_progress(self): 56 pass 57 58 file_progress = property(_get_file_progress, 59 _set_file_progress, 60 _del_file_progress) 61 62 def _get_file_progress_from_args(self, headers, get, querystring): 63 """ 64 This parses the request for a file progress_id value. 65 Note that there are two distinct ways of getting the progress 66 ID -- header and GET. One is used primarily to attach via JavaScript 67 to the end of an HTML form action while the other is used for AJAX 68 communication. 69 70 All progress IDs must be valid 32-digit hexadecimal numbers. 71 """ 72 if 'X-Progress-ID' in headers: 73 progress_id = headers['X-Upload-ID'] 74 elif 'progress_id' in get: 75 progress_id = get['progress_id'] 76 else: 77 return None 78 79 if not self.upload_id_re.match(progress_id): 80 return None 81 82 return progress_id 83 84 def parse_file_upload(headers, input, request): 85 from django.conf import settings 86 87 # Only stream files to disk if FILE_STREAMING_DIR is set 88 file_upload_dir = settings.FILE_UPLOAD_DIR 89 streaming_min_post_size = settings.STREAMING_MIN_POST_SIZE 90 91 try: 92 parser = MultiPartParser(headers, input, request, file_upload_dir, streaming_min_post_size) 93 return parser.parse() 94 except MultiPartParserError, e: 95 return MultiValueDict({ '_file_upload_error': [e.message] }), {} 96 97 76 98 class QueryDict(MultiValueDict): 77 99 """A specialized MultiValueDict that takes a query string when initialized. 78 100 This is immutable unless you create a copy of it.""" -
django/oldforms/__init__.py
666 666 self.validator_list = [self.isNonEmptyFile] + validator_list 667 667 668 668 def isNonEmptyFile(self, field_data, all_data): 669 try:670 content = field_data['content']671 except TypeError:669 if field_data.has_key('_file_upload_error'): 670 raise validators.CriticalValidationError, field_data['_file_upload_error'] 671 if not field_data.has_key('filename'): 672 672 raise validators.CriticalValidationError, gettext("No file was submitted. Check the encoding type on the form.") 673 if not content:673 if not field_data['content-length']: 674 674 raise validators.CriticalValidationError, gettext("The submitted file is empty.") 675 675 676 676 def render(self, data): 677 677 return '<input type="file" id="%s" class="v%s" name="%s" />' % \ 678 678 (self.get_id(), self.__class__.__name__, self.field_name) 679 679 680 def prepare(self, new_data): 681 if new_data.has_key('_file_upload_error'): 682 # pretend we got something in the field to raise a validation error later 683 new_data[self.field_name] = { '_file_upload_error': new_data['_file_upload_error'] } 684 680 685 def html2python(data): 681 686 if data is None: 682 687 raise EmptyValue -
django/db/models/base.py
12 12 from django.dispatch import dispatcher 13 13 from django.utils.datastructures import SortedDict 14 14 from django.utils.functional import curry 15 from django.utils.file import file_move_safe 15 16 from django.conf import settings 16 17 from itertools import izip 17 18 import types … … 361 362 def _get_FIELD_size(self, field): 362 363 return os.path.getsize(self._get_FIELD_filename(field)) 363 364 364 def _save_FIELD_file(self, field, filename, raw_ contents, save=True):365 def _save_FIELD_file(self, field, filename, raw_field, save=True): 365 366 directory = field.get_directory_name() 366 367 try: # Create the date-based directory if it doesn't exist. 367 368 os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) 368 369 except OSError: # Directory probably already exists. 369 370 pass 371 372 if filename is None: 373 filename = raw_field['filename'] 374 370 375 filename = field.get_filename(filename) 371 376 372 377 # If the filename already exists, keep adding an underscore to the name of … … 383 388 setattr(self, field.attname, filename) 384 389 385 390 full_filename = self._get_FIELD_filename(field) 386 fp = open(full_filename, 'wb') 387 fp.write(raw_contents) 388 fp.close() 391 if raw_field.has_key('tmpfilename'): 392 raw_field['tmpfile'].close() 393 file_move_safe(raw_field['tmpfilename'], full_filename) 394 else: 395 from django.utils import file_locks 396 fp = open(full_filename, 'wb') 397 # exclusive lock 398 file_locks.lock(fp, file_locks.LOCK_EX) 399 fp.write(raw_field['content']) 400 fp.close() 389 401 390 402 # Save the width and/or height, if applicable. 391 403 if isinstance(field, ImageField) and (field.width_field or field.height_field): -
django/db/models/fields/__init__.py
701 701 setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self)) 702 702 setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self)) 703 703 setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self)) 704 setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save)) 704 setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save)) 705 setattr(cls, 'move_%s_file' % self.name, lambda instance, raw_field, save=True: instance._save_FIELD_file(self, None, raw_field, save)) 705 706 dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls) 706 707 707 708 def delete_file(self, instance): … … 724 725 if new_data.get(upload_field_name, False): 725 726 func = getattr(new_object, 'save_%s_file' % self.name) 726 727 if rel: 727 func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0] ["content"], save)728 func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0], save) 728 729 else: 729 func(new_data[upload_field_name]["filename"], new_data[upload_field_name] ["content"], save)730 func(new_data[upload_field_name]["filename"], new_data[upload_field_name], save) 730 731 731 732 def get_directory_name(self): 732 733 return os.path.normpath(datetime.datetime.now().strftime(self.upload_to)) -
django/conf/global_settings.py
242 242 # isExistingURL validator. 243 243 URL_VALIDATOR_USER_AGENT = "Django/0.96pre (http://www.djangoproject.com)" 244 244 245 # The directory to place streamed file uploads. The web server needs write 246 # permissions on this directory. 247 # If this is None, streaming uploads are disabled. 248 FILE_UPLOAD_DIR = None 249 250 # The minimum size of a POST before file uploads are streamed to disk. 251 # Any less than this number, and the file is uploaded to memory. 252 # Size is in bytes. 253 STREAMING_MIN_POST_SIZE = 512 * (2**10) 254 245 255 ############## 246 256 # MIDDLEWARE # 247 257 ############## -
django/core/handlers/wsgi.py
75 75 self.environ = environ 76 76 self.path = environ['PATH_INFO'] 77 77 self.META = environ 78 self.META['UPLOAD_PROGRESS_ID'] = self._get_file_progress_id() 78 79 self.method = environ['REQUEST_METHOD'].upper() 79 80 80 81 def __repr__(self): … … 111 112 if self.environ.get('CONTENT_TYPE', '').startswith('multipart'): 112 113 header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')]) 113 114 header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '') 114 self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data) 115 header_dict['Content-Length'] = self.environ.get('CONTENT_LENGTH', '') 116 header_dict['X-Progress-ID'] = self.environ.get('HTTP_X_PROGRESS_ID', '') 117 try: 118 self._post, self._files = http.parse_file_upload(header_dict, self.environ['wsgi.input'], self) 119 except: 120 self._post, self._files = {}, {} # make sure we dont read the input stream again 121 raise 122 self._raw_post_data = None # raw data is not available for streamed multipart messages 115 123 else: 116 124 self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict() 117 125 else: … … 167 175 buf.close() 168 176 return self._raw_post_data 169 177 178 def _get_file_progress_id(self): 179 """ 180 Returns the Progress ID of the request, 181 usually provided if there is a file upload 182 going on. 183 Returns ``None`` if no progress ID is specified. 184 """ 185 return self._get_file_progress_from_args(self.environ, 186 self.GET, 187 self.environ.get('QUERY_STRING', '')) 188 170 189 GET = property(_get_get, _set_get) 171 190 POST = property(_get_post, _set_post) 172 191 COOKIES = property(_get_cookies, _set_cookies) -
django/core/handlers/modpython.py
47 47 def _load_post_and_files(self): 48 48 "Populates self._post and self._files" 49 49 if 'content-type' in self._req.headers_in and self._req.headers_in['content-type'].startswith('multipart'): 50 self._post, self._files = http.parse_file_upload(self._req.headers_in, self.raw_post_data) 50 self._raw_post_data = None # raw data is not available for streamed multipart messages 51 try: 52 self._post, self._files = http.parse_file_upload(self._req.headers_in, self._req, self) 53 except: 54 self._post, self._files = {}, {} # make sure we dont read the input stream again 55 raise 51 56 else: 52 57 self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict() 53 58 … … 92 97 'AUTH_TYPE': self._req.ap_auth_type, 93 98 'CONTENT_LENGTH': self._req.clength, # This may be wrong 94 99 'CONTENT_TYPE': self._req.content_type, # This may be wrong 95 'GATEWAY_INTERFACE': 'CGI/1.1', 96 'PATH_INFO': self._req.path_info, 97 'PATH_TRANSLATED': None, # Not supported 98 'QUERY_STRING': self._req.args, 99 'REMOTE_ADDR': self._req.connection.remote_ip, 100 'REMOTE_HOST': None, # DNS lookups not supported 101 'REMOTE_IDENT': self._req.connection.remote_logname, 102 'REMOTE_USER': self._req.user, 103 'REQUEST_METHOD': self._req.method, 104 'SCRIPT_NAME': None, # Not supported 105 'SERVER_NAME': self._req.server.server_hostname, 106 'SERVER_PORT': self._req.server.port, 107 'SERVER_PROTOCOL': self._req.protocol, 108 'SERVER_SOFTWARE': 'mod_python' 100 'GATEWAY_INTERFACE': 'CGI/1.1', 101 'PATH_INFO': self._req.path_info, 102 'PATH_TRANSLATED': None, # Not supported 103 'QUERY_STRING': self._req.args, 104 'REMOTE_ADDR': self._req.connection.remote_ip, 105 'REMOTE_HOST': None, # DNS lookups not supported 106 'REMOTE_IDENT': self._req.connection.remote_logname, 107 'REMOTE_USER': self._req.user, 108 'REQUEST_METHOD': self._req.method, 109 'SCRIPT_NAME': None, # Not supported 110 'SERVER_NAME': self._req.server.server_hostname, 111 'SERVER_PORT': self._req.server.port, 112 'SERVER_PROTOCOL': self._req.protocol, 113 'UPLOAD_PROGRESS_ID': self._get_file_progress_id(), 114 'SERVER_SOFTWARE': 'mod_python' 109 115 } 110 116 for key, value in self._req.headers_in.items(): 111 117 key = 'HTTP_' + key.upper().replace('-', '_') … … 122 128 def _get_method(self): 123 129 return self.META['REQUEST_METHOD'].upper() 124 130 131 def _get_file_progress_id(self): 132 """ 133 Returns the Progress ID of the request, 134 usually provided if there is a file upload 135 going on. 136 Returns ``None`` if no progress ID is specified. 137 """ 138 return self._get_file_progress_from_args(self._req.headers_in, 139 self.GET, 140 self._req.args) 141 125 142 GET = property(_get_get, _set_get) 126 143 POST = property(_get_post, _set_post) 127 144 COOKIES = property(_get_cookies, _set_cookies) -
django/utils/file_locks.py
1 """ 2 Locking portability by Jonathan Feignberg <jdf@pobox.com> in python cookbook 3 4 Example Usage:: 5 6 from django.utils import file_locks 7 8 f = open('./file', 'wb') 9 10 file_locks.lock(f, file_locks.LOCK_EX) 11 f.write('Django') 12 f.close() 13 """ 14 15 16 import os 17 18 __all__ = ['LOCK_EX','LOCK_SH','LOCK_NB','lock','unlock'] 19 20 if os.name == 'nt': 21 import win32con 22 import win32file 23 import pywintypes 24 LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK 25 LOCK_SH = 0 26 LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY 27 __overlapped = pywintypes.OVERLAPPED() 28 elif os.name == 'posix': 29 import fcntl 30 LOCK_EX = fcntl.LOCK_EX 31 LOCK_SH = fcntl.LOCK_SH 32 LOCK_NB = fcntl.LOCK_NB 33 else: 34 raise RuntimeError("Locking only defined for nt and posix platforms") 35 36 if os.name == 'nt': 37 def lock(file, flags): 38 hfile = win32file._get_osfhandle(file.fileno()) 39 win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped) 40 41 def unlock(file): 42 hfile = win32file._get_osfhandle(file.fileno()) 43 win32file.UnlockFileEx(hfile, 0, -0x10000, __overlapped) 44 45 elif os.name =='posix': 46 def lock(file, flags): 47 fcntl.flock(file.fileno(), flags) 48 49 def unlock(file): 50 fcntl.flock(file.fileno(), fcntl.LOCK_UN) -
django/utils/file.py
1 import os 2 3 __all__ = ['file_move_safe'] 4 5 try: 6 import shutil 7 file_move = shutil.move 8 except ImportError: 9 file_move = os.rename 10 11 def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_overwrite=False): 12 """ 13 Moves a file from one location to another in the safest way possible. 14 15 First, it tries using shutils.move, which is OS-dependent but doesn't 16 break with change of filesystems. Then it tries os.rename, which will 17 break if it encounters a change in filesystems. Lastly, it streams 18 it manually from one file to another in python. 19 20 Without ``allow_overwrite``, if the destination file exists, the 21 file will raise an IOError. 22 """ 23 24 from django.utils import file_locks 25 26 if old_file_name == new_file_name: 27 # No file moving takes place. 28 return 29 30 if not allow_overwrite and os.path.exists(new_file_name): 31 raise IOError, "Django does not allow overwriting files." 32 33 try: 34 file_move(old_file_name, new_file_name) 35 return 36 except OSError: # moving to another filesystem 37 pass 38 39 new_file = open(new_file_name, 'wb') 40 # exclusive lock 41 file_locks.lock(new_file, file_locks.LOCK_EX) 42 old_file = open(old_file_name, 'rb') 43 current_chunk = None 44 45 while current_chunk != '': 46 current_chunk = old_file.read(chunk_size) 47 new_file.write(current_chunk) 48 49 new_file.close() 50 old_file.close() 51 52 os.remove(old_file_name) 53 -
tests/modeltests/test_client/views.py
46 46 47 47 return HttpResponse(t.render(c)) 48 48 49 def post_file_view(request): 50 "A view that expects a multipart post and returns a file in the context" 51 t = Template('File {{ file.filename }} received', name='POST Template') 52 c = Context({'file': request.FILES['file_file']}) 53 return HttpResponse(t.render(c)) 54 49 55 def redirect_view(request): 50 56 "A view that redirects all requests to the GET view" 51 57 return HttpResponseRedirect('/test_client/get_view/') -
tests/modeltests/test_client/models.py
3 3 4 4 The test client is a class that can act like a simple 5 5 browser for testing purposes. 6 6 7 7 It allows the user to compose GET and POST requests, and 8 8 obtain the response that the server gave to those requests. 9 9 The server Response objects are annotated with the details … … 76 76 self.assertEqual(response.template.name, "Book template") 77 77 self.assertEqual(response.content, "Blink - Malcolm Gladwell") 78 78 79 def test_post_file_view(self): 80 "POST this python file to a view" 81 import os, tempfile 82 from django.conf import settings 83 file = __file__.replace('.pyc', '.py') 84 for upload_dir in [None, tempfile.gettempdir()]: 85 settings.FILE_UPLOAD_DIR = upload_dir 86 post_data = { 'name': file, 'file': open(file) } 87 response = self.client.post('/test_client/post_file_view/', post_data) 88 self.failUnless('models.py' in response.context['file']['filename']) 89 self.failUnless(len(response.context['file']['content']) == os.path.getsize(file)) 90 if upload_dir: 91 self.failUnless(response.context['file']['tmpfilename']) 92 79 93 def test_redirect(self): 80 94 "GET a URL that redirects elsewhere" 81 95 response = self.client.get('/test_client/redirect_view/') -
tests/modeltests/test_client/urls.py
5 5 urlpatterns = patterns('', 6 6 (r'^get_view/$', views.get_view), 7 7 (r'^post_view/$', views.post_view), 8 (r'^post_file_view/$', views.post_file_view), 8 9 (r'^raw_post_view/$', views.raw_post_view), 9 10 (r'^redirect_view/$', views.redirect_view), 10 11 (r'^permanent_redirect_view/$', redirect_to, { 'url': '/test_client/get_view/' }), -
docs/request_response.txt
72 72 ``FILES`` 73 73 A dictionary-like object containing all uploaded files. Each key in 74 74 ``FILES`` is the ``name`` from the ``<input type="file" name="" />``. Each 75 value in ``FILES`` is a standard Python dictionary with the following three75 value in ``FILES`` is a standard Python dictionary with the following four 76 76 keys: 77 77 78 78 * ``filename`` -- The name of the uploaded file, as a Python string. 79 79 * ``content-type`` -- The content type of the uploaded file. 80 80 * ``content`` -- The raw content of the uploaded file. 81 * ``content-length`` -- The length of the content in bytes. 81 82 83 If streaming file uploads are enabled two additional keys 84 describing the uploaded file will be present: 85 86 * ``tmpfilename`` -- The filename for the temporary file. 87 * ``tmpfile`` -- An open file object for the temporary file. 88 89 The temporary file will be removed when the request finishes. 90 91 Note that accessing ``content`` when streaming uploads are enabled 92 will read the whole file into memory which may not be what you want. 93 82 94 Note that ``FILES`` will only contain data if the request method was POST 83 95 and the ``<form>`` that posted to the request had 84 96 ``enctype="multipart/form-data"``. Otherwise, ``FILES`` will be a blank -
docs/settings.txt
448 448 449 449 .. _Testing Django Applications: ../testing/ 450 450 451 FILE_UPLOAD_DIR 452 --------------- 453 454 Default: ``None`` 455 456 Path to a directory where temporary files should be written during 457 file uploads. Leaving this as ``None`` will disable streaming file uploads, 458 and cause all uploaded files to be stored (temporarily) in memory. 459 451 460 IGNORABLE_404_ENDS 452 461 ------------------ 453 462 … … 764 773 765 774 .. _site framework docs: ../sites/ 766 775 776 STREAMING_MIN_POST_SIZE 777 ----------------------- 778 779 Default: 524288 (``512*1024``) 780 781 An integer specifying the minimum number of bytes that has to be 782 received (in a POST) for file upload streaming to take place. Any 783 request smaller than this will be handled in memory. 784 Note: ``FILE_UPLOAD_DIR`` has to be defined to enable streaming. 785 767 786 TEMPLATE_CONTEXT_PROCESSORS 768 787 --------------------------- 769 788 -
docs/forms.txt
475 475 new_data = request.POST.copy() 476 476 new_data.update(request.FILES) 477 477 478 Streaming file uploads. 479 ----------------------- 480 481 File uploads will be read into memory by default. This works fine for 482 small to medium sized uploads (from 1MB to 100MB depending on your 483 setup and usage). If you want to support larger uploads you can enable 484 upload streaming where only a small part of the file will be in memory 485 at any time. To do this you need to specify the ``FILE_UPLOAD_DIR`` 486 setting (see the settings_ document for more details). 487 488 See `request object`_ for more details about ``request.FILES`` objects 489 with streaming file uploads enabled. 490 478 491 Validators 479 492 ========== 480 493 … … 698 711 .. _`generic views`: ../generic_views/ 699 712 .. _`models API`: ../model-api/ 700 713 .. _settings: ../settings/ 714 .. _request object: ../request_response/#httprequest-objects