Ticket #2070: 6469_streaming_file_upload.diff

File 6469_streaming_file_upload.diff, 38.5 KB (added by Faheem Mitha <faheem@…>, 17 years ago)
  • django/conf/global_settings.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/conf/global_settings.py
    a b from django import get_version  
    251251from django import get_version
    252252URL_VALIDATOR_USER_AGENT = "Django/%s (http://www.djangoproject.com)" % get_version()
    253253
     254# The directory to place streamed file uploads. The web server needs write
     255# permissions on this directory.
     256# If this is None, streaming uploads are disabled.
     257FILE_UPLOAD_DIR = None
     258
     259# The minimum size of a POST before file uploads are streamed to disk.
     260# Any less than this number, and the file is uploaded to memory.
     261# Size is in bytes.
     262STREAMING_MIN_POST_SIZE = 512 * (2**10)
     263
    254264##############
    255265# MIDDLEWARE #
    256266##############
  • django/core/handlers/modpython.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/core/handlers/modpython.py
    a b class ModPythonRequest(http.HttpRequest)  
    5151    def _load_post_and_files(self):
    5252        "Populates self._post and self._files"
    5353        if 'content-type' in self._req.headers_in and self._req.headers_in['content-type'].startswith('multipart'):
    54             self._post, self._files = http.parse_file_upload(self._req.headers_in, self.raw_post_data)
     54            self._raw_post_data = None # raw data is not available for streamed multipart messages
     55            try:
     56                self._post, self._files = http.parse_file_upload(self._req.headers_in, self._req, self)
     57            except:
     58                self._post, self._files = {}, {} # make sure we dont read the input stream again
     59                raise
    5560        else:
    5661            self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
    5762
    class ModPythonRequest(http.HttpRequest)  
    96101                'AUTH_TYPE':         self._req.ap_auth_type,
    97102                'CONTENT_LENGTH':    self._req.clength, # This may be wrong
    98103                'CONTENT_TYPE':      self._req.content_type, # This may be wrong
    99                 'GATEWAY_INTERFACE': 'CGI/1.1',
    100                 'PATH_INFO':         self._req.path_info,
    101                 'PATH_TRANSLATED':   None, # Not supported
    102                 'QUERY_STRING':      self._req.args,
    103                 'REMOTE_ADDR':       self._req.connection.remote_ip,
    104                 'REMOTE_HOST':       None, # DNS lookups not supported
    105                 'REMOTE_IDENT':      self._req.connection.remote_logname,
    106                 'REMOTE_USER':       self._req.user,
    107                 'REQUEST_METHOD':    self._req.method,
    108                 'SCRIPT_NAME':       None, # Not supported
    109                 'SERVER_NAME':       self._req.server.server_hostname,
    110                 'SERVER_PORT':       self._req.server.port,
    111                 'SERVER_PROTOCOL':   self._req.protocol,
    112                 'SERVER_SOFTWARE':   'mod_python'
     104                'GATEWAY_INTERFACE':  'CGI/1.1',
     105                'PATH_INFO':          self._req.path_info,
     106                'PATH_TRANSLATED':    None, # Not supported
     107                'QUERY_STRING':       self._req.args,
     108                'REMOTE_ADDR':        self._req.connection.remote_ip,
     109                'REMOTE_HOST':        None, # DNS lookups not supported
     110                'REMOTE_IDENT':       self._req.connection.remote_logname,
     111                'REMOTE_USER':        self._req.user,
     112                'REQUEST_METHOD':     self._req.method,
     113                'SCRIPT_NAME':        None, # Not supported
     114                'SERVER_NAME':        self._req.server.server_hostname,
     115                'SERVER_PORT':        self._req.server.port,
     116                'SERVER_PROTOCOL':    self._req.protocol,
     117                'UPLOAD_PROGRESS_ID': self._get_file_progress_id(),
     118                'SERVER_SOFTWARE':    'mod_python'
    113119            }
    114120            for key, value in self._req.headers_in.items():
    115121                key = 'HTTP_' + key.upper().replace('-', '_')
    class ModPythonRequest(http.HttpRequest)  
    125131
    126132    def _get_method(self):
    127133        return self.META['REQUEST_METHOD'].upper()
     134
     135    def _get_file_progress_id(self):
     136        """
     137        Returns the Progress ID of the request,
     138        usually provided if there is a file upload
     139        going on.
     140        Returns ``None`` if no progress ID is specified.
     141        """
     142        return self._get_file_progress_from_args(self._req.headers_in,
     143                                                 self.GET,
     144                                                 self._req.args)
    128145
    129146    GET = property(_get_get, _set_get)
    130147    POST = property(_get_post, _set_post)
  • django/core/handlers/wsgi.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/core/handlers/wsgi.py
    a b class WSGIRequest(http.HttpRequest):  
    7777        self.environ = environ
    7878        self.path = force_unicode(environ['PATH_INFO'])
    7979        self.META = environ
     80        self.META['UPLOAD_PROGRESS_ID'] = self._get_file_progress_id()
    8081        self.method = environ['REQUEST_METHOD'].upper()
    8182
    8283    def __repr__(self):
    class WSGIRequest(http.HttpRequest):  
    114115            if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
    115116                header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')])
    116117                header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '')
    117                 self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data)
     118                header_dict['Content-Length'] = self.environ.get('CONTENT_LENGTH', '')
     119                header_dict['X-Progress-ID'] = self.environ.get('HTTP_X_PROGRESS_ID', '')
     120                try:
     121                    self._post, self._files = http.parse_file_upload(header_dict, self.environ['wsgi.input'], self)
     122                except:
     123                    self._post, self._files = {}, {} # make sure we dont read the input stream again
     124                    raise
     125                self._raw_post_data = None # raw data is not available for streamed multipart messages
    118126            else:
    119127                self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
    120128        else:
    class WSGIRequest(http.HttpRequest):  
    170178            buf.close()
    171179            return self._raw_post_data
    172180
     181    def _get_file_progress_id(self):
     182        """
     183        Returns the Progress ID of the request,
     184        usually provided if there is a file upload
     185        going on.
     186        Returns ``None`` if no progress ID is specified.
     187        """
     188        return self._get_file_progress_from_args(self.environ,
     189                                                 self.GET,
     190                                                 self.environ.get('QUERY_STRING', ''))
     191
    173192    GET = property(_get_get, _set_get)
    174193    POST = property(_get_post, _set_post)
    175194    COOKIES = property(_get_cookies, _set_cookies)
  • django/db/models/base.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/db/models/base.py
    a b from django.dispatch import dispatcher  
    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.utils.encoding import smart_str, force_unicode, smart_unicode
    1617from django.conf import settings
    1718from itertools import izip
    class Model(object):  
    379380    def _get_FIELD_size(self, field):
    380381        return os.path.getsize(self._get_FIELD_filename(field))
    381382
    382     def _save_FIELD_file(self, field, filename, raw_contents, save=True):
     383    def _save_FIELD_file(self, field, filename, raw_field, save=True):
    383384        directory = field.get_directory_name()
    384385        try: # Create the date-based directory if it doesn't exist.
    385386            os.makedirs(os.path.join(settings.MEDIA_ROOT, directory))
    386387        except OSError: # Directory probably already exists.
    387388            pass
     389
     390        if filename is None:
     391            filename = raw_field['filename']
     392
    388393        filename = field.get_filename(filename)
    389394
    390395        # If the filename already exists, keep adding an underscore to the name of
    class Model(object):  
    401406        setattr(self, field.attname, filename)
    402407
    403408        full_filename = self._get_FIELD_filename(field)
    404         fp = open(full_filename, 'wb')
    405         fp.write(raw_contents)
    406         fp.close()
     409        if raw_field.has_key('tmpfilename'):
     410            raw_field['tmpfile'].close()
     411            file_move_safe(raw_field['tmpfilename'], full_filename)
     412        else:
     413            from django.utils import file_locks
     414            fp = open(full_filename, 'wb')
     415            # exclusive lock
     416            file_locks.lock(fp, file_locks.LOCK_EX)
     417            fp.write(raw_field['content'])
     418            fp.close()
    407419
    408420        # Save the width and/or height, if applicable.
    409421        if isinstance(field, ImageField) and (field.width_field or field.height_field):
  • django/db/models/fields/__init__.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/db/models/fields/__init__.py
    a b class FileField(Field):  
    757757        setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
    758758        setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
    759759        setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
    760         setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save))
     760        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save))
     761        setattr(cls, 'move_%s_file' % self.name, lambda instance, raw_field, save=True: instance._save_FIELD_file(self, None, raw_field, save))       
    761762        dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
    762763
    763764    def delete_file(self, instance):
    class FileField(Field):  
    780781        if new_data.get(upload_field_name, False):
    781782            func = getattr(new_object, 'save_%s_file' % self.name)
    782783            if rel:
    783                 func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"], save)
     784                func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0], save)
    784785            else:
    785                 func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"], save)
     786                func(new_data[upload_field_name]["filename"], new_data[upload_field_name], save)
    786787
    787788    def get_directory_name(self):
    788789        return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
  • django/http/__init__.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/http/__init__.py
    a b import os  
    11import os
     2import re
    23from Cookie import SimpleCookie
    34from pprint import pformat
    45from urllib import urlencode
    56from urlparse import urljoin
     7from django.http.multipartparser import MultiPartParser, MultiPartParserError
    68from django.utils.datastructures import MultiValueDict, FileDict
    79from django.utils.encoding import smart_str, iri_to_uri, force_unicode
     10
     11upload_id_re = re.compile(r'^[a-fA-F0-9]{32}$') # file progress id Regular expression
    812
    913RESERVED_CHARS="!*'();:@&=+$,/?%#[]"
    1014
    class HttpRequest(object):  
    7882
    7983    def is_secure(self):
    8084        return os.environ.get("HTTPS") == "on"
    81 
     85       
    8286    def _set_encoding(self, val):
    8387        """
    8488        Sets the encoding used for GET/POST accesses. If the GET or POST
    class HttpRequest(object):  
    96100
    97101    encoding = property(_get_encoding, _set_encoding)
    98102
    99 def parse_file_upload(header_dict, post_data):
    100     "Returns a tuple of (POST QueryDict, FILES MultiValueDict)"
    101     import email, email.Message
    102     from cgi import parse_header
    103     raw_message = '\r\n'.join(['%s:%s' % pair for pair in header_dict.items()])
    104     raw_message += '\r\n\r\n' + post_data
    105     msg = email.message_from_string(raw_message)
    106     POST = QueryDict('', mutable=True)
    107     FILES = MultiValueDict()
    108     for submessage in msg.get_payload():
    109         if submessage and isinstance(submessage, email.Message.Message):
    110             name_dict = parse_header(submessage['Content-Disposition'])[1]
    111             # name_dict is something like {'name': 'file', 'filename': 'test.txt'} for file uploads
    112             # or {'name': 'blah'} for POST fields
    113             # We assume all uploaded files have a 'filename' set.
    114             if 'filename' in name_dict:
    115                 assert type([]) != type(submessage.get_payload()), "Nested MIME messages are not supported"
    116                 if not name_dict['filename'].strip():
    117                     continue
    118                 # IE submits the full path, so trim everything but the basename.
    119                 # (We can't use os.path.basename because that uses the server's
    120                 # directory separator, which may not be the same as the
    121                 # client's one.)
    122                 filename = name_dict['filename'][name_dict['filename'].rfind("\\")+1:]
    123                 FILES.appendlist(name_dict['name'], FileDict({
    124                     'filename': filename,
    125                     'content-type': 'Content-Type' in submessage and submessage['Content-Type'] or None,
    126                     'content': submessage.get_payload(),
    127                 }))
    128             else:
    129                 POST.appendlist(name_dict['name'], submessage.get_payload())
    130     return POST, FILES
     103    def _get_file_progress(self):
     104        return {}
     105   
     106    def _set_file_progress(self,value):
     107        pass
     108
     109    def _del_file_progress(self):
     110        pass
     111
     112    file_progress = property(_get_file_progress,
     113                             _set_file_progress,
     114                             _del_file_progress)
     115
     116    def _get_file_progress_from_args(self, headers, get, querystring):
     117        """
     118        This parses the request for a file progress_id value.
     119        Note that there are two distinct ways of getting the progress
     120        ID -- header and GET. One is used primarily to attach via JavaScript
     121        to the end of an HTML form action while the other is used for AJAX
     122        communication.
     123
     124        All progress IDs must be valid 32-digit hexadecimal numbers.
     125        """
     126        if 'X-Upload-ID' in headers:
     127            progress_id = headers['X-Upload-ID']
     128        elif 'progress_id' in get:
     129            progress_id = get['progress_id']
     130        else:
     131            return None
     132
     133        if not upload_id_re.match(progress_id):
     134            return None
     135
     136        return progress_id
     137
     138def parse_file_upload(headers, input, request):
     139    from django.conf import settings
     140
     141    # Only stream files to disk if FILE_STREAMING_DIR is set
     142    file_upload_dir = settings.FILE_UPLOAD_DIR
     143    streaming_min_post_size = settings.STREAMING_MIN_POST_SIZE
     144
     145    try:
     146        parser = MultiPartParser(headers, input, request, file_upload_dir, streaming_min_post_size)
     147        return parser.parse()
     148    except MultiPartParserError, e:
     149        return MultiValueDict({ '_file_upload_error': [e.message] }), {}
     150
    131151
    132152class QueryDict(MultiValueDict):
    133153    """
  • new file django/http/multipartparser.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/http/multipartparser.py
    - +  
     1"""
     2MultiPart parsing for file uploads.
     3If both a progress id is sent (either through ``X-Progress-ID``
     4header or ``progress_id`` GET) and ``FILE_UPLOAD_DIR`` is set
     5in the settings, then the file progress will be tracked using
     6``request.file_progress``.
     7
     8To 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
     35from django.utils.datastructures import MultiValueDict
     36import os
     37
     38try:
     39    from cStringIO import StringIO
     40except ImportError:
     41    from StringIO import StringIO
     42
     43
     44class MultiPartParserError(Exception):
     45    def __init__(self, message):
     46        self.message = message
     47    def __str__(self):
     48        return repr(self.message)
     49
     50class 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), e:
     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/oldforms/__init__.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/oldforms/__init__.py
    a b class FileUploadField(FormField):  
    680680        self.validator_list = [self.isNonEmptyFile] + validator_list
    681681
    682682    def isNonEmptyFile(self, field_data, all_data):
    683         try:
    684             content = field_data['content']
    685         except TypeError:
     683        if field_data.has_key('_file_upload_error'):
     684            raise validators.CriticalValidationError, field_data['_file_upload_error']
     685        if not field_data.has_key('filename'):
    686686            raise validators.CriticalValidationError, ugettext("No file was submitted. Check the encoding type on the form.")
    687         if not content:
     687        if not field_data['content-length']:
    688688            raise validators.CriticalValidationError, ugettext("The submitted file is empty.")
    689689
    690690    def render(self, data):
    691691        return u'<input type="file" id="%s" class="v%s" name="%s" />' % \
    692692            (self.get_id(), self.__class__.__name__, self.field_name)
     693
     694    def prepare(self, new_data):
     695        if new_data.has_key('_file_upload_error'):
     696            # pretend we got something in the field to raise a validation error later
     697            new_data[self.field_name] = { '_file_upload_error': new_data['_file_upload_error'] }
    693698
    694699    def html2python(data):
    695700        if data is None:
  • new file django/utils/file.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/utils/file.py
    - +  
     1import os
     2
     3__all__ = ['file_move_safe']
     4
     5try:
     6    import shutil
     7    file_move = shutil.move
     8except ImportError:
     9    file_move = os.rename
     10
     11def 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
  • new file django/utils/file_locks.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 django/utils/file_locks.py
    - +  
     1"""
     2Locking portability by Jonathan Feignberg <jdf@pobox.com> in python cookbook
     3
     4Example 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
     16import os
     17
     18__all__ = ['LOCK_EX','LOCK_SH','LOCK_NB','lock','unlock']
     19
     20if 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()
     28elif os.name == 'posix':
     29        import fcntl
     30        LOCK_EX = fcntl.LOCK_EX
     31        LOCK_SH = fcntl.LOCK_SH
     32        LOCK_NB = fcntl.LOCK_NB
     33else:
     34        raise RuntimeError("Locking only defined for nt and posix platforms")
     35
     36if 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
     45elif 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)
  • docs/forms.txt

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 docs/forms.txt
    a b this::  
    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
    fails. If no message is passed in, a def  
    698711.. _`generic views`: ../generic_views/
    699712.. _`models API`: ../model-api/
    700713.. _settings: ../settings/
     714.. _request object: ../request_response/#httprequest-objects
  • docs/request_response.txt

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 docs/request_response.txt
    a b All attributes except ``session`` should  
    8282``FILES``
    8383    A dictionary-like object containing all uploaded files. Each key in
    8484    ``FILES`` is the ``name`` from the ``<input type="file" name="" />``. Each
    85     value in ``FILES`` is a standard Python dictionary with the following three
     85    value in ``FILES`` is a standard Python dictionary with the following four
    8686    keys:
    8787
    8888        * ``filename`` -- The name of the uploaded file, as a Python string.
    8989        * ``content-type`` -- The content type of the uploaded file.
    9090        * ``content`` -- The raw content of the uploaded file.
     91        * ``content-length`` -- The length of the content in bytes.
     92
     93    If streaming file uploads are enabled two additional keys
     94    describing the uploaded file will be present:
     95
     96        * ``tmpfilename`` -- The filename for the temporary file.
     97        * ``tmpfile`` -- An open file object for the temporary file.
     98
     99    The temporary file will be removed when the request finishes.
     100
     101    Note that accessing ``content`` when streaming uploads are enabled
     102    will read the whole file into memory which may not be what you want.
    91103
    92104    Note that ``FILES`` will only contain data if the request method was POST
    93105    and the ``<form>`` that posted to the request had
  • docs/settings.txt

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 docs/settings.txt
    a b these paths should use Unix-style forwar  
    479479`Testing Django Applications`_.
    480480
    481481.. _Testing Django Applications: ../testing/
     482
     483FILE_UPLOAD_DIR
     484---------------
     485
     486Default: ``None``
     487
     488Path to a directory where temporary files should be written during
     489file uploads. Leaving this as ``None`` will disable streaming file uploads,
     490and cause all uploaded files to be stored (temporarily) in memory.
    482491
    483492IGNORABLE_404_ENDS
    484493------------------
    See the `site framework docs`_.  
    822831
    823832.. _site framework docs: ../sites/
    824833
     834STREAMING_MIN_POST_SIZE
     835-----------------------
     836
     837Default: 524288 (``512*1024``)
     838
     839An integer specifying the minimum number of bytes that has to be
     840received (in a POST) for file upload streaming to take place. Any
     841request smaller than this will be handled in memory.
     842Note: ``FILE_UPLOAD_DIR`` has to be defined to enable streaming.
     843
    825844TEMPLATE_CONTEXT_PROCESSORS
    826845---------------------------
    827846
  • tests/modeltests/test_client/models.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 tests/modeltests/test_client/models.py
    a b 38. Testing using the Test Client  
    44
    55The test client is a class that can act like a simple
    66browser for testing purposes.
    7  
     7
    88It allows the user to compose GET and POST requests, and
    99obtain the response that the server gave to those requests.
    1010The server Response objects are annotated with the details
    class ClientTest(TestCase):  
    8080        self.assertEqual(response.template.name, "Book template")
    8181        self.assertEqual(response.content, "Blink - Malcolm Gladwell")
    8282
     83    def test_post_file_view(self):
     84        "POST this python file to a view"
     85        import os, tempfile
     86        from django.conf import settings
     87        file = __file__.replace('.pyc', '.py')
     88        for upload_dir, streaming_size in [(None,512*1000), (tempfile.gettempdir(), 1)]:
     89            settings.FILE_UPLOAD_DIR = upload_dir
     90            settings.STREAMING_MIN_POST_SIZE = streaming_size
     91            post_data = { 'name': file, 'file_file': open(file) }
     92            response = self.client.post('/test_client/post_file_view/', post_data)
     93            self.failUnless('models.py' in response.context['file']['filename'])
     94            self.failUnless(len(response.context['file']['content']) == os.path.getsize(file))
     95            if upload_dir:
     96                self.failUnless(response.context['file']['tmpfilename'])
     97
    8398    def test_redirect(self):
    8499        "GET a URL that redirects elsewhere"
    85100        response = self.client.get('/test_client/redirect_view/')
  • tests/modeltests/test_client/urls.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 tests/modeltests/test_client/urls.py
    a b urlpatterns = patterns('',  
    55urlpatterns = patterns('',
    66    (r'^get_view/$', views.get_view),
    77    (r'^post_view/$', views.post_view),
     8    (r'^post_file_view/$', views.post_file_view),
    89    (r'^raw_post_view/$', views.raw_post_view),
    910    (r'^redirect_view/$', views.redirect_view),
    1011    (r'^permanent_redirect_view/$', redirect_to, { 'url': '/test_client/get_view/' }),
  • tests/modeltests/test_client/views.py

    diff -r 4ce69c5b8f1d -r 1c51845bdd22 tests/modeltests/test_client/views.py
    a b def raw_post_view(request):  
    4545        t = Template("GET request.", name="Book GET template")
    4646        c = Context()
    4747
     48    return HttpResponse(t.render(c))
     49
     50def post_file_view(request):
     51    "A view that expects a multipart post and returns a file in the context"
     52    t = Template('File {{ file.filename }} received', name='POST Template')
     53    c = Context({'file': request.FILES['file_file']})
    4854    return HttpResponse(t.render(c))
    4955
    5056def redirect_view(request):
Back to Top