Django

Code

Ticket #2070: 5100_file_upload_core.diff

File 5100_file_upload_core.diff, 34.1 kB (added by Michael Axiak <axiak@mit.edu>, 1 year ago)

Meant to be cleaner, especially in light of #4165

  • django/http/__init__.py

    old new  
    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 
     56    def _get_file_progress(self): 
     57        """ 
     58        Returns the file progress for this request. 
     59        If no file progress is known, returns an empty 
     60        dictionary. 
     61        The request also keeps a local copy so that 
     62        the file is not accessed every time one wants to 
     63        ask for something. 
     64        """ 
     65        from django.conf import settings 
    7566 
     67        file_upload_dir = settings.FILE_UPLOAD_DIR 
     68        progress_id     = self.META['UPLOAD_PROGRESS_ID'] 
     69         
     70        if not progress_id or not file_upload_dir: 
     71            return {} 
     72 
     73        if getattr(self, '_file_progress', False) != False: 
     74            return self._file_progress 
     75 
     76        try: 
     77            f = open(os.path.join(file_upload_dir, progress_id), 'rb') 
     78            progress = pickle.load(f) 
     79            f.close() 
     80            self._file_progress = progress 
     81            return progress 
     82 
     83        except: 
     84            self._file_progress = {} 
     85            return {} 
     86 
     87    def _set_file_progress(self, new_progress): 
     88        """ 
     89        Sets the value of the file progress for this request. 
     90        If no file progress is underway, fails silently unless 
     91        DEBUG = True 
     92        """ 
     93 
     94        class NoUploadInProgress(Exception): 
     95            pass 
     96         
     97        from django.conf import settings 
     98 
     99        file_upload_dir = settings.FILE_UPLOAD_DIR 
     100        progress_id     = self.META['UPLOAD_PROGRESS_ID'] 
     101 
     102        if not progress_id or not file_upload_dir: 
     103            if settings.DEBUG: 
     104                raise NoUploadInProgress, 'There is no upload in progress.' 
     105 
     106 
     107        self._file_progress = new_progress 
     108        f = open(os.path.join(file_upload_dir, progress_id), 'wb') 
     109        pickle.dump(new_progress, f) 
     110        f.close() 
     111 
     112    file_progress = property(_get_file_progress, _set_file_progress) 
     113 
     114    def _get_file_progress_from_args(self, headers, get, querystring): 
     115 
     116        if 'X-Upload-ID' in headers: 
     117            progress_id = headers['X-Upload-ID'] 
     118        elif 'X-Progress-ID' in headers: 
     119            progress_id = headers['X-Progress-ID'] 
     120        elif 'upload_id' in get: 
     121            progress_id = get['upload_id'] 
     122        elif 'progress_id' in get: 
     123            progress_id = get['progress_id'] 
     124        elif querystring != None and len(querystring.strip()) == 32: 
     125            progress_id = querystring 
     126        else: 
     127            return None 
     128 
     129        if not self.upload_id_re.match(progress_id): 
     130            return None 
     131 
     132        return progress_id 
     133 
     134 
     135def parse_file_upload(headers, input, request): 
     136    from django.conf import settings 
     137 
     138    # Only stream files to disk if FILE_STREAMING_DIR is set 
     139    file_upload_dir = settings.FILE_UPLOAD_DIR 
     140    streaming_min_post_size = settings.STREAMING_MIN_POST_SIZE 
     141 
     142    try: 
     143        parser = MultiPartParser(headers, input, request, file_upload_dir, streaming_min_post_size) 
     144        return parser.parse() 
     145    except MultiPartParserError, e: 
     146        return MultiValueDict({ '_file_upload_error': [e.message] }), {} 
     147 
     148class MultiPartParserError(Exception): 
     149    def __init__(self, message): 
     150        self.message = message 
     151    def __str__(self): 
     152        return repr(self.message) 
     153         
     154class MultiPartParser(object): 
     155    """ 
     156    A rfc2388 multipart/form-data parser. 
     157     
     158    parse() reads the input stream in chunk_size chunks and returns a 
     159    tuple of (POST MultiValueDict, FILES MultiValueDict). If 
     160    file_upload_dir is defined files will be streamed to temporary 
     161    files in the specified directory. 
     162 
     163    The FILES dictionary will have 'filename', 'content-type', 
     164    'content' and 'content-length' entries. For streamed files it will 
     165    also have 'tmpfilename' and 'tmpfile'. The 'content' entry will 
     166    only be read from disk when referenced for streamed files. 
     167 
     168    If the header X-Progress-ID is sent with a 32 character hex string 
     169    a temporary file with the same name will be created in 
     170    `file_upload_dir`` with a pickled { 'received', 'size' } 
     171    dictionary with the number of bytes received and the size expected 
     172    respectively. The file will be unlinked when the parser finishes. 
     173 
     174    """ 
     175 
     176    def __init__(self, headers, input, request, file_upload_dir=None, streaming_min_post_size=None, chunk_size=1024*64): 
     177        try: 
     178            content_length = int(headers['Content-Length']) 
     179        except: 
     180            raise MultiPartParserError('Invalid Content-Length: %s' % headers.get('Content-Length')) 
     181 
     182        content_type = headers.get('Content-Type') 
     183 
     184        if not content_type or not content_type.startswith('multipart/'): 
     185            raise MultiPartParserError('Invalid Content-Type: %s' % content_type) 
     186             
     187        ctype, opts = self.parse_header(content_type) 
     188        boundary = opts.get('boundary') 
     189        from cgi import valid_boundary 
     190        if not boundary or not valid_boundary(boundary): 
     191            raise MultiPartParserError('Invalid boundary in multipart form: %s' % boundary) 
     192 
     193        progress_id = request.META['UPLOAD_PROGRESS_ID'] 
     194 
     195        if file_upload_dir and progress_id: 
     196            self._progress_filename = os.path.join(file_upload_dir, progress_id) 
     197        else: 
     198            self._progress_filename = None 
     199        self._boundary = '--' + boundary 
     200        self._input = input 
     201        self._size = content_length 
     202        self._received = 0 
     203        self._file_upload_dir = file_upload_dir 
     204        self._chunk_size = chunk_size 
     205        self._state = 'PREAMBLE' 
     206        self._partial = '' 
     207        self._post = MultiValueDict() 
     208        self._files = MultiValueDict() 
     209        self._request = request 
     210         
     211        if streaming_min_post_size is not None and content_length < streaming_min_post_size: 
     212            self._file_upload_dir = None # disable file streaming for small request 
     213 
     214        try: 
     215            # use mx fast string search if available 
     216            from mx.TextTools import FS 
     217            self._fs = FS(self._boundary) 
     218        except ImportError: 
     219            self._fs = None 
     220 
     221    def parse(self): 
     222        try: 
     223            self._parse() 
     224        finally: 
     225            if self._progress_filename: 
     226                self._request.file_progress = {'state': 'done'} 
     227                 
     228         
     229        return self._post, self._files 
     230 
     231    def _parse(self): 
     232        size = self._size 
     233 
     234        try: 
     235            while size > 0: 
     236                n = self._read(self._input, min(self._chunk_size, size)) 
     237                if not n: 
     238                    break 
     239                size -= n 
     240        except: 
     241            # consume any remaining data so we dont generate a "Connection Reset" error 
     242            size = self._size - self._received 
     243            while size > 0: 
     244                data = self._input.read(min(self._chunk_size, size)) 
     245                size -= len(data) 
     246            raise 
     247 
     248    def _find_boundary(self, data, start, stop): 
     249        """ 
     250        Find the next boundary and return the end of current part 
     251        and start of next part. 
     252        """ 
     253        if self._fs: 
     254            boundary = self._fs.find(data, start, stop) 
     255        else: 
     256            boundary = data.find(self._boundary, start, stop) 
     257        if boundary >= 0: 
     258            end = boundary 
     259            next = boundary + len(self._boundary) 
     260 
     261            # backup over CRLF 
     262            if end > 0 and data[end-1] == '\n': end -= 1 
     263            if end > 0 and data[end-1] == '\r': end -= 1 
     264            # skip over --CRLF 
     265            if next < stop and data[next] == '-': next += 1 
     266            if next < stop and data[next] == '-': next += 1 
     267            if next < stop and data[next] == '\r': next += 1 
     268            if next < stop and data[next] == '\n': next += 1 
     269 
     270            return True, end, next 
     271        else: 
     272            return False, stop, stop 
     273 
     274    class TemporaryFile(object): 
     275        "A temporary file that tries to delete itself when garbage collected." 
     276        def __init__(self, dir): 
     277            import tempfile 
     278            (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir) 
     279            self.file = os.fdopen(fd, 'w+b') 
     280            self.name = name 
     281 
     282        def __getattr__(self, name): 
     283            a = getattr(self.__dict__['file'], name) 
     284            if type(a) != type(0): 
     285                setattr(self, name, a) 
     286            return a 
     287 
     288        def __del__(self): 
     289            try: 
     290                os.unlink(self.name) 
     291            except OSError: 
     292                pass 
     293             
     294    class LazyContent(dict): 
     295        """ 
     296        A lazy FILES dictionary entry that reads the contents from 
     297        tmpfile only when referenced. 
     298        """ 
     299        def __init__(self, data): 
     300            dict.__init__(self, data) 
     301         
     302        def __getitem__(self, key): 
     303            if key == 'content' and not self.has_key(key): 
     304                self['tmpfile'].seek(0) 
     305                self['content'] = self['tmpfile'].read() 
     306            return dict.__getitem__(self, key) 
     307 
     308    def _read(self, input, size): 
     309        data = input.read(size) 
     310 
     311        if not data: 
     312            return 0 
     313 
     314        read_size = len(data) 
     315        self._received += read_size 
     316 
     317        if self._partial: 
     318            data = self._partial + data 
     319 
     320        start = 0 
     321        stop = len(data) 
     322         
     323        while start < stop: 
     324            boundary, end, next = self._find_boundary(data, start, stop) 
     325 
     326            if not boundary and read_size: 
     327                # make sure we dont treat a partial boundary (and its separators) as data 
     328                stop -= len(self._boundary) + 16 
     329                end = next = stop 
     330                if end <= start: 
     331                    break # need more data 
     332 
     333            if self._state == 'PREAMBLE': 
     334                # Preamble, just ignore it 
     335                self._state = 'HEADER' 
     336 
     337            elif self._state == 'HEADER': 
     338                # Beginning of header, look for end of header and parse it if found. 
     339 
     340                header_end = data.find('\r\n\r\n', start, stop) 
     341                if header_end == -1: 
     342                    break # need more data 
     343 
     344                header = data[start:header_end] 
     345 
     346                self._fieldname = None 
     347                self._filename = None 
     348                self._content_type = None 
     349 
     350                for line in header.split('\r\n'): 
     351                    ctype, opts = self.parse_header(line) 
     352                    if ctype == 'content-disposition: form-data': 
     353                        self._fieldname = opts.get('name') 
     354                        self._filename = opts.get('filename') 
     355                    elif ctype.startswith('content-type: '): 
     356                        self._content_type = ctype[14:] 
     357 
     358                if self._filename is not None: 
     359                    # cleanup filename from IE full paths: 
     360                    self._filename = self._filename[self._filename.rfind("\\")+1:].strip() 
     361 
     362                    if self._filename: # ignore files without filenames 
     363                        if self._file_upload_dir: 
     364                            try: 
     365                                self._file = self.TemporaryFile(dir=self._file_upload_dir) 
     366                            except: 
     367                                raise MultiPartParserError("Failed to create temporary file.") 
     368                        else: 
     369                            self._file = StringIO() 
     370                    else: 
     371                        self._file = None 
     372                    self._filesize = 0 
     373                    self._state = 'FILE' 
     374                else: 
     375                    self._field = StringIO() 
     376                    self._state = 'FIELD' 
     377                next = header_end + 4 
     378 
     379            elif self._state == 'FIELD': 
     380                # In a field, collect data until a boundary is found. 
     381 
     382                self._field.write(data[start:end]) 
     383                if boundary: 
     384                    if self._fieldname: 
     385                        self._post.appendlist(self._fieldname, self._field.getvalue()) 
     386                    self._field.close() 
     387                    self._state = 'HEADER' 
     388 
     389            elif self._state == 'FILE': 
     390                # In a file, collect data until a boundary is found. 
     391 
     392                if self._file: 
     393                    try: 
     394                        self._file.write(data[start:end]) 
     395                    except IOError, e: 
     396                        raise MultiPartParserError("Failed to write to temporary file.") 
     397                    self._filesize += end-start 
     398 
     399                    if self._progress_filename: 
     400                        self._request.file_progress = {'received': self._received, 
     401                                                       'size':     self._size, 
     402                                                       'state':    'uploading'} 
     403 
     404                if boundary: 
     405                    if self._file: 
     406                        if self._file_upload_dir: 
     407                            self._file.seek(0) 
     408                            file = self.LazyContent({ 
     409                                'filename': self._filename, 
     410                                'content-type':  self._content_type, 
     411                                # 'content': is read on demand 
     412                                'content-length': self._filesize, 
     413                                'tmpfilename': self._file.name, 
     414                                'tmpfile': self._file 
     415                            }) 
     416                        else: 
     417                            file = { 
     418                                'filename': self._filename, 
     419                                'content-type':  self._content_type, 
     420                                'content': self._file.getvalue(), 
     421                                'content-length': self._filesize 
     422                            } 
     423                            self._file.close() 
     424 
     425                        self._files.appendlist(self._fieldname, file) 
     426 
     427                    self._state = 'HEADER' 
     428 
     429            start = next 
     430                 
     431        self._partial = data[start:] 
     432 
     433        return read_size 
     434 
     435    def parse_header(self, line): 
     436        from cgi import parse_header 
     437        return parse_header(line) 
     438 
    76439class QueryDict(MultiValueDict): 
    77440    """A specialized MultiValueDict that takes a query string when initialized. 
    78441    This is immutable unless you create a copy of it.""" 
     
    306669    if not host: 
    307670        host = request.META.get('HTTP_HOST', '') 
    308671    return host 
     672 
  • django/oldforms/__init__.py

    old new  
    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/db/models/base.py

    old new  
    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

    old new  
    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/conf/global_settings.py

    old new  
    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/core/handlers/wsgi.py

    old new  
    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) 
     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        return self._get_file_progress_from_args(self.environ, 
     185                                                 self.GET, 
     186                                                 self._req.args) 
     187 
    170188    GET = property(_get_get, _set_get) 
    171189    POST = property(_get_post, _set_post) 
    172190    COOKIES = property(_get_cookies, _set_cookies) 
  • django/core/handlers/modpython.py

    old new  
    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) 
     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 
     
    9297                'AUTH_TYPE':         self._req.ap_auth_type, 
    9398                'CONTENT_LENGTH':    self._req.clength, # This may be wrong 
    9499                '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' 
    109115            } 
    110116            for key, value in self._req.headers_in.items(): 
    111117                key = 'HTTP_' + key.upper().replace('-', '_') 
     
    122128    def _get_method(self): 
    123129        return self.META['REQUEST_METHOD'].upper() 
    124130 
     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 
     142 
    125143    GET = property(_get_get, _set_get) 
    126144    POST = property(_get_post, _set_post) 
    127145    COOKIES = property(_get_cookies, _set_cookies) 
  • tests/modeltests/test_client/views.py

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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

    old new  
    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