Ticket #13960: filetransfers.2.diff

File filetransfers.2.diff, 17.3 KB (added by Waldemar Kornewald, 14 years ago)

patch against trunk

  • django/conf/global_settings.py

    diff -r 709838c4a6bb django/conf/global_settings.py
    a b  
    257257# Default file storage mechanism that holds media.
    258258DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
    259259
     260FILE_BACKENDS = ()
     261
    260262# Absolute path to the directory that holds media.
    261263# Example: "/home/media/media.lawrence.com/"
    262264MEDIA_ROOT = ''
  • new file django/core/files/transfers.py

    diff -r 709838c4a6bb django/core/files/transfers.py
    - +  
     1from django.conf import settings
     2from django.core.exceptions import ImproperlyConfigured
     3from django.core.files.storage import default_storage
     4from django.http import HttpResponse
     5from django.utils.encoding import smart_str
     6from django.utils.importlib import import_module
     7import mimetypes
     8
     9class BaseFileBackend(object):
     10    def __init__(self, model, fields):
     11        self.model = model
     12        if not isinstance(fields, (tuple, list)):
     13            fields = (fields,)
     14        self.fields = []
     15        for field in fields:
     16            if isinstance(field, basestring):
     17                field = model._meta.get_field(field)
     18            self.fields.append(field)
     19
     20    # Add a convenience accessor for serve, download_url,
     21    # and get_storage_backend which only act on a single FileField
     22    def _get_field(self):
     23        if len(self.fields) != 1:
     24            raise ValueError('Expected only a single FileField, got %d' % len(self.fields))
     25        return self.fields[0]
     26    field = property(_get_field)
     27
     28    def prepare_upload(self, request, url):
     29        return None
     30
     31    def serve(self, request, file, save_as=False, content_type=None):
     32        return None
     33
     34    def download_url(self, file):
     35        return None
     36
     37    def get_storage_backend(self):
     38        return None
     39
     40# Public API
     41def prepare_upload(model, fields, request, url=None):
     42    if not url:
     43        url = request.get_full_path()
     44    for backend in _get_backends(model, fields):
     45        result = backend.prepare_upload(request, url)
     46        if result is not None:
     47            return result
     48
     49    # By default we simply return the URL unmodified
     50    return url, {}
     51
     52def serve_file(model, fields, request, file, save_as=False, content_type=None):
     53    filename = file.name.rsplit('/')[-1]
     54    if save_as is True:
     55        save_as = filename
     56    if not content_type:
     57        content_type = mimetypes.guess_type(filename)[0]
     58    for backend in _get_backends(model, fields):
     59        result = backend.serve(request, file, save_as=save_as, content_type=content_type)
     60        if result is not None:
     61            return result
     62
     63    return _default_serve_file(file, save_as, content_type)
     64
     65def download_url(model, fields, file):
     66    for backend in _get_backends(model, fields):
     67        result = backend.download_url(file)
     68        if result is not None:
     69            return result
     70
     71    # By default we use MEDIA_URL
     72    return settings.MEDIA_URL + file.name
     73
     74def get_storage_backend(model, fields):
     75    for backend in _get_backends(model, fields):
     76        result = backend.get_storage_backend()
     77        if result is not None:
     78            return result
     79
     80    # By default there is no public download URL
     81    return default_storage
     82
     83# Internal utilities
     84class ChunkedFile(object):
     85    def __init__(self, file):
     86        self.file = file
     87
     88    def __iter__(self):
     89        return self.file.chunks()
     90
     91def _default_serve_file(file, save_as, content_type):   
     92    """
     93    Serves the file in chunks for efficiency reasons.
     94   
     95    The transfer still goes through Django itself, so it's much worse than
     96    using the web server, but at least it works with all configurations.
     97    """
     98    response = HttpResponse(ChunkedFile(file), content_type=content_type)
     99    if save_as:
     100        response['Content-Disposition'] = smart_str(u'attachment; filename=%s' % save_as)
     101    if file.size is not None:
     102        response['Content-Length'] = file.size
     103    return response
     104
     105def _get_backends(model, fields):
     106    return (_load_backend(name, model, fields)
     107            for name in settings.FILE_BACKENDS)
     108
     109def _load_backend(path, model, fields):
     110    module_name, attr_name = path.rsplit('.', 1)
     111    try:
     112        mod = import_module(module_name)
     113    except ImportError, e:
     114        raise ImproperlyConfigured('Error importing file backend module %s: "%s"' % (module_name, e))
     115    except ValueError, e:
     116        raise ImproperlyConfigured('Error importing file backend module. Is FILE_BACKENDS a correctly defined list or tuple?')
     117    try:
     118        backend = getattr(mod, attr_name)
     119    except AttributeError:
     120        raise ImproperlyConfigured('Module "%s" does not define a "%s" file backend' % (module_name, attr_name))
     121    return backend(model, fields)
  • django/core/files/uploadhandler.py

    diff -r 709838c4a6bb django/core/files/uploadhandler.py
    a b  
    8484        """
    8585        pass
    8686
    87     def new_file(self, field_name, file_name, content_type, content_length, charset=None):
     87    def new_file(self, field_name, file_name, content_type, content_length,  charset=None, content_type_extra=None):
    8888        """
    8989        Signal that a new file has been started.
    9090
     
    9696        self.content_type = content_type
    9797        self.content_length = content_length
    9898        self.charset = charset
     99        if content_type_extra is None:
     100            content_type_extra = {}
     101        self.content_type_extra = content_type_extra
    99102
    100103    def receive_data_chunk(self, raw_data, start):
    101104        """
  • django/db/models/fields/files.py

    diff -r 709838c4a6bb django/db/models/fields/files.py
    a b  
    66from django.conf import settings
    77from django.db.models.fields import Field
    88from django.core.files.base import File, ContentFile
    9 from django.core.files.storage import default_storage
    109from django.core.files.images import ImageFile, get_image_dimensions
     10from django.core.files.transfers import serve_file, download_url, get_storage_backend
    1111from django.core.files.uploadedfile import UploadedFile
    1212from django.utils.functional import curry
    1313from django.db.models import signals
     
    139139        # be restored later, by FileDescriptor below.
    140140        return {'name': self.name, 'closed': False, '_committed': True, '_file': None}
    141141
     142    def serve(self, request, save_as=False, content_type=None):
     143        self._require_file()
     144        return serve_file(self.field.model, self.field, request, self, save_as=save_as, content_type=content_type)
     145
     146    def download_url(self):
     147        self._require_file()
     148        return download_url(self.field.model, self.field, self)
     149
    142150class FileDescriptor(object):
    143151    """
    144152    The descriptor for the file attribute on the model instance. Returns a
     
    224232            if arg in kwargs:
    225233                raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
    226234
    227         self.storage = storage or default_storage
     235        self.storage = storage
    228236        self.upload_to = upload_to
    229237        if callable(upload_to):
    230238            self.generate_filename = upload_to
     
    257265
    258266    def contribute_to_class(self, cls, name):
    259267        super(FileField, self).contribute_to_class(cls, name)
     268        if self.storage is None:
     269            self.storage = get_storage_backend(self.model, self)
    260270        setattr(cls, self.name, self.descriptor_class(self))
    261271        signals.post_delete.connect(self.delete_file, sender=cls)
    262272
  • django/forms/models.py

    diff -r 709838c4a6bb django/forms/models.py
    a b  
    33and database field objects.
    44"""
    55
     6from django.core.files.transfers import prepare_upload
    67from django.db import connections
    78from django.utils.encoding import smart_unicode, force_unicode
    89from django.utils.datastructures import SortedDict
     
    231232    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
    232233                 initial=None, error_class=ErrorList, label_suffix=':',
    233234                 empty_permitted=False, instance=None):
     235        self._file_upload_data = {}
     236
    234237        opts = self._meta
    235238        if instance is None:
    236239            if opts.model is None:
     
    372375
    373376    save.alters_data = True
    374377
     378    def prepare_upload(self, request, url=None):
     379        from django.db import models
     380        for name in self._file_upload_data:
     381            del self.fields[name]
     382
     383        file_fields = []
     384        for name in self.fields:
     385            try:
     386                field = self._meta.model._meta.get_field(name)
     387                if isinstance(field, models.FileField):
     388                    file_fields.append(field)
     389            except models.FieldDoesNotExist:
     390                pass
     391
     392        result = prepare_upload(self._meta.model, file_fields, request, url=url)
     393        self.upload_url, self._file_upload_data = result
     394
     395        for name, value in self._file_upload_data.items():
     396            self.fields[name] = Field(initial=value, required=False, widget=HiddenInput)
     397
    375398class ModelForm(BaseModelForm):
    376399    __metaclass__ = ModelFormMetaclass
    377400
  • django/http/multipartparser.py

    diff -r 709838c4a6bb django/http/multipartparser.py
    a b  
    169169                    file_name = self.IE_sanitize(unescape_entities(file_name))
    170170
    171171                    content_type = meta_data.get('content-type', ('',))[0].strip()
     172                    content_type_extra = meta_data.get('content-type', (0,{}))[1]
    172173                    try:
    173                         charset = meta_data.get('content-type', (0,{}))[1].get('charset', None)
     174                        charset = content_type_extra.get('charset', None)
    174175                    except:
    175176                        charset = None
    176177
     
    185186                            try:
    186187                                handler.new_file(field_name, file_name,
    187188                                                 content_type, content_length,
    188                                                  charset)
     189                                                 charset, content_type_extra)
    189190                            except StopFutureHandlers:
    190191                                break
    191192
  • new file tests/regressiontests/file_uploads/backends.py

    diff -r 709838c4a6bb tests/regressiontests/file_uploads/backends.py
    - +  
     1from django.conf import settings
     2from django.core.files.transfers import BaseFileBackend
     3from django.http import HttpResponseRedirect
     4import models
     5
     6class TestFileBackend(BaseFileBackend):
     7    def prepare_upload(self, request, url, *args, **kwargs):
     8        if self.model is models.FileModel:
     9            return '/redirect' + url, {'x-extra': 'abcd', 'x-data': 'efg'}
     10
     11    def serve(self, request, file, *args, **kwargs):
     12        if self.model is models.FileModel and not file.name.endswith('pass'):
     13            return HttpResponseRedirect('/download/')
     14
     15    def download_url(self, file):
     16        if self.model is models.FileModel and not file.name.endswith('pass'):
     17            return '/public/'
     18
     19    def get_storage_backend(self, *args, **kwargs):
     20        if self.model.__module__ == models.__name__ and \
     21                self.model.__name__ == 'FileModel':
     22            return models.temp_storage
  • new file tests/regressiontests/file_uploads/forms.py

    diff -r 709838c4a6bb tests/regressiontests/file_uploads/forms.py
    - +  
     1from django import forms
     2from models import FileModel
     3
     4class FileForm(forms.ModelForm):
     5    class Meta:
     6        model = FileModel
  • tests/regressiontests/file_uploads/models.py

    diff -r 709838c4a6bb tests/regressiontests/file_uploads/models.py
    a b  
    11import tempfile
    22import os
     3from django.conf import settings
    34from django.db import models
    45from django.core.files.storage import FileSystemStorage
    56
    67temp_storage = FileSystemStorage(tempfile.mkdtemp())
    78UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
     9settings.FILE_BACKENDS = ('%s.backends.TestFileBackend' % __name__.rsplit('.', 1)[0],) + \
     10    settings.FILE_BACKENDS
    811
    912class FileModel(models.Model):
    10     testfile = models.FileField(storage=temp_storage, upload_to='test_upload')
     13    testfile = models.FileField(upload_to='test_upload')
  • tests/regressiontests/file_uploads/tests.py

    diff -r 709838c4a6bb tests/regressiontests/file_uploads/tests.py
    a b  
    55import unittest
    66from StringIO import StringIO
    77
     8from django.conf import settings
    89from django.core.files import temp as tempfile
    910from django.core.files.uploadedfile import SimpleUploadedFile
    1011from django.test import TestCase, client
     
    1718
    1819UNICODE_FILENAME = u'test-0123456789_中文_Orléans.jpg'
    1920
     21class FileBackendTests(TestCase):
     22    def setUp(self):
     23        if not os.path.isdir(temp_storage.location):
     24            os.makedirs(temp_storage.location)
     25        if os.path.isdir(UPLOAD_TO):
     26            shutil.rmtree(UPLOAD_TO)
     27
     28    def tearDown(self):
     29        shutil.rmtree(temp_storage.location)
     30
     31    def test_prepare_upload(self):
     32        response = self.client.get('/file_uploads/prepare_upload/')
     33        self.assertEqual(response.status_code, 200)
     34        data = simplejson.loads(response.content)
     35        self.assertEqual(data, [
     36            '/redirect/file_uploads/prepare_upload/', [
     37                '<input type="file" name="testfile" id="id_testfile" />',
     38                '<input type="hidden" name="x-data" value="efg" id="id_x-data" />',
     39                '<input type="hidden" name="x-extra" value="abcd" id="id_x-extra" />',
     40            ]
     41        ])
     42
     43    def test_serve_file(self):
     44        obj = FileModel()
     45        obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
     46        obj.save()
     47        response = self.client.get('/file_uploads/serve/')
     48        self.assertEqual(response.status_code, 302)
     49        self.assertEqual(response['Location'], 'http://testserver/download/')
     50
     51    def test_serve_file_fallback(self):
     52        obj = FileModel()
     53        obj.testfile.save('foo.pass', SimpleUploadedFile('foo.pass', 'x'))
     54        obj.save()
     55        response = self.client.get('/file_uploads/serve/')
     56        self.assertEqual(response.status_code, 200)
     57
     58    def test_download_url(self):
     59        obj = FileModel()
     60        obj.testfile.save('foo2.txt', SimpleUploadedFile('foo2.txt', 'x'))
     61        obj.save()
     62        url = obj.testfile.download_url()
     63        self.assertEqual(url, '/public/')
     64
     65    def test_download_url_fallback(self):
     66        obj = FileModel()
     67        obj.testfile.save('foo2.pass', SimpleUploadedFile('foo2.pass', 'x'))
     68        obj.save()
     69        url = obj.testfile.download_url()
     70        self.assertEqual(url, settings.MEDIA_URL + obj.testfile.name)
     71
    2072class FileUploadTests(TestCase):
    2173    def test_simple_upload(self):
    2274        post_data = {
     
    122174        response = self.client.request(**r)
    123175
    124176        # The filenames should have been sanitized by the time it got to the view.
    125         recieved = simplejson.loads(response.content)
     177        received = simplejson.loads(response.content)
    126178        for i, name in enumerate(scary_file_names):
    127             got = recieved["file%s" % i]
     179            got = received["file%s" % i]
    128180            self.assertEqual(got, "hax0rd.txt")
    129181
    130182    def test_filename_overflow(self):
  • tests/regressiontests/file_uploads/urls.py

    diff -r 709838c4a6bb tests/regressiontests/file_uploads/urls.py
    a b  
    1010    (r'^quota/broken/$',    views.file_upload_quota_broken),
    1111    (r'^getlist_count/$',   views.file_upload_getlist_count),
    1212    (r'^upload_errors/$',   views.file_upload_errors),
     13    (r'^prepare_upload/$',  views.file_prepare_upload),
     14    (r'^serve/$', views.file_serve),
    1315)
  • tests/regressiontests/file_uploads/views.py

    diff -r 709838c4a6bb tests/regressiontests/file_uploads/views.py
    a b  
    22from django.core.files.uploadedfile import UploadedFile
    33from django.http import HttpResponse, HttpResponseServerError
    44from django.utils import simplejson
     5from forms import FileForm
    56from models import FileModel, UPLOAD_TO
    67from uploadhandler import QuotaUploadHandler, ErroringUploadHandler
    78from django.utils.hashcompat import sha_constructor
     
    112113def file_upload_errors(request):
    113114    request.upload_handlers.insert(0, ErroringUploadHandler())
    114115    return file_upload_echo(request)
     116
     117def file_prepare_upload(request):
     118    form = FileForm()
     119    form.prepare_upload(request)
     120    return HttpResponse(simplejson.dumps([form.upload_url, list(map(unicode, form))]))
     121
     122def file_serve(request):
     123    return FileModel.objects.all()[0].testfile.serve(request)
Back to Top