Ticket #2070: 5099_patch_for_streaming_uploads.diff

File 5099_patch_for_streaming_uploads.diff, 30.8 KB (added by Michael Axiak <axiak@…>, 17 years ago)

Uses multiple mechanisms for determining the progress id.

  • django/http/__init__.py

     
    1 import os
     1import os, pickle
    22from Cookie import SimpleCookie
    33from pprint import pformat
    44from urllib import urlencode, quote
    55from django.utils.datastructures import MultiValueDict
     6import re
    67
     8try:
     9    from cStringIO import StringIO
     10except ImportError:
     11    from StringIO import StringIO
     12
    713RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
    814
     15
    916try:
    1017    # The mod_python version is more efficient, so try importing it first.
    1118    from mod_python.util import parse_qsl
     
    1724
    1825class HttpRequest(object):
    1926    "A basic HTTP request"
     27
     28    upload_id_re = re.compile(r'[a-fA-F0-9]{32}')
     29
     30
    2031    def __init__(self):
    2132        self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {}
    2233        self.path = ''
     
    4253    def is_secure(self):
    4354        return os.environ.get("HTTPS") == "on"
    4455
    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
     56def parse_file_upload(headers, input, progress_id = None):
     57    from django.conf import settings
    7558
     59    # Only stream files to disk if FILE_STREAMING_DIR is set
     60    file_upload_dir = settings.FILE_UPLOAD_DIR
     61    streaming_min_post_size = settings.STREAMING_MIN_POST_SIZE
     62
     63    try:
     64        parser = MultiPartParser(headers, input, progress_id, file_upload_dir, streaming_min_post_size)
     65        return parser.parse()
     66    except MultiPartParserError, e:
     67        return MultiValueDict({ '_file_upload_error': [e.message] }), {}
     68
     69class MultiPartParserError(Exception):
     70    def __init__(self, message):
     71        self.message = message
     72    def __str__(self):
     73        return repr(self.message)
     74       
     75class MultiPartParser(object):
     76    """
     77    A rfc2388 multipart/form-data parser.
     78   
     79    parse() reads the input stream in chunk_size chunks and returns a
     80    tuple of (POST MultiValueDict, FILES MultiValueDict). If
     81    file_upload_dir is defined files will be streamed to temporary
     82    files in the specified directory.
     83
     84    The FILES dictionary will have 'filename', 'content-type',
     85    'content' and 'content-length' entries. For streamed files it will
     86    also have 'tmpfilename' and 'tmpfile'. The 'content' entry will
     87    only be read from disk when referenced for streamed files.
     88
     89    If the header X-Progress-ID is sent with a 32 character hex string
     90    a temporary file with the same name will be created in
     91    `file_upload_dir`` with a pickled { 'received', 'size' }
     92    dictionary with the number of bytes received and the size expected
     93    respectively. The file will be unlinked when the parser finishes.
     94
     95    """
     96
     97    def __init__(self, headers, input, progress_id=None, file_upload_dir=None, streaming_min_post_size=None, chunk_size=1024*64):
     98        try:
     99            content_length = int(headers['Content-Length'])
     100        except:
     101            raise MultiPartParserError('Invalid Content-Length: %s' % headers.get('Content-Length'))
     102
     103        content_type = headers.get('Content-Type')
     104
     105        if not content_type or not content_type.startswith('multipart/'):
     106            raise MultiPartParserError('Invalid Content-Type: %s' % content_type)
     107           
     108        ctype, opts = self.parse_header(content_type)
     109        boundary = opts.get('boundary')
     110        from cgi import valid_boundary
     111        if not boundary or not valid_boundary(boundary):
     112            raise MultiPartParserError('Invalid boundary in multipart form: %s' % boundary)
     113
     114        # check if we got a valid X-Progress-ID id
     115        if file_upload_dir and progress_id:
     116            import re
     117            if re.match(r'^[0-9a-zA-Z]{32}$', progress_id):
     118                self._progress_filename = os.path.join(file_upload_dir, progress_id)
     119                raise MultiPartParserError('Invalid X-Progress-ID: %s' % progress_id)
     120        else:
     121            self._progress_filename = None
     122        self._boundary = '--' + boundary
     123        self._input = input
     124        self._size = content_length
     125        self._received = 0
     126        self._file_upload_dir = file_upload_dir
     127        self._chunk_size = chunk_size
     128        self._state = 'PREAMBLE'
     129        self._partial = ''
     130        self._post = MultiValueDict()
     131        self._files = MultiValueDict()
     132        if streaming_min_post_size is not None and content_length < streaming_min_post_size:
     133            self._file_upload_dir = None # disable file streaming for small request
     134
     135        try:
     136            # use mx fast string search if available
     137            from mx.TextTools import FS
     138            self._fs = FS(self._boundary)
     139        except ImportError:
     140            self._fs = None
     141
     142    def parse(self):
     143        try:
     144            self._parse()
     145        finally:
     146            if self._progress_filename:
     147                try:
     148                    os.unlink(self._progress_filename)
     149                except OSError:
     150                    pass
     151       
     152        return self._post, self._files
     153
     154    def _parse(self):
     155        size = self._size
     156
     157        try:
     158            while size > 0:
     159                n = self._read(self._input, min(self._chunk_size, size))
     160                if not n:
     161                    break
     162                size -= n
     163        except:
     164            # consume any remaining data so we dont generate a "Connection Reset" error
     165            size = self._size - self._received
     166            while size > 0:
     167                data = self._input.read(min(self._chunk_size, size))
     168                size -= len(data)
     169            raise
     170
     171    def _find_boundary(self, data, start, stop):
     172        """
     173        Find the next boundary and return the end of current part
     174        and start of next part.
     175        """
     176        if self._fs:
     177            boundary = self._fs.find(data, start, stop)
     178        else:
     179            boundary = data.find(self._boundary, start, stop)
     180        if boundary >= 0:
     181            end = boundary
     182            next = boundary + len(self._boundary)
     183
     184            # backup over CRLF
     185            if end > 0 and data[end-1] == '\n': end -= 1
     186            if end > 0 and data[end-1] == '\r': end -= 1
     187            # skip over --CRLF
     188            if next < stop and data[next] == '-': next += 1
     189            if next < stop and data[next] == '-': next += 1
     190            if next < stop and data[next] == '\r': next += 1
     191            if next < stop and data[next] == '\n': next += 1
     192
     193            return True, end, next
     194        else:
     195            return False, stop, stop
     196
     197    class TemporaryFile(object):
     198        "A temporary file that tries to delete itself when garbage collected."
     199        def __init__(self, dir):
     200            import tempfile
     201            (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir)
     202            self.file = os.fdopen(fd, 'w+b')
     203            self.name = name
     204
     205        def __getattr__(self, name):
     206            a = getattr(self.__dict__['file'], name)
     207            if type(a) != type(0):
     208                setattr(self, name, a)
     209            return a
     210
     211        def __del__(self):
     212            try:
     213                os.unlink(self.name)
     214            except OSError:
     215                pass
     216           
     217    class LazyContent(dict):
     218        """
     219        A lazy FILES dictionary entry that reads the contents from
     220        tmpfile only when referenced.
     221        """
     222        def __init__(self, data):
     223            dict.__init__(self, data)
     224       
     225        def __getitem__(self, key):
     226            if key == 'content' and not self.has_key(key):
     227                self['tmpfile'].seek(0)
     228                self['content'] = self['tmpfile'].read()
     229            return dict.__getitem__(self, key)
     230
     231    def _read(self, input, size):
     232        data = input.read(size)
     233
     234        if not data:
     235            return 0
     236
     237        read_size = len(data)
     238        self._received += read_size
     239
     240        if self._partial:
     241            data = self._partial + data
     242
     243        start = 0
     244        stop = len(data)
     245       
     246        while start < stop:
     247            boundary, end, next = self._find_boundary(data, start, stop)
     248
     249            if not boundary and read_size:
     250                # make sure we dont treat a partial boundary (and its separators) as data
     251                stop -= len(self._boundary) + 16
     252                end = next = stop
     253                if end <= start:
     254                    break # need more data
     255
     256            if self._state == 'PREAMBLE':
     257                # Preamble, just ignore it
     258                self._state = 'HEADER'
     259
     260            elif self._state == 'HEADER':
     261                # Beginning of header, look for end of header and parse it if found.
     262
     263                header_end = data.find('\r\n\r\n', start, stop)
     264                if header_end == -1:
     265                    break # need more data
     266
     267                header = data[start:header_end]
     268
     269                self._fieldname = None
     270                self._filename = None
     271                self._content_type = None
     272
     273                for line in header.split('\r\n'):
     274                    ctype, opts = self.parse_header(line)
     275                    if ctype == 'content-disposition: form-data':
     276                        self._fieldname = opts.get('name')
     277                        self._filename = opts.get('filename')
     278                    elif ctype.startswith('content-type: '):
     279                        self._content_type = ctype[14:]
     280
     281                if self._filename is not None:
     282                    # cleanup filename from IE full paths:
     283                    self._filename = self._filename[self._filename.rfind("\\")+1:].strip()
     284
     285                    if self._filename: # ignore files without filenames
     286                        if self._file_upload_dir:
     287                            try:
     288                                self._file = self.TemporaryFile(dir=self._file_upload_dir)
     289                            except:
     290                                raise MultiPartParserError("Failed to create temporary file.")
     291                        else:
     292                            self._file = StringIO()
     293                    else:
     294                        self._file = None
     295                    self._filesize = 0
     296                    self._state = 'FILE'
     297                else:
     298                    self._field = StringIO()
     299                    self._state = 'FIELD'
     300                next = header_end + 4
     301
     302            elif self._state == 'FIELD':
     303                # In a field, collect data until a boundary is found.
     304
     305                self._field.write(data[start:end])
     306                if boundary:
     307                    if self._fieldname:
     308                        self._post.appendlist(self._fieldname, self._field.getvalue())
     309                    self._field.close()
     310                    self._state = 'HEADER'
     311
     312            elif self._state == 'FILE':
     313                # In a file, collect data until a boundary is found.
     314
     315                if self._file:
     316                    try:
     317                        self._file.write(data[start:end])
     318                    except IOError, e:
     319                        raise MultiPartParserError("Failed to write to temporary file.")
     320                    self._filesize += end-start
     321
     322                    if self._progress_filename:
     323                        f = open(os.path.join(self._file_upload_dir, self._progress_filename), 'w')
     324                        pickle.dump({ 'received': self._received, 'size': self._size }, f)
     325                        f.close()
     326
     327                if boundary:
     328                    if self._file:
     329                        if self._file_upload_dir:
     330                            self._file.seek(0)
     331                            file = self.LazyContent({
     332                                'filename': self._filename,
     333                                'content-type':  self._content_type,
     334                                # 'content': is read on demand
     335                                'content-length': self._filesize,
     336                                'tmpfilename': self._file.name,
     337                                'tmpfile': self._file
     338                            })
     339                        else:
     340                            file = {
     341                                'filename': self._filename,
     342                                'content-type':  self._content_type,
     343                                'content': self._file.getvalue(),
     344                                'content-length': self._filesize
     345                            }
     346                            self._file.close()
     347
     348                        self._files.appendlist(self._fieldname, file)
     349
     350                    self._state = 'HEADER'
     351
     352            start = next
     353               
     354        self._partial = data[start:]
     355
     356        return read_size
     357
     358    def parse_header(self, line):
     359        from cgi import parse_header
     360        return parse_header(line)
     361
    76362class QueryDict(MultiValueDict):
    77363    """A specialized MultiValueDict that takes a query string when initialized.
    78364    This is immutable unless you create a copy of it."""
     
    306592    if not host:
    307593        host = request.META.get('HTTP_HOST', '')
    308594    return host
     595
  • django/conf/global_settings.py

     
    240240# isExistingURL validator.
    241241URL_VALIDATOR_USER_AGENT = "Django/0.96pre (http://www.djangoproject.com)"
    242242
     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.
     246FILE_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.
     252STREAMING_MIN_POST_SIZE = 512 * (2**10)
     253
     254
     255
     256
    243257##############
    244258# MIDDLEWARE #
    245259##############
     
    335349
    336350# The list of directories to search for fixtures
    337351FIXTURE_DIRS = ()
     352
     353
  • django/db/models/base.py

     
    1212from django.dispatch import dispatcher
    1313from django.utils.datastructures import SortedDict
    1414from django.utils.functional import curry
     15from django.utils.file import file_move_safe
    1516from django.conf import settings
    1617from itertools import izip
    1718import types
    1819import sys
    1920import os
    2021
     22               
    2123class ModelBase(type):
    2224    "Metaclass for all models"
    2325    def __new__(cls, name, bases, attrs):
     
    361363    def _get_FIELD_size(self, field):
    362364        return os.path.getsize(self._get_FIELD_filename(field))
    363365
    364     def _save_FIELD_file(self, field, filename, raw_contents, save=True):
     366    def _save_FIELD_file(self, field, filename, raw_field, save=True):
    365367        directory = field.get_directory_name()
    366368        try: # Create the date-based directory if it doesn't exist.
    367369            os.makedirs(os.path.join(settings.MEDIA_ROOT, directory))
     
    383385        setattr(self, field.attname, filename)
    384386
    385387        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()
    389395
    390396        # Save the width and/or height, if applicable.
    391397        if isinstance(field, ImageField) and (field.width_field or field.height_field):
  • django/db/models/fields/__init__.py

     
    636636        setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
    637637        setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
    638638        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))
    640640        dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
    641641
    642642    def delete_file(self, instance):
     
    659659        if new_data.get(upload_field_name, False):
    660660            func = getattr(new_object, 'save_%s_file' % self.name)
    661661            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)
    663663            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)
    665665
    666666    def get_directory_name(self):
    667667        return os.path.normpath(datetime.datetime.now().strftime(self.upload_to))
  • django/oldforms/__init__.py

     
    666666        self.validator_list = [self.isNonEmptyFile] + validator_list
    667667
    668668    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'):
    672672            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']:
    674674            raise validators.CriticalValidationError, gettext("The submitted file is empty.")
    675675
    676676    def render(self, data):
    677677        return '<input type="file" id="%s" class="v%s" name="%s" />' % \
    678678            (self.get_id(), self.__class__.__name__, self.field_name)
    679679
     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
    680685    def html2python(data):
    681686        if data is None:
    682687            raise EmptyValue
  • django/core/handlers/wsgi.py

     
    111111            if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
    112112                header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')])
    113113                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                header_dict['X-Progress-ID'] = self.environ.get('HTTP_X_PROGRESS_ID', '')
     116                try:
     117                    self._post, self._files = http.parse_file_upload(header_dict, self.environ['wsgi.input'], self.progress_id)
     118                except:
     119                    self._post, self._files = {}, {} # make sure we dont read the input stream again
     120                    raise
     121                self._raw_post_data = None # raw data is not available for streamed multipart messages
    115122            else:
    116123                self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict()
    117124        else:
     
    167174            buf.close()
    168175            return self._raw_post_data
    169176
     177    def _get_file_progress_id(self):
     178        """
     179        Returns the Progress ID of the request,
     180        usually provided if there is a file upload
     181        going on.
     182        Returns ``None`` if no progress ID is specified.
     183        """
     184        _get = self.GET
     185        _querystring = self.environ.get('QUERY_STRING', '')
     186
     187        if 'HTTP_X_UPLOAD_ID' in self.environ:
     188            progress_id = self.environ['HTTP_X_UPLOAD_ID']
     189        elif 'HTTP_X_PROGRESS_ID' in self.environ:
     190            progress_id = self.environ['HTTP_X_PROGRESS_ID']
     191        elif 'upload_id' in _get:
     192            progress_id = _get['upload_id']
     193        elif 'progress_id' in _get:
     194            progress_id = _get['progress_id']
     195        elif len(_querystring) == 32:
     196            progress_id = _querystring
     197        else:
     198            return None
     199
     200        if not self.upload_id_re.match(progress_id):
     201            return None
     202
     203        return progress_id
     204
     205    progress_id = property(_get_file_progress_id)
     206
    170207    GET = property(_get_get, _set_get)
    171208    POST = property(_get_post, _set_post)
    172209    COOKIES = property(_get_cookies, _set_cookies)
  • django/core/handlers/modpython.py

     
    4747    def _load_post_and_files(self):
    4848        "Populates self._post and self._files"
    4949        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.progress_id)
     53            except:
     54                self._post, self._files = {}, {} # make sure we dont read the input stream again
     55                raise
    5156        else:
    5257            self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict()
    5358
     
    122127    def _get_method(self):
    123128        return self.META['REQUEST_METHOD'].upper()
    124129
     130    def _get_file_progress_id(self):
     131        """
     132        Returns the Progress ID of the request,
     133        usually provided if there is a file upload
     134        going on.
     135        Returns ``None`` if no progress ID is specified.
     136        """
     137        _get = self.GET
     138
     139        if 'X-Upload-ID' in self._req.headers_in:
     140            progress_id = self._req.headers_in['X-Upload-ID']
     141        elif 'X-Progress-ID' in self._req.headers_in:
     142            progress_id = self._req.headers_in['X-Progress-ID']
     143        elif 'upload_id' in _get:
     144            progress_id = _get['upload_id']
     145        elif 'progress_id' in _get:
     146            progress_id = _get['progress_id']
     147        elif self._req.args != None and len(self._req.args.strip()) == 32:
     148            progress_id = self._req.args
     149        else:
     150            return None
     151
     152        if not self.upload_id_re.match(progress_id):
     153            return None
     154
     155        return progress_id
     156
     157    progress_id = property(_get_file_progress_id)
     158
    125159    GET = property(_get_get, _set_get)
    126160    POST = property(_get_post, _set_post)
    127161    COOKIES = property(_get_cookies, _set_cookies)
  • tests/modeltests/test_client/views.py

     
    4444
    4545    return HttpResponse(t.render(c))
    4646
     47def 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
    4753def redirect_view(request):
    4854    "A view that redirects all requests to the GET view"
    4955    return HttpResponseRedirect('/test_client/get_view/')
  • tests/modeltests/test_client/models.py

     
    7575        self.assertEqual(response.template.name, "Book template")
    7676        self.assertEqual(response.content, "Blink - Malcolm Gladwell")
    7777
     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
    7893    def test_redirect(self):
    7994        "GET a URL that redirects elsewhere"
    8095        response = self.client.get('/test_client/redirect_view/')
  • tests/modeltests/test_client/urls.py

     
    44urlpatterns = patterns('',
    55    (r'^get_view/$', views.get_view),
    66    (r'^post_view/$', views.post_view),
     7    (r'^post_file_view/$', views.post_file_view),
    78    (r'^raw_post_view/$', views.raw_post_view),
    89    (r'^redirect_view/$', views.redirect_view),
    910    (r'^form_view/$', views.form_view),
  • docs/request_response.txt

     
    7272``FILES``
    7373    A dictionary-like object containing all uploaded files. Each key in
    7474    ``FILES`` is the ``name`` from the ``<input type="file" name="" />``. Each
    75     value in ``FILES`` is a standard Python dictionary with the following three
     75    value in ``FILES`` is a standard Python dictionary with the following four
    7676    keys:
    7777
    7878        * ``filename`` -- The name of the uploaded file, as a Python string.
    7979        * ``content-type`` -- The content type of the uploaded file.
    8080        * ``content`` -- The raw content of the uploaded file.
     81        * ``content-length`` -- The length of the content in bytes.
    8182
     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
    8294    Note that ``FILES`` will only contain data if the request method was POST
    8395    and the ``<form>`` that posted to the request had
    8496    ``enctype="multipart/form-data"``. Otherwise, ``FILES`` will be a blank
  • docs/settings.txt

     
    437437
    438438.. _Testing Django Applications: ../testing/
    439439
     440FILE_UPLOAD_DIR
     441---------------
     442
     443Default: ``None``
     444
     445Path to a directory where temporary files should be written during
     446file uploads. Leaving this as ``None`` will disable streaming file uploads,
     447and cause all uploaded files to be stored (temporarily) in memory.
     448
    440449IGNORABLE_404_ENDS
    441450------------------
    442451
     
    780789
    781790.. _site framework docs: ../sites/
    782791
     792STREAMING_MIN_POST_SIZE
     793-----------------------
     794
     795Default: 524288 (``512*1024``)
     796
     797An integer specifying the minimum number of bytes that has to be
     798received (in a POST) for file upload streaming to take place. Any
     799request smaller than this will be handled in memory.
     800Note: ``FILE_UPLOAD_DIR`` has to be defined to enable streaming.
     801
    783802TEMPLATE_CONTEXT_PROCESSORS
    784803---------------------------
    785804
  • docs/forms.txt

     
    475475   new_data = request.POST.copy()
    476476   new_data.update(request.FILES)
    477477
     478Streaming file uploads.
     479-----------------------
     480
     481File uploads will be read into memory by default. This works fine for
     482small to medium sized uploads (from 1MB to 100MB depending on your
     483setup and usage). If you want to support larger uploads you can enable
     484upload streaming where only a small part of the file will be in memory
     485at any time. To do this you need to specify the ``FILE_UPLOAD_DIR``
     486setting (see the settings_ document for more details).
     487
     488See `request object`_ for more details about ``request.FILES`` objects
     489with streaming file uploads enabled.
     490
    478491Validators
    479492==========
    480493
     
    693706.. _`generic views`: ../generic_views/
    694707.. _`models API`: ../model-api/
    695708.. _settings: ../settings/
     709.. _request object: ../request_response/#httprequest-objects
Back to Top