Ticket #13960: filetransfers.3.diff

File filetransfers.3.diff, 20.8 KB (added by Waldemar Kornewald, 14 years ago)

patch with RequestForm, RequestModelForm. now independent of models (uses string ID instead of model and field)

  • django/conf/global_settings.py

    diff -r 9a6a6b7a64e8 django/conf/global_settings.py
    a b  
    258258# Default file storage mechanism that holds media.
    259259DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
    260260
     261# High-level transfer backends
     262FILE_TRANSFER_BACKENDS = (
     263    'django.core.files.transfers.DefaultFileBackend',
     264)
     265
     266# The default transfer backend's configuration mapping transfer_id to
     267# backend name
     268DEFAULT_FILE_TRANSFER_BACKENDS = {}
     269
    261270# Absolute path to the directory that holds media.
    262271# Example: "/home/media/media.lawrence.com/"
    263272MEDIA_ROOT = ''
  • new file django/core/files/transfers.py

    diff -r 9a6a6b7a64e8 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, transfer_id):
     11        self.transfer_id = transfer_id
     12
     13    def prepare_upload(self, request, url):
     14        return None
     15
     16    def serve(self, request, file, save_as=False, content_type=None):
     17        return None
     18
     19    def download_url(self, file):
     20        return None
     21
     22    def get_storage_backend(self):
     23        return None
     24
     25class DefaultFileBackend(BaseFileBackend):
     26    def _get_backend_path(self):
     27        if self.transfer_id in settings.DEFAULT_FILE_TRANSFER_BACKENDS:
     28            return settings.DEFAULT_FILE_TRANSFER_BACKENDS[self.transfer_id]
     29        parts = self.transfer_id.split('.')[:-1]
     30        while parts:
     31            name = '.'.join(parts) + '.*'
     32            if name in settings.DEFAULT_FILE_TRANSFER_BACKENDS:
     33                return settings.DEFAULT_FILE_TRANSFER_BACKENDS[name]
     34            del parts[-1]
     35        return None
     36
     37    def _get_backend(self):
     38        path = self._get_backend_path()
     39        if path:
     40            return load_backend(path, self.transfer_id)
     41        return None
     42
     43    def prepare_upload(self, *args, **kwargs):
     44        backend = self._get_backend()
     45        if backend:
     46            return backend.prepare_upload(*args, **kwargs)
     47        return None
     48
     49    def serve(self, *args, **kwargs):
     50        backend = self._get_backend()
     51        if backend:
     52            return backend.serve(*args, **kwargs)
     53        return None
     54
     55    def download_url(self, *args, **kwargs):
     56        backend = self._get_backend()
     57        if backend:
     58            return backend.download_url(*args, **kwargs)
     59        return None
     60
     61    def get_storage_backend(self, *args, **kwargs):
     62        backend = self._get_backend()
     63        if backend:
     64            return backend.get_storage_backend(*args, **kwargs)
     65        return None
     66
     67# Public API
     68def prepare_upload(transfer_id, request, url=None):
     69    if not url:
     70        url = request.get_full_path()
     71    for backend in _get_backends(transfer_id):
     72        result = backend.prepare_upload(request, url)
     73        if result is not None:
     74            return result
     75
     76    # By default we simply return the URL unmodified
     77    return url, {}
     78
     79def serve_file(transfer_id, request, file, save_as=False, content_type=None):
     80    filename = file.name.rsplit('/')[-1]
     81    if save_as is True:
     82        save_as = filename
     83    if not content_type:
     84        content_type = mimetypes.guess_type(filename)[0]
     85    for backend in _get_backends(transfer_id):
     86        result = backend.serve(request, file, save_as=save_as, content_type=content_type)
     87        if result is not None:
     88            return result
     89
     90    return _default_serve_file(file, save_as, content_type)
     91
     92def download_url(transfer_id, file):
     93    for backend in _get_backends(transfer_id):
     94        result = backend.download_url(file)
     95        if result is not None:
     96            return result
     97
     98    # By default we use MEDIA_URL
     99    return settings.MEDIA_URL + file.name
     100
     101def get_storage_backend(transfer_id):
     102    for backend in _get_backends(transfer_id):
     103        result = backend.get_storage_backend()
     104        if result is not None:
     105            return result
     106
     107    # By default there is no public download URL
     108    return default_storage
     109
     110# Internal utilities
     111class ChunkedFile(object):
     112    def __init__(self, file):
     113        self.file = file
     114
     115    def __iter__(self):
     116        return self.file.chunks()
     117
     118def _default_serve_file(file, save_as, content_type):   
     119    """
     120    Serves the file in chunks for efficiency reasons.
     121   
     122    The transfer still goes through Django itself, so it's much worse than
     123    using the web server, but at least it works with all configurations.
     124    """
     125    response = HttpResponse(ChunkedFile(file), content_type=content_type)
     126    if save_as:
     127        response['Content-Disposition'] = smart_str(u'attachment; filename=%s' % save_as)
     128    if file.size is not None:
     129        response['Content-Length'] = file.size
     130    return response
     131
     132def _get_backends(*args, **kwargs):
     133    return (load_backend(name, *args, **kwargs)
     134            for name in settings.FILE_TRANSFER_BACKENDS)
     135
     136def load_backend(path, *args, **kwargs):
     137    module_name, attr_name = path.rsplit('.', 1)
     138    try:
     139        mod = import_module(module_name)
     140    except ImportError, e:
     141        raise ImproperlyConfigured('Error importing file backend module %s: "%s"' % (module_name, e))
     142    except ValueError, e:
     143        raise ImproperlyConfigured('Error importing file backend module. Is FILE_TRANSFER_BACKENDS a correctly defined list or tuple?')
     144    try:
     145        backend = getattr(mod, attr_name)
     146    except AttributeError:
     147        raise ImproperlyConfigured('Module "%s" does not define a "%s" file backend' % (module_name, attr_name))
     148    return backend(*args, **kwargs)
  • django/core/files/uploadhandler.py

    diff -r 9a6a6b7a64e8 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 9a6a6b7a64e8 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.transfer_id, 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.transfer_id, self)
     149
    142150class FileDescriptor(object):
    143151    """
    144152    The descriptor for the file attribute on the model instance. Returns a
     
    219227
    220228    description = ugettext_lazy("File path")
    221229
    222     def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs):
     230    def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, transfer_id=None, **kwargs):
    223231        for arg in ('primary_key', 'unique'):
    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.transfer_id = transfer_id
     236        self.storage = storage
    228237        self.upload_to = upload_to
    229238        if callable(upload_to):
    230239            self.generate_filename = upload_to
     
    257266
    258267    def contribute_to_class(self, cls, name):
    259268        super(FileField, self).contribute_to_class(cls, name)
     269        if self.transfer_id is None:
     270            self.transfer_id = '%s.%s.%s' % (cls.__module__,
     271                                             cls.__name__,
     272                                             self.name)
     273        if self.storage is None:
     274            self.storage = get_storage_backend(self.transfer_id)
    260275        setattr(cls, self.name, self.descriptor_class(self))
    261276        signals.post_delete.connect(self.delete_file, sender=cls)
    262277
  • django/forms/forms.py

    diff -r 9a6a6b7a64e8 django/forms/forms.py
    a b  
    33"""
    44
    55from django.core.exceptions import ValidationError
     6from django.core.files.transfers import prepare_upload
    67from django.utils.copycompat import deepcopy
    78from django.utils.datastructures import SortedDict
    89from django.utils.html import conditional_escape
     
    1011from django.utils.safestring import mark_safe
    1112
    1213from fields import Field, FileField
    13 from widgets import Media, media_property, TextInput, Textarea
     14from widgets import Media, media_property, TextInput, Textarea, HiddenInput
    1415from util import flatatt, ErrorDict, ErrorList
    1516
    16 __all__ = ('BaseForm', 'Form')
     17__all__ = ('BaseForm', 'Form', 'RequestForm')
    1718
    1819NON_FIELD_ERRORS = '__all__'
    1920
     
    386387    # BaseForm itself has no way of designating self.fields.
    387388    __metaclass__ = DeclarativeFieldsMetaclass
    388389
     390class BaseRequestFormTraits(object):
     391    def __init__(self, request, **kwargs):
     392        self.request = request
     393        self._url = kwargs.pop('url', request.get_full_path())
     394        if request.method == 'POST':
     395            kwargs.setdefault('data', request.POST)
     396            kwargs.setdefault('files', request.FILES)
     397        self._file_upload_data = {}
     398        super(BaseRequestFormTraits, self).__init__(**kwargs)
     399        self.transfer_id = '%s.%s' % (self.__class__.__module__,
     400                                      self.__class__.__name__)
     401        self.prepare_upload()
     402
     403    def prepare_upload(self):
     404        for name in self._file_upload_data:
     405            del self.fields[name]
     406
     407        result = prepare_upload(self.transfer_id, self.request, url=self._url)
     408        self.upload_url, self._file_upload_data = result
     409
     410        for name, value in self._file_upload_data.items():
     411            self.fields[name] = Field(initial=value, required=False, widget=HiddenInput)
     412
     413class RequestForm(BaseRequestFormTraits, BaseForm):
     414    __metaclass__ = DeclarativeFieldsMetaclass
     415
    389416class BoundField(StrAndUnicode):
    390417    "A Field plus data"
    391418    def __init__(self, form, field, name):
  • django/forms/models.py

    diff -r 9a6a6b7a64e8 django/forms/models.py
    a b  
    1212from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
    1313from django.core.validators import EMPTY_VALUES
    1414from util import ErrorList
    15 from forms import BaseForm, get_declared_fields
     15from forms import BaseForm, BaseRequestFormTraits, get_declared_fields
    1616from fields import Field, ChoiceField
    1717from widgets import SelectMultiple, HiddenInput, MultipleHiddenInput
    1818from widgets import media_property
     
    2121__all__ = (
    2222    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
    2323    'save_instance', 'form_for_fields', 'ModelChoiceField',
    24     'ModelMultipleChoiceField',
     24    'ModelMultipleChoiceField', 'RequestModelForm',
    2525)
    2626
    2727def construct_instance(form, instance, fields=None, exclude=None):
     
    201201        formfield_callback = attrs.pop('formfield_callback',
    202202                lambda f, **kwargs: f.formfield(**kwargs))
    203203        try:
    204             parents = [b for b in bases if issubclass(b, ModelForm)]
     204            parents = [b for b in bases if issubclass(b, (BaseModelForm))]
    205205        except NameError:
    206206            # We are defining ModelForm itself.
    207207            parents = None
     
    375375class ModelForm(BaseModelForm):
    376376    __metaclass__ = ModelFormMetaclass
    377377
     378class RequestModelForm(BaseRequestFormTraits, BaseModelForm):
     379    __metaclass__ = ModelFormMetaclass
     380
    378381def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
    379382                       formfield_callback=lambda f: f.formfield()):
    380383    # Create the inner Meta class. FIXME: ideally, we should be able to
  • django/http/multipartparser.py

    diff -r 9a6a6b7a64e8 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 9a6a6b7a64e8 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        return '/redirect' + url, {'x-extra': 'abcd', 'x-data': 'efg'}
     9
     10    def serve(self, request, file, *args, **kwargs):
     11        if not file.name.endswith('pass'):
     12            return HttpResponseRedirect('/download/')
     13
     14    def download_url(self, file):
     15        if not file.name.endswith('pass'):
     16            return '/public/'
     17
     18    def get_storage_backend(self, *args, **kwargs):
     19        return models.temp_storage
  • new file tests/regressiontests/file_uploads/forms.py

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

    diff -r 9a6a6b7a64e8 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.DEFAULT_FILE_TRANSFER_BACKENDS.update({
     10    'regressiontests.file_uploads.*': 'regressiontests.file_uploads.backends.TestFileBackend',
     11})
    812
    913class FileModel(models.Model):
    10     testfile = models.FileField(storage=temp_storage, upload_to='test_upload')
     14    testfile = models.FileField(upload_to='test_upload')
  • tests/regressiontests/file_uploads/tests.py

    diff -r 9a6a6b7a64e8 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 9a6a6b7a64e8 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 9a6a6b7a64e8 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(request)
     119    return HttpResponse(simplejson.dumps([form.upload_url, list(map(unicode, form))]))
     120
     121def file_serve(request):
     122    return FileModel.objects.all()[0].testfile.serve(request)
Back to Top