Ticket #4165: 5099-streaming_file_upload_with_safe_file_move_progress.diff
File 5099-streaming_file_upload_with_safe_file_move_progress.diff, 15.6 KB (added by , 18 years ago) |
---|
-
django/http/__init__.py
1 import os 1 import os, pickle 2 2 from Cookie import SimpleCookie 3 3 from pprint import pformat 4 4 from urllib import urlencode, quote 5 5 from django.utils.datastructures import MultiValueDict 6 6 7 try: 8 from cStringIO import StringIO 9 except ImportError: 10 from StringIO import StringIO 11 7 12 RESERVED_CHARS="!*'();:@&=+$,/?%#[]" 8 13 9 14 try: … … 306 311 if not host: 307 312 host = request.META.get('HTTP_HOST', '') 308 313 return host 314 -
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 18 19 import sys 19 20 import os 20 21 22 21 23 class ModelBase(type): 22 24 "Metaclass for all models" 23 25 def __new__(cls, name, bases, attrs): … … 361 363 def _get_FIELD_size(self, field): 362 364 return os.path.getsize(self._get_FIELD_filename(field)) 363 365 364 def _save_FIELD_file(self, field, filename, raw_ contents, save=True):366 def _save_FIELD_file(self, field, filename, raw_field, save=True): 365 367 directory = field.get_directory_name() 366 368 try: # Create the date-based directory if it doesn't exist. 367 369 os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) … … 383 385 setattr(self, field.attname, filename) 384 386 385 387 full_filename = self._get_FIELD_filename(field) 386 fp = open(full_filename, 'wb') 387 fp.write(raw_contents) 388 fp.close() 388 if raw_field.has_key('tmpfilename'): 389 raw_field['tmpfile'].close() 390 file_move_safe(raw_field['tmpfilename'], full_filename) 391 else: 392 fp = open(full_filename, 'wb') 393 fp.write(raw_field['content']) 394 fp.close() 389 395 390 396 # Save the width and/or height, if applicable. 391 397 if isinstance(field, ImageField) and (field.width_field or field.height_field): -
django/db/models/fields/__init__.py
636 636 setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self)) 637 637 setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self)) 638 638 setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self)) 639 setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_ contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save))639 setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save)) 640 640 dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls) 641 641 642 642 def delete_file(self, instance): … … 659 659 if new_data.get(upload_field_name, False): 660 660 func = getattr(new_object, 'save_%s_file' % self.name) 661 661 if rel: 662 func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0] ["content"], save)662 func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0], save) 663 663 else: 664 func(new_data[upload_field_name]["filename"], new_data[upload_field_name] ["content"], save)664 func(new_data[upload_field_name]["filename"], new_data[upload_field_name], save) 665 665 666 666 def get_directory_name(self): 667 667 return os.path.normpath(datetime.datetime.now().strftime(self.upload_to)) -
django/conf/global_settings.py
240 240 # isExistingURL validator. 241 241 URL_VALIDATOR_USER_AGENT = "Django/0.96pre (http://www.djangoproject.com)" 242 242 243 # The directory to place streamed file uploads. The web server needs write 244 # permissions on this directory. 245 # If this is None, streaming uploads are disabled. 246 FILE_UPLOAD_DIR = None 247 248 249 # The minimum size of a POST before file uploads are streamed to disk. 250 # Any less than this number, and the file is uploaded to memory. 251 # Size is in bytes. 252 STREAMING_MIN_POST_SIZE = 512 * (2**10) 253 254 255 256 243 257 ############## 244 258 # MIDDLEWARE # 245 259 ############## … … 335 349 336 350 # The list of directories to search for fixtures 337 351 FIXTURE_DIRS = () 352 353 -
django/core/handlers/wsgi.py
111 111 if self.environ.get('CONTENT_TYPE', '').startswith('multipart'): 112 112 header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')]) 113 113 header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '') 114 self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data) 114 header_dict['Content-Length'] = self.environ.get('CONTENT_LENGTH', '') 115 try: 116 self._post, self._files = http.parse_file_upload(header_dict, self.environ['wsgi.input'], self.META.get('QUERY_STRING')) 117 except: 118 self._post, self._files = {}, {} # make sure we dont read the input stream again 119 raise 120 self._raw_post_data = None # raw data is not available for streamed multipart messages 115 121 else: 116 122 self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict() 117 123 else: -
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.META.get('QUERY_STRING')) 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 -
django/utils/file.py
1 import os 2 3 try: 4 import shutils 5 file_move = shutils.move 6 except: 7 file_move = os.rename 8 9 def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64): 10 """ 11 Moves a file from one location to another in the safest way possible. 12 13 First, it tries using shutils.move, which is OS-dependent but doesn't 14 break with change of filesystems. Then it tries os.rename, which will 15 break if it encounters a change in filesystems. Lastly, it streams 16 it manually from one file to another in python. 17 """ 18 19 try: 20 file_move(old_file_name, new_file_name) 21 return 22 except: 23 pass 24 25 new_file = open(new_file_name, 'wb') 26 old_file = open(old_file_name, 'rb') 27 current_chunk = None 28 29 while current_chunk != '': 30 current_chunk = old_file.read(chunk_size) 31 new_file.write(current_chunk) 32 33 new_file.close() 34 old_file.close() 35 36 os.remove(old_file_name) -
tests/modeltests/test_client/views.py
44 44 45 45 return HttpResponse(t.render(c)) 46 46 47 def post_file_view(request): 48 "A view that expects a multipart post and returns a file in the context" 49 t = Template('File {{ file.filename }} received', name='POST Template') 50 c = Context({'file': request.FILES['file_file']}) 51 return HttpResponse(t.render(c)) 52 47 53 def redirect_view(request): 48 54 "A view that redirects all requests to the GET view" 49 55 return HttpResponseRedirect('/test_client/get_view/') -
tests/modeltests/test_client/models.py
75 75 self.assertEqual(response.template.name, "Book template") 76 76 self.assertEqual(response.content, "Blink - Malcolm Gladwell") 77 77 78 def test_post_file_view(self): 79 "POST this python file to a view" 80 import os, tempfile 81 from django.conf import settings 82 file = __file__.replace('.pyc', '.py') 83 for upload_dir in [None, tempfile.gettempdir()]: 84 settings.FILE_UPLOAD_DIR = upload_dir 85 post_data = { 'name': file, 'file': open(file) } 86 response = self.client.post('/test_client/post_file_view/', post_data) 87 self.failUnless('models.py' in response.context['file']['filename']) 88 self.failUnless(len(response.context['file']['content']) == os.path.getsize(file)) 89 if upload_dir: 90 self.failUnless(response.context['file']['tmpfilename']) 91 92 78 93 def test_redirect(self): 79 94 "GET a URL that redirects elsewhere" 80 95 response = self.client.get('/test_client/redirect_view/') -
tests/modeltests/test_client/urls.py
4 4 urlpatterns = patterns('', 5 5 (r'^get_view/$', views.get_view), 6 6 (r'^post_view/$', views.post_view), 7 (r'^post_file_view/$', views.post_file_view), 7 8 (r'^raw_post_view/$', views.raw_post_view), 8 9 (r'^redirect_view/$', views.redirect_view), 9 10 (r'^form_view/$', views.form_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
437 437 438 438 .. _Testing Django Applications: ../testing/ 439 439 440 FILE_UPLOAD_DIR 441 --------------- 442 443 Default: ``None`` 444 445 Path to a directory where temporary files should be written during 446 file uploads. Leaving this as ``None`` will disable streaming file uploads, 447 and cause all uploaded files to be stored (temporarily) in memory. 448 440 449 IGNORABLE_404_ENDS 441 450 ------------------ 442 451 … … 780 789 781 790 .. _site framework docs: ../sites/ 782 791 792 STREAMING_MIN_POST_SIZE 793 ----------------------- 794 795 Default: 524288 (``512*1024``) 796 797 An integer specifying the minimum number of bytes that has to be 798 received (in a POST) for file upload streaming to take place. Any 799 request smaller than this will be handled in memory. 800 Note: ``FILE_UPLOAD_DIR`` has to be defined to enable streaming. 801 783 802 TEMPLATE_CONTEXT_PROCESSORS 784 803 --------------------------- 785 804 -
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 … … 693 706 .. _`generic views`: ../generic_views/ 694 707 .. _`models API`: ../model-api/ 695 708 .. _settings: ../settings/ 709 .. _request object: ../request_response/#httprequest-objects