Django

Code

Ticket #2070: 5126_file_upload_wsgi_tested.diff

File 5126_file_upload_wsgi_tested.diff, 41.3 kB (added by Michael Axiak <axiak@mit.edu>, 1 year ago)

Tested with WSGI and made a few changes.

  • django/http/file_descriptor.py

    old new  
     1""" 
     2This file contains a fallback FileProgressDescriptor 
     3for file upload progress. 
     4""" 
     5import pickle 
     6import os 
     7 
     8class DefaultFileProgressDescriptor(object): 
     9 
     10    def __init__(self, FileException): 
     11        self.FileException = FileException 
     12     
     13    def __get__(self, request, HttpRequest): 
     14        """ 
     15        Returns the file progress for this request. 
     16        If no file progress is known, returns an empty 
     17        dictionary. 
     18        The request also keeps a local copy so that 
     19        the file is not accessed every time one wants to 
     20        ask for something. 
     21        """ 
     22        from django.conf import settings 
     23 
     24        file_upload_dir = settings.FILE_UPLOAD_DIR 
     25        progress_id     = request.META['UPLOAD_PROGRESS_ID'] 
     26         
     27        if not progress_id or not file_upload_dir: 
     28            return {} 
     29 
     30        if getattr(self, '_file_progress', False) != False: 
     31            return self._file_progress 
     32 
     33        try: 
     34            f = open(os.path.join(file_upload_dir, progress_id), 'rb') 
     35            progress = pickle.load(f) 
     36            f.close() 
     37            self._file_progress = progress 
     38            return progress 
     39        except: 
     40            self._file_progress = {} 
     41            return {} 
     42 
     43    def __set__(self, request, new_progress): 
     44        """ 
     45        Sets the value of the file progress for this request. 
     46        If no file progress is underway, raises an error. 
     47        """ 
     48 
     49        from django.conf import settings 
     50 
     51        file_upload_dir = settings.FILE_UPLOAD_DIR 
     52        progress_id     = request.META['UPLOAD_PROGRESS_ID'] 
     53 
     54        if not progress_id or not file_upload_dir: 
     55            raise self.FileException('There is no upload in progress.') 
     56 
     57        self._file_progress = new_progress 
     58        f = open(os.path.join(file_upload_dir, progress_id), 'wb') 
     59        pickle.dump(new_progress, f) 
     60        f.close() 
     61 
     62    def __delete__(self, request): 
     63        """ 
     64        Removes the file if there is an upload in process. 
     65        """ 
     66        file_upload_dir = settings.FILE_UPLOAD_DIR 
     67        progress_id     = request.META['UPLOAD_PROGRESS_ID'] 
     68 
     69        if not progress_id or not file_upload_dir: 
     70            raise self.FileException('There is no upload in progress.') 
     71 
     72        try: 
     73            os.remove(os.path.join(file_upload_dir, progress_id)) 
     74        except: 
     75            pass 
  • 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 
     6from django.http.file_descriptor import DefaultFileProgressDescriptor 
     7import re 
    68 
     9try: 
     10    from cStringIO import StringIO 
     11except ImportError: 
     12    from StringIO import StringIO 
     13 
    714RESERVED_CHARS="!*'();:@&=+$,/?%#[]" 
    815 
     16 
    917try: 
    1018    # The mod_python version is more efficient, so try importing it first. 
    1119    from mod_python.util import parse_qsl 
    1220except ImportError: 
    1321    from cgi import parse_qsl 
    1422 
     23class MetaFileProgressDescriptor(object): 
     24    """ 
     25    This descriptor allows other descriptors to 
     26    be loaded in runtime to a request instance. 
     27    """ 
     28    def __get__(self, request, *args, **kwargs): 
     29        return request._file_progress.__get__(request, *args, **kwargs) 
     30 
     31    def __set__(self, request, *args, **kwargs): 
     32        return request._file_progress.__set__(request, *args, **kwargs) 
     33 
     34    def __delete__(self, request, *args, **kwargs): 
     35        return request._file_progress.__delete__(request, *args, **kwargs) 
     36 
    1537class Http404(Exception): 
    1638    pass 
    1739 
    1840class HttpRequest(object): 
    1941    "A basic HTTP request" 
     42 
     43    upload_id_re = re.compile(r'^[a-fA-F0-9]{32}$') 
     44    file_progress = MetaFileProgressDescriptor() 
     45 
    2046    def __init__(self): 
    2147        self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} 
    2248        self.path = '' 
     
    4167 
    4268    def is_secure(self): 
    4369        return os.environ.get("HTTPS") == "on" 
     70  
     71    def _get_file_progress_from_args(self, headers, get, querystring): 
    4472 
    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 
     73        if 'X-Upload-ID' in headers: 
     74            progress_id = headers['X-Upload-ID'] 
     75        elif 'X-Progress-ID' in headers: 
     76            progress_id = headers['X-Progress-ID'] 
     77        elif 'HTTP_X_UPLOAD_ID' in headers: 
     78            progress_id = headers['HTTP_X_UPLOAD_ID'] 
     79        elif 'HTTP_X_PROGRESS_ID' in headers: 
     80            progress_id = headers['HTTP_X_PROGRESS_ID'] 
     81        elif 'upload_id' in get: 
     82            progress_id = get['upload_id'] 
     83        elif 'progress_id' in get: 
     84            progress_id = get['progress_id'] 
     85        elif querystring != None and len(querystring.strip()) == 32: 
     86            progress_id = querystring 
     87        else: 
     88            return None 
    7589 
     90        if not self.upload_id_re.match(progress_id): 
     91            return None 
     92 
     93        return progress_id 
     94 
     95 
     96def parse_file_upload(headers, input, request): 
     97    from django.conf import settings 
     98 
     99    # Only stream files to disk if FILE_STREAMING_DIR is set 
     100    file_upload_dir = settings.FILE_UPLOAD_DIR 
     101    streaming_min_post_size = settings.STREAMING_MIN_POST_SIZE 
     102 
     103    try: 
     104        parser = MultiPartParser(headers, input, request, file_upload_dir, streaming_min_post_size) 
     105        return parser.parse() 
     106    except MultiPartParserError, e: 
     107        return MultiValueDict({ '_file_upload_error': [e.message] }), {} 
     108 
     109class MultiPartParserError(Exception): 
     110    def __init__(self, message): 
     111        self.message = message 
     112    def __str__(self): 
     113        return repr(self.message) 
     114         
     115class MultiPartParser(object): 
     116    """ 
     117    A rfc2388 multipart/form-data parser. 
     118     
     119    parse() reads the input stream in chunk_size chunks and returns a 
     120    tuple of (POST MultiValueDict, FILES MultiValueDict). If 
     121    file_upload_dir is defined files will be streamed to temporary 
     122    files in the specified directory. 
     123 
     124    The FILES dictionary will have 'filename', 'content-type', 
     125    'content' and 'content-length' entries. For streamed files it will 
     126    also have 'tmpfilename' and 'tmpfile'. The 'content' entry will 
     127    only be read from disk when referenced for streamed files. 
     128 
     129    If the header X-Progress-ID is sent with a 32 character hex string 
     130    a temporary file with the same name will be created in 
     131    `file_upload_dir`` with a pickled { 'received', 'size' } 
     132    dictionary with the number of bytes received and the size expected 
     133    respectively. The file will be unlinked when the parser finishes. 
     134 
     135    """ 
     136 
     137    def __init__(self, headers, input, request, file_upload_dir=None, streaming_min_post_size=None, chunk_size=1024*64): 
     138        try: 
     139            content_length = int(headers['Content-Length']) 
     140        except: 
     141            raise MultiPartParserError('Invalid Content-Length: %s' % headers.get('Content-Length')) 
     142 
     143        content_type = headers.get('Content-Type') 
     144 
     145        if not content_type or not content_type.startswith('multipart/'): 
     146            raise MultiPartParserError('Invalid Content-Type: %s' % content_type) 
     147             
     148        ctype, opts = self.parse_header(content_type) 
     149        boundary = opts.get('boundary') 
     150        from cgi import valid_boundary 
     151        if not boundary or not valid_boundary(boundary): 
     152            raise MultiPartParserError('Invalid boundary in multipart form: %s' % boundary) 
     153 
     154        progress_id = request.META['UPLOAD_PROGRESS_ID'] 
     155 
     156        if file_upload_dir and progress_id: 
     157            self._progress_filename = os.path.join(file_upload_dir, progress_id) 
     158        else: 
     159            self._progress_filename = None 
     160        self._boundary = '--' + boundary 
     161        self._input = input 
     162        self._size = content_length 
     163        self._received = 0 
     164        self._file_upload_dir = file_upload_dir 
     165        self._chunk_size = chunk_size 
     166        self._state = 'PREAMBLE' 
     167        self._partial = '' 
     168        self._post = MultiValueDict() 
     169        self._files = MultiValueDict() 
     170        self._request = request 
     171         
     172        if streaming_min_post_size is not None and content_length < streaming_min_post_size: 
     173            self._file_upload_dir = None # disable file streaming for small request 
     174 
     175        try: 
     176            # use mx fast string search if available 
     177            from mx.TextTools import FS 
     178            self._fs = FS(self._boundary) 
     179        except ImportError: 
     180            self._fs = None 
     181 
     182    def parse(self): 
     183        try: 
     184            self._parse() 
     185        finally: 
     186            if self._progress_filename: 
     187                self._request.file_progress = {'state': 'done'} 
     188                 
     189         
     190        return self._post, self._files 
     191 
     192    def _parse(self): 
     193        size = self._size 
     194 
     195        try: 
     196            while size > 0: 
     197                n = self._read(self._input, min(self._chunk_size, size)) 
     198                if not n: 
     199                    break 
     200                size -= n 
     201        except: 
     202            # consume any remaining data so we dont generate a "Connection Reset" error 
     203            size = self._size - self._received 
     204            while size > 0: 
     205                data = self._input.read(min(self._chunk_size, size)) 
     206                size -= len(data) 
     207            raise 
     208 
     209    def _find_boundary(self, data, start, stop): 
     210        """ 
     211        Find the next boundary and return the end of current part 
     212        and start of next part. 
     213        """ 
     214        if self._fs: 
     215            boundary = self._fs.find(data, start, stop) 
     216        else: 
     217            boundary = data.find(self._boundary, start, stop) 
     218        if boundary >= 0: 
     219            end = boundary 
     220            next = boundary + len(self._boundary) 
     221 
     222            # backup over CRLF 
     223            if end > 0 and data[end-1] == '\n': end -= 1 
     224            if end > 0 and data[end-1] == '\r': end -= 1 
     225            # skip over --CRLF 
     226            if next < stop and data[next] == '-': next += 1 
     227            if next < stop and data[next] == '-': next += 1 
     228            if next < stop and data[next] == '\r': next += 1 
     229            if next < stop and data[next] == '\n': next += 1 
     230 
     231            return True, end, next 
     232        else: 
     233            return False, stop, stop 
     234 
     235    class TemporaryFile(object): 
     236        "A temporary file that tries to delete itself when garbage collected." 
     237        def __init__(self, dir): 
     238            import tempfile 
     239            (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir) 
     240            self.file = os.fdopen(fd, 'w+b') 
     241            self.name = name 
     242 
     243        def __getattr__(self, name): 
     244            a = getattr(self.__dict__['file'], name) 
     245            if type(a) != type(0): 
     246                setattr(self, name, a) 
     247            return a 
     248 
     249        def __del__(self): 
     250            try: 
     251                os.unlink(self.name) 
     252            except OSError: 
     253                pass 
     254             
     255    class LazyContent(dict): 
     256        """ 
     257        A lazy FILES dictionary entry that reads the contents from 
     258        tmpfile only when referenced. 
     259        """ 
     260        def __init__(self, data): 
     261            dict.__init__(self, data) 
     262         
     263        def __getitem__(self, key): 
     264            if key == 'content' and not self.has_key(key): 
     265                self['tmpfile'].seek(0) 
     266                self['content'] = self['tmpfile'].read() 
     267            return dict.__getitem__(self, key) 
     268 
     269    def _read(self, input, size): 
     270        data = input.read(size) 
     271 
     272        if not data: 
     273            return 0 
     274 
     275        read_size = len(data) 
     276        self._received += read_size 
     277 
     278        if self._partial: 
     279            data = self._partial + data 
     280 
     281        start = 0 
     282        stop = len(data) 
     283         
     284        while start < stop: 
     285            boundary, end, next = self._find_boundary(data, start, stop) 
     286 
     287            if not boundary and read_size: 
     288                # make sure we dont treat a partial boundary (and its separators) as data 
     289                stop -= len(self._boundary) + 16 
     290                end = next = stop 
     291                if end <= start: 
     292                    break # need more data 
     293 
     294            if self._state == 'PREAMBLE': 
     295                # Preamble, just ignore it 
     296                self._state = 'HEADER' 
     297 
     298            elif self._state == 'HEADER': 
     299                # Beginning of header, look for end of header and parse it if found. 
     300 
     301                header_end = data.find('\r\n\r\n', start, stop) 
     302                if header_end == -1: 
     303                    break # need more data 
     304 
     305                header = data[start:header_end] 
     306 
     307                self._fieldname = None 
     308                self._filename = None 
     309                self._content_type = None 
     310 
     311                for line in header.split('\r\n'): 
     312                    ctype, opts = self.parse_header(line) 
     313                    if ctype == 'content-disposition: form-data': 
     314                        self._fieldname = opts.get('name') 
     315                        self._filename = opts.get('filename') 
     316                    elif ctype.startswith('content-type: '): 
     317                        self._content_type = ctype[14:] 
     318 
     319                if self._filename is not None: 
     320                    # cleanup filename from IE full paths: 
     321                    self._filename = self._filename[self._filename.rfind("\\")+1:].strip() 
     322 
     323                    if self._filename: # ignore files without filenames 
     324                        if self._file_upload_dir: 
     325                            try: 
     326                                self._file = self.TemporaryFile(dir=self._file_upload_dir) 
     327                            except: 
     328                                raise MultiPartParserError("Failed to create temporary file.") 
     329                        else: 
     330                            self._file = StringIO() 
     331                    else: 
     332                        self._file = None 
     333                    self._filesize = 0 
     334                    self._state = 'FILE' 
     335                else: 
     336                    self._field = StringIO() 
     337                    self._state = 'FIELD' 
     338                next = header_end + 4 
     339 
     340            elif self._state == 'FIELD': 
     341                # In a field, collect data until a boundary is found. 
     342 
     343                self._field.write(data[start:end]) 
     344                if boundary: 
     345                    if self._fieldname: 
     346                        self._post.appendlist(self._fieldname, self._field.getvalue()) 
     347                    self._field.close() 
     348                    self._state = 'HEADER' 
     349 
     350            elif self._state == 'FILE': 
     351                # In a file, collect data until a boundary is found. 
     352 
     353                if self._file: 
     354                    try: 
     355                        self._file.write(data[start:end]) 
     356                    except IOError, e: 
     357                        raise MultiPartParserError("Failed to write to temporary file.") 
     358                    self._filesize += end-start 
     359 
     360                    if self._progress_filename: 
     361                        self._request.file_progress = {'received': self._received, 
     362                                                       'size':     self._size, 
     363                                                       'state':    'uploading'} 
     364 
     365                if boundary: 
     366                    if self._file: 
     367                        if self._file_upload_dir: 
     368                            self._file.seek(0) 
     369                            file = self.LazyContent({ 
     370                                'filename': self._filename, 
     371                                'content-type':  self._content_type, 
     372                                # 'content': is read on demand 
     373                                'content-length': self._filesize, 
     374                                'tmpfilename': self._file.name, 
     375                                'tmpfile': self._file 
     376                            }) 
     377                        else: 
     378                            file = { 
     379                                'filename': self._filename, 
     380                                'content-type':  self._content_type, 
     381                                'content': self._file.getvalue(), 
     382                                'content-length': self._filesize 
     383                            } 
     384                            self._file.close() 
     385 
     386                        self._files.appendlist(self._fieldname, file) 
     387 
     388                    self._state = 'HEADER' 
     389 
     390            start = next 
     391                 
     392        self._partial = data[start:] 
     393 
     394        return read_size 
     395 
     396    def parse_header(self, line): 
     397        from cgi import parse_header 
     398        return parse_header(line) 
     399 
    76400class QueryDict(MultiValueDict): 
    77401    """A specialized MultiValueDict that takes a query string when initialized. 
    78402    This is immutable unless you create a copy of it.""" 
     
    306630    if not host: 
    307631        host = request.META.get('HTTP_HOST', '') 
    308632    return host 
     633 
  • 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/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  
    638638        setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self)) 
    639639        setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self)) 
    640640        setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self)) 
    641         setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save)) 
     641        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save)) 
    642642        dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls) 
    643643 
    644644    def delete_file(self, instance): 
     
    661661        if new_data.get(upload_field_name, False): 
    662662            func = getattr(new_object, 'save_%s_file' % self.name) 
    663663            if rel: 
    664                 func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"], save) 
     664                func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0], save) 
    665665            else: 
    666                 func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"], save) 
     666                func(new_data[upload_field_name]["filename"], new_data[upload_field_name], save) 
    667667 
    668668    def get_directory_name(self): 
    669669        return os.path.normpath(datetime.datetime.now().strftime(self.upload_to)) 
  • 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/core/handlers/wsgi.py

    old new  
    7575        self.environ = environ 
    7676        self.path = environ['PATH_INFO'] 
    7777        self.META = environ 
     78        self.META['UPLOAD_PROGRESS_ID'] = self._get_file_progress_id() 
    7879        self.method = environ['REQUEST_METHOD'].upper() 
    7980 
    8081    def __repr__(self): 
     
    111112            if self.environ.get('CONTENT_TYPE', '').startswith('multipart'): 
    112113                header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')]) 
    113114                header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '') 
    114                 self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data) 
     115                header_dict['Content-Length'] = self.environ.get('CONTENT_LENGTH', '') 
     116                header_dict['X-Progress-ID'] = self.environ.get('HTTP_X_PROGRESS_ID', '') 
     117                try: 
     118                    self._post, self._files = http.parse_file_upload(header_dict, self.environ['wsgi.input'], self) 
     119                except: 
     120                    self._post, self._files = {}, {} # make sure we dont read the input stream again 
     121                    raise 
     122                self._raw_post_data = None # raw data is not available for streamed multipart messages 
    115123            else: 
    116124                self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict() 
    117125        else: 
     
    167175            buf.close() 
    168176            return self._raw_post_data 
    169177 
     178    def _get_file_progress_id(self): 
     179        """ 
     180        Returns the Progress ID of the request, 
     181        usually provided if there is a file upload 
     182        going on. 
     183        Returns ``None`` if no progress ID is specified. 
     184        """ 
     185        return self._get_file_progress_from_args(self.environ, 
     186                                                 self.GET, 
     187                                                 self.environ.get('QUERY_STRING', '')) 
     188 
    170189    GET = property(_get_get, _set_get) 
    171190    POST = property(_get_post, _set_post) 
    172191    COOKIES = property(_get_cookies, _set_cookies) 
  • django/core/handlers/base.py

    old new  
    55 
    66class BaseHandler(object): 
    77    def __init__(self): 
    8         self._request_middleware = self._view_middleware = self._response_middleware = self._exception_middleware = None 
     8        self._upload_middleware = self._request_middleware = self._view_middleware = self._response_middleware = self._exception_middleware = None 
    99 
    1010    def load_middleware(self): 
    1111        """ 
     
    1919        self._view_middleware = [] 
    2020        self._response_middleware = [] 
    2121        self._exception_middleware = [] 
     22        self._upload_middleware = [] 
    2223        for middleware_path in settings.MIDDLEWARE_CLASSES: 
    2324            try: 
    2425                dot = middleware_path.rindex('.') 
     
    4748                self._response_middleware.insert(0, mw_instance.process_response) 
    4849            if hasattr(mw_instance, 'process_exception'): 
    4950                self._exception_middleware.insert(0, mw_instance.process_exception) 
     51            if hasattr(mw_instance, 'process_upload'): 
     52                self._upload_middleware.append(mw_instance.process_upload) 
    5053 
     54    def file_progress_descriptor(self, request): 
     55        """ 
     56        Returns a descriptor that manages the file_progress 
     57        """         
     58        for mw_call in self._upload_middleware: 
     59            result = mw_call(http.MultiPartParserError) 
     60            if result != None: 
     61                return result 
     62        return http.DefaultFileProgressDescriptor(http.MultiPartParserError) 
     63         
    5164    def get_response(self, request): 
    5265        "Returns an HttpResponse object for the given HttpRequest" 
    5366        from django.core import exceptions, urlresolvers 
    5467        from django.core.mail import mail_admins 
    5568        from django.conf import settings 
    5669 
     70        # Add file_progress descriptor 
     71        request._file_progress = self.file_progress_descriptor(request) 
     72 
    5773        # Apply request middleware 
    5874        for middleware_method in self._request_middleware: 
    5975            response = middleware_method(request) 
  • 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) 
  • django/utils/file.py

    old new  
     1import os 
     2 
     3try: 
     4    import shutils 
     5    file_move = shutils.move 
     6except: 
     7    file_move = os.rename 
     8 
     9def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64): 
     10    """ 
     11    Moves a file from one location to another in the safest way possible. 
     12     
     13    First, it tries using shutils.move, which is OS-dependent but doesn't 
     14    break with change of filesystems. Then it tries os.rename, which will 
     15    break if it encounters a change in filesystems. Lastly, it streams 
     16    it manually from one file to another in python. 
     17    """ 
     18     
     19    try: 
     20        file_move(old_file_name, new_file_name) 
     21        return 
     22    except: 
     23        pass 
     24     
     25    new_file = open(new_file_name, 'wb') 
     26    old_file = open(old_file_name, 'rb') 
     27    current_chunk = None 
     28     
     29    while current_chunk != '': 
     30        current_chunk = old_file.read(chunk_size) 
     31        new_file.write(current_chunk) 
     32         
     33    new_file.close() 
     34    old_file.close() 
     35 
     36    os.remove(old_file_name) 
  • django/contrib/admin/urls.py

    old new  
    1010    ('^$', 'django.contrib.admin.views.main.index'), 
    1111    ('^r/(\d+)/(.*)/$', 'django.views.defaults.shortcut'), 
    1212    ('^jsi18n/$', i18n_view, {'packages': 'django.conf'}), 
     13    ('^upload_progress/$', 'django.contrib.admin.views.main.upload_progress'), 
    1314    ('^logout/$', 'django.contrib.auth.views.logout'), 
    1415    ('^password_change/$', 'django.contrib.auth.views.password_change'), 
    1516    ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'), 
  • django/contrib/admin/views/main.py

    old new  
    8181def get_javascript_imports(opts, auto_populated_fields, field_sets): 
    8282# Put in any necessary JavaScript imports. 
    8383    js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] 
     84    if opts.has_field_type(models.FileField): 
     85        js.append('js/UploadProgress.js') 
    8486    if auto_populated_fields: 
    8587        js.append('js/urlify.js') 
    8688    if opts.has_field_type(models.DateTimeField) or opts.has_field_type(models.TimeField) or opts.has_field_type(models.DateField): 
     
    777779                               'admin/%s/change_list.html' % app_label, 
    778780                               'admin/change_list.html'], context_instance=c) 
    779781change_list = staff_member_required(never_cache(change_list)) 
     782 
     783def upload_progress(request): 
     784    """ 
     785    Given this request, returns a JSON 
     786    object that has information on a file upload progress. 
     787    If there is no file upload in progress, returns an 
     788    empty dictionary, '{}'. 
     789    """ 
     790    from django.utils import simplejson 
     791 
     792    content = simplejson.dumps(request.file_progress) 
     793 
     794    return HttpResponse(content=content, mimetype='text/plain') 
  • 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