Ticket #13960: filetransfers.3.diff
File filetransfers.3.diff, 20.8 KB (added by , 14 years ago) |
---|
-
django/conf/global_settings.py
diff -r 9a6a6b7a64e8 django/conf/global_settings.py
a b 258 258 # Default file storage mechanism that holds media. 259 259 DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' 260 260 261 # High-level transfer backends 262 FILE_TRANSFER_BACKENDS = ( 263 'django.core.files.transfers.DefaultFileBackend', 264 ) 265 266 # The default transfer backend's configuration mapping transfer_id to 267 # backend name 268 DEFAULT_FILE_TRANSFER_BACKENDS = {} 269 261 270 # Absolute path to the directory that holds media. 262 271 # Example: "/home/media/media.lawrence.com/" 263 272 MEDIA_ROOT = '' -
new file django/core/files/transfers.py
diff -r 9a6a6b7a64e8 django/core/files/transfers.py
- + 1 from django.conf import settings 2 from django.core.exceptions import ImproperlyConfigured 3 from django.core.files.storage import default_storage 4 from django.http import HttpResponse 5 from django.utils.encoding import smart_str 6 from django.utils.importlib import import_module 7 import mimetypes 8 9 class 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 25 class 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 68 def 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 79 def 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 92 def 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 101 def 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 111 class ChunkedFile(object): 112 def __init__(self, file): 113 self.file = file 114 115 def __iter__(self): 116 return self.file.chunks() 117 118 def _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 132 def _get_backends(*args, **kwargs): 133 return (load_backend(name, *args, **kwargs) 134 for name in settings.FILE_TRANSFER_BACKENDS) 135 136 def 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 84 84 """ 85 85 pass 86 86 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): 88 88 """ 89 89 Signal that a new file has been started. 90 90 … … 96 96 self.content_type = content_type 97 97 self.content_length = content_length 98 98 self.charset = charset 99 if content_type_extra is None: 100 content_type_extra = {} 101 self.content_type_extra = content_type_extra 99 102 100 103 def receive_data_chunk(self, raw_data, start): 101 104 """ -
django/db/models/fields/files.py
diff -r 9a6a6b7a64e8 django/db/models/fields/files.py
a b 6 6 from django.conf import settings 7 7 from django.db.models.fields import Field 8 8 from django.core.files.base import File, ContentFile 9 from django.core.files.storage import default_storage10 9 from django.core.files.images import ImageFile, get_image_dimensions 10 from django.core.files.transfers import serve_file, download_url, get_storage_backend 11 11 from django.core.files.uploadedfile import UploadedFile 12 12 from django.utils.functional import curry 13 13 from django.db.models import signals … … 139 139 # be restored later, by FileDescriptor below. 140 140 return {'name': self.name, 'closed': False, '_committed': True, '_file': None} 141 141 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 142 150 class FileDescriptor(object): 143 151 """ 144 152 The descriptor for the file attribute on the model instance. Returns a … … 219 227 220 228 description = ugettext_lazy("File path") 221 229 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): 223 231 for arg in ('primary_key', 'unique'): 224 232 if arg in kwargs: 225 233 raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__)) 226 234 227 self.storage = storage or default_storage 235 self.transfer_id = transfer_id 236 self.storage = storage 228 237 self.upload_to = upload_to 229 238 if callable(upload_to): 230 239 self.generate_filename = upload_to … … 257 266 258 267 def contribute_to_class(self, cls, name): 259 268 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) 260 275 setattr(cls, self.name, self.descriptor_class(self)) 261 276 signals.post_delete.connect(self.delete_file, sender=cls) 262 277 -
django/forms/forms.py
diff -r 9a6a6b7a64e8 django/forms/forms.py
a b 3 3 """ 4 4 5 5 from django.core.exceptions import ValidationError 6 from django.core.files.transfers import prepare_upload 6 7 from django.utils.copycompat import deepcopy 7 8 from django.utils.datastructures import SortedDict 8 9 from django.utils.html import conditional_escape … … 10 11 from django.utils.safestring import mark_safe 11 12 12 13 from fields import Field, FileField 13 from widgets import Media, media_property, TextInput, Textarea 14 from widgets import Media, media_property, TextInput, Textarea, HiddenInput 14 15 from util import flatatt, ErrorDict, ErrorList 15 16 16 __all__ = ('BaseForm', 'Form' )17 __all__ = ('BaseForm', 'Form', 'RequestForm') 17 18 18 19 NON_FIELD_ERRORS = '__all__' 19 20 … … 386 387 # BaseForm itself has no way of designating self.fields. 387 388 __metaclass__ = DeclarativeFieldsMetaclass 388 389 390 class 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 413 class RequestForm(BaseRequestFormTraits, BaseForm): 414 __metaclass__ = DeclarativeFieldsMetaclass 415 389 416 class BoundField(StrAndUnicode): 390 417 "A Field plus data" 391 418 def __init__(self, form, field, name): -
django/forms/models.py
diff -r 9a6a6b7a64e8 django/forms/models.py
a b 12 12 from django.core.exceptions import ValidationError, NON_FIELD_ERRORS 13 13 from django.core.validators import EMPTY_VALUES 14 14 from util import ErrorList 15 from forms import BaseForm, get_declared_fields15 from forms import BaseForm, BaseRequestFormTraits, get_declared_fields 16 16 from fields import Field, ChoiceField 17 17 from widgets import SelectMultiple, HiddenInput, MultipleHiddenInput 18 18 from widgets import media_property … … 21 21 __all__ = ( 22 22 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 23 23 'save_instance', 'form_for_fields', 'ModelChoiceField', 24 'ModelMultipleChoiceField', 24 'ModelMultipleChoiceField', 'RequestModelForm', 25 25 ) 26 26 27 27 def construct_instance(form, instance, fields=None, exclude=None): … … 201 201 formfield_callback = attrs.pop('formfield_callback', 202 202 lambda f, **kwargs: f.formfield(**kwargs)) 203 203 try: 204 parents = [b for b in bases if issubclass(b, ModelForm)]204 parents = [b for b in bases if issubclass(b, (BaseModelForm))] 205 205 except NameError: 206 206 # We are defining ModelForm itself. 207 207 parents = None … … 375 375 class ModelForm(BaseModelForm): 376 376 __metaclass__ = ModelFormMetaclass 377 377 378 class RequestModelForm(BaseRequestFormTraits, BaseModelForm): 379 __metaclass__ = ModelFormMetaclass 380 378 381 def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 379 382 formfield_callback=lambda f: f.formfield()): 380 383 # 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 169 169 file_name = self.IE_sanitize(unescape_entities(file_name)) 170 170 171 171 content_type = meta_data.get('content-type', ('',))[0].strip() 172 content_type_extra = meta_data.get('content-type', (0,{}))[1] 172 173 try: 173 charset = meta_data.get('content-type', (0,{}))[1].get('charset', None)174 charset = content_type_extra.get('charset', None) 174 175 except: 175 176 charset = None 176 177 … … 185 186 try: 186 187 handler.new_file(field_name, file_name, 187 188 content_type, content_length, 188 charset )189 charset, content_type_extra) 189 190 except StopFutureHandlers: 190 191 break 191 192 -
new file tests/regressiontests/file_uploads/backends.py
diff -r 9a6a6b7a64e8 tests/regressiontests/file_uploads/backends.py
- + 1 from django.conf import settings 2 from django.core.files.transfers import BaseFileBackend 3 from django.http import HttpResponseRedirect 4 import models 5 6 class 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
- + 1 from django import forms 2 from models import FileModel 3 4 class 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 1 1 import tempfile 2 2 import os 3 from django.conf import settings 3 4 from django.db import models 4 5 from django.core.files.storage import FileSystemStorage 5 6 6 7 temp_storage = FileSystemStorage(tempfile.mkdtemp()) 7 8 UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload') 9 settings.DEFAULT_FILE_TRANSFER_BACKENDS.update({ 10 'regressiontests.file_uploads.*': 'regressiontests.file_uploads.backends.TestFileBackend', 11 }) 8 12 9 13 class 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 5 5 import unittest 6 6 from StringIO import StringIO 7 7 8 from django.conf import settings 8 9 from django.core.files import temp as tempfile 9 10 from django.core.files.uploadedfile import SimpleUploadedFile 10 11 from django.test import TestCase, client … … 17 18 18 19 UNICODE_FILENAME = u'test-0123456789_中文_Orléans.jpg' 19 20 21 class 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 20 72 class FileUploadTests(TestCase): 21 73 def test_simple_upload(self): 22 74 post_data = { … … 122 174 response = self.client.request(**r) 123 175 124 176 # The filenames should have been sanitized by the time it got to the view. 125 rec ieved = simplejson.loads(response.content)177 received = simplejson.loads(response.content) 126 178 for i, name in enumerate(scary_file_names): 127 got = rec ieved["file%s" % i]179 got = received["file%s" % i] 128 180 self.assertEqual(got, "hax0rd.txt") 129 181 130 182 def test_filename_overflow(self): -
tests/regressiontests/file_uploads/urls.py
diff -r 9a6a6b7a64e8 tests/regressiontests/file_uploads/urls.py
a b 10 10 (r'^quota/broken/$', views.file_upload_quota_broken), 11 11 (r'^getlist_count/$', views.file_upload_getlist_count), 12 12 (r'^upload_errors/$', views.file_upload_errors), 13 (r'^prepare_upload/$', views.file_prepare_upload), 14 (r'^serve/$', views.file_serve), 13 15 ) -
tests/regressiontests/file_uploads/views.py
diff -r 9a6a6b7a64e8 tests/regressiontests/file_uploads/views.py
a b 2 2 from django.core.files.uploadedfile import UploadedFile 3 3 from django.http import HttpResponse, HttpResponseServerError 4 4 from django.utils import simplejson 5 from forms import FileForm 5 6 from models import FileModel, UPLOAD_TO 6 7 from uploadhandler import QuotaUploadHandler, ErroringUploadHandler 7 8 from django.utils.hashcompat import sha_constructor … … 112 113 def file_upload_errors(request): 113 114 request.upload_handlers.insert(0, ErroringUploadHandler()) 114 115 return file_upload_echo(request) 116 117 def file_prepare_upload(request): 118 form = FileForm(request) 119 return HttpResponse(simplejson.dumps([form.upload_url, list(map(unicode, form))])) 120 121 def file_serve(request): 122 return FileModel.objects.all()[0].testfile.serve(request)