Ticket #13960: filetransfers.diff

File filetransfers.diff, 18.5 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 hasattr(fields, '__iter__'):
     13            fields = (fields,)
     14        self.fields = fields
     15
     16    # Add a convenience accessor for serve, download_url,
     17    # and get_storage_backend which only act on a single FileField
     18    def _get_field(self):
     19        if len(self.fields) != 1:
     20            raise ValueError('Expected only a single FileField, got %d' % len(self.fields))
     21        return self.fields[0]
     22    field = property(_get_field)
     23
     24    def prepare_upload(self, request, url):
     25        return None
     26
     27    def serve(self, request, file, save_as=False, content_type=None):
     28        return None
     29
     30    def download_url(self, file):
     31        return None
     32
     33    def get_storage_backend(self):
     34        return None
     35
     36# Public API
     37def prepare_upload(model, fields, request, url=None):
     38    if not url:
     39        url = request.get_full_path()
     40    for backend in _get_backends(model, fields):
     41        result = backend.prepare_upload(request, url)
     42        if result is not None:
     43            return result
     44
     45    # By default we simply return the URL unmodified
     46    return url, {}
     47
     48def serve_file(model, fields, request, file, save_as=False, content_type=None):
     49    filename = file.name.rsplit('/')[-1]
     50    if save_as is True:
     51        save_as = filename
     52    if not content_type:
     53        content_type = mimetypes.guess_type(filename)[0]
     54    for backend in _get_backends(model, fields):
     55        result = backend.serve(request, file, save_as=save_as, content_type=content_type)
     56        if result is not None:
     57            return result
     58
     59    return _default_serve_file(file, save_as, content_type)
     60
     61def download_url(model, fields, file):
     62    for backend in _get_backends(model, fields):
     63        result = backend.download_url(file)
     64        if result is not None:
     65            return result
     66
     67    # By default we use MEDIA_URL
     68    return settings.MEDIA_URL + file.name
     69
     70def get_storage_backend(model, fields):
     71    for backend in _get_backends(model, fields):
     72        result = backend.get_storage_backend()
     73        if result is not None:
     74            return result
     75
     76    # By default there is no public download URL
     77    return default_storage
     78
     79# Internal utilities
     80class ChunkedFile(object):
     81    def __init__(self, file):
     82        self.file = file
     83
     84    def __iter__(self):
     85        return self.file.chunks()
     86
     87def _default_serve_file(file, save_as, content_type):   
     88    """
     89    Serves the file in chunks for efficiency reasons.
     90   
     91    The transfer still goes through Django itself, so it's much worse than
     92    using the web server, but at least it works with all configurations.
     93    """
     94    response = HttpResponse(ChunkedFile(file), content_type=content_type)
     95    if save_as:
     96        response['Content-Disposition'] = smart_str(u'attachment; filename=%s' % save_as)
     97    if file.size is not None:
     98        response['Content-Length'] = file.size
     99    return response
     100
     101def _get_backends(model, fields):
     102    return (_load_backend(name, model, fields)
     103            for name in settings.FILE_BACKENDS)
     104
     105def _load_backend(path, model, fields):
     106    module_name, attr_name = path.rsplit('.', 1)
     107    try:
     108        mod = import_module(module_name)
     109    except ImportError, e:
     110        raise ImproperlyConfigured('Error importing file backend module %s: "%s"' % (module_name, e))
     111    except ValueError, e:
     112        raise ImproperlyConfigured('Error importing file backend module. Is FILE_BACKENDS a correctly defined list or tuple?')
     113    try:
     114        backend = getattr(mod, attr_name)
     115    except AttributeError:
     116        raise ImproperlyConfigured('Module "%s" does not define a "%s" file backend' % (module_name, attr_name))
     117    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 prepare_upload, 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
     
    156164
    157165    def __get__(self, instance=None, owner=None):
    158166        if instance is None:
    159             raise AttributeError(
    160                 "The '%s' attribute can only be accessed from %s instances."
    161                 % (self.field.name, owner.__name__))
     167            return self.field
    162168
    163169        # This is slightly complicated, so worth an explanation.
    164170        # instance.file`needs to ultimately return some instance of `File`,
     
    224230            if arg in kwargs:
    225231                raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
    226232
    227         self.storage = storage or default_storage
     233        self.storage = storage
    228234        self.upload_to = upload_to
    229235        if callable(upload_to):
    230236            self.generate_filename = upload_to
     
    257263
    258264    def contribute_to_class(self, cls, name):
    259265        super(FileField, self).contribute_to_class(cls, name)
     266        if self.storage is None:
     267            self.storage = get_storage_backend(self.model, self)
    260268        setattr(cls, self.name, self.descriptor_class(self))
    261269        signals.post_delete.connect(self.delete_file, sender=cls)
    262270
     
    297305        defaults.update(kwargs)
    298306        return super(FileField, self).formfield(**defaults)
    299307
     308    def prepare_upload(self, request, url=None):
     309        return prepare_upload(self.model, self, request, url=url)
     310
    300311class ImageFileDescriptor(FileDescriptor):
    301312    """
    302313    Just like the FileDescriptor, but for ImageFields. The only difference is
  • 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
  • tests/modeltests/files/models.py

    diff -r 709838c4a6bb tests/modeltests/files/models.py
    a b  
    3434__test__ = {'API_TESTS':"""
    3535# Attempting to access a FileField from the class raises a descriptive error
    3636>>> Storage.normal
    37 Traceback (most recent call last):
    38 ...
    39 AttributeError: The 'normal' attribute can only be accessed from Storage instances.
     37<django.db.models.fields.files.FileField object at ...>
    4038
    4139# An object without a file has limited functionality.
    4240
  • 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.__module__ == models.__name__:
     9            return '/redirect' + url, {'x-extra': 'abcd', 'x-data': 'efg'}
     10
     11    def serve(self, request, file, *args, **kwargs):
     12        if self.model.__module__ == models.__name__ and not file.name.endswith('pass'):
     13            return HttpResponseRedirect('/download/')
     14
     15    def download_url(self, file):
     16        if self.model.__module__ == models.__name__ 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__:
     21            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