Fixed #13721 -- Added UploadedFile.content_type_extra.

Thanks Waldemar Kornewald and mvschaik for work on the patch.
This commit is contained in:
Benjamin Kagia 2013-04-19 20:20:23 +03:00 committed by Tim Graham
parent ecd746191c
commit b0953dc913
9 changed files with 109 additions and 24 deletions

View File

@ -23,11 +23,12 @@ class UploadedFile(File):
""" """
DEFAULT_CHUNK_SIZE = 64 * 2**10 DEFAULT_CHUNK_SIZE = 64 * 2**10
def __init__(self, file=None, name=None, content_type=None, size=None, charset=None): def __init__(self, file=None, name=None, content_type=None, size=None, charset=None, content_type_extra=None):
super(UploadedFile, self).__init__(file, name) super(UploadedFile, self).__init__(file, name)
self.size = size self.size = size
self.content_type = content_type self.content_type = content_type
self.charset = charset self.charset = charset
self.content_type_extra = content_type_extra
def __repr__(self): def __repr__(self):
return force_str("<%s: %s (%s)>" % ( return force_str("<%s: %s (%s)>" % (
@ -55,13 +56,13 @@ class TemporaryUploadedFile(UploadedFile):
""" """
A file uploaded to a temporary location (i.e. stream-to-disk). A file uploaded to a temporary location (i.e. stream-to-disk).
""" """
def __init__(self, name, content_type, size, charset): def __init__(self, name, content_type, size, charset, content_type_extra):
if settings.FILE_UPLOAD_TEMP_DIR: if settings.FILE_UPLOAD_TEMP_DIR:
file = tempfile.NamedTemporaryFile(suffix='.upload', file = tempfile.NamedTemporaryFile(suffix='.upload',
dir=settings.FILE_UPLOAD_TEMP_DIR) dir=settings.FILE_UPLOAD_TEMP_DIR)
else: else:
file = tempfile.NamedTemporaryFile(suffix='.upload') file = tempfile.NamedTemporaryFile(suffix='.upload')
super(TemporaryUploadedFile, self).__init__(file, name, content_type, size, charset) super(TemporaryUploadedFile, self).__init__(file, name, content_type, size, charset, content_type_extra)
def temporary_file_path(self): def temporary_file_path(self):
""" """
@ -83,8 +84,8 @@ class InMemoryUploadedFile(UploadedFile):
""" """
A file uploaded into memory (i.e. stream-to-memory). A file uploaded into memory (i.e. stream-to-memory).
""" """
def __init__(self, file, field_name, name, content_type, size, charset): def __init__(self, file, field_name, name, content_type, size, charset, content_type_extra):
super(InMemoryUploadedFile, self).__init__(file, name, content_type, size, charset) super(InMemoryUploadedFile, self).__init__(file, name, content_type, size, charset, content_type_extra)
self.field_name = field_name self.field_name = field_name
def open(self, mode=None): def open(self, mode=None):
@ -109,7 +110,7 @@ class SimpleUploadedFile(InMemoryUploadedFile):
def __init__(self, name, content, content_type='text/plain'): def __init__(self, name, content, content_type='text/plain'):
content = content or b'' content = content or b''
super(SimpleUploadedFile, self).__init__(BytesIO(content), None, name, super(SimpleUploadedFile, self).__init__(BytesIO(content), None, name,
content_type, len(content), None) content_type, len(content), None, None)
def from_dict(cls, file_dict): def from_dict(cls, file_dict):
""" """

View File

@ -64,6 +64,7 @@ class FileUploadHandler(object):
self.content_type = None self.content_type = None
self.content_length = None self.content_length = None
self.charset = None self.charset = None
self.content_type_extra = None
self.request = request self.request = request
def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None): def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None):
@ -84,7 +85,7 @@ class FileUploadHandler(object):
""" """
pass pass
def new_file(self, field_name, file_name, content_type, content_length, charset=None): def new_file(self, field_name, file_name, content_type, content_length, charset=None, content_type_extra=None):
""" """
Signal that a new file has been started. Signal that a new file has been started.
@ -96,6 +97,7 @@ class FileUploadHandler(object):
self.content_type = content_type self.content_type = content_type
self.content_length = content_length self.content_length = content_length
self.charset = charset self.charset = charset
self.content_type_extra = content_type_extra
def receive_data_chunk(self, raw_data, start): def receive_data_chunk(self, raw_data, start):
""" """
@ -132,7 +134,7 @@ class TemporaryFileUploadHandler(FileUploadHandler):
Create the file object to append to as data is coming in. Create the file object to append to as data is coming in.
""" """
super(TemporaryFileUploadHandler, self).new_file(file_name, *args, **kwargs) super(TemporaryFileUploadHandler, self).new_file(file_name, *args, **kwargs)
self.file = TemporaryUploadedFile(self.file_name, self.content_type, 0, self.charset) self.file = TemporaryUploadedFile(self.file_name, self.content_type, 0, self.charset, self.content_type_extra)
def receive_data_chunk(self, raw_data, start): def receive_data_chunk(self, raw_data, start):
self.file.write(raw_data) self.file.write(raw_data)
@ -187,7 +189,8 @@ class MemoryFileUploadHandler(FileUploadHandler):
name = self.file_name, name = self.file_name,
content_type = self.content_type, content_type = self.content_type,
size = file_size, size = file_size,
charset = self.charset charset = self.charset,
content_type_extra = self.content_type_extra
) )

View File

@ -177,11 +177,9 @@ class MultiPartParser(object):
file_name = force_text(file_name, encoding, errors='replace') file_name = force_text(file_name, encoding, errors='replace')
file_name = self.IE_sanitize(unescape_entities(file_name)) file_name = self.IE_sanitize(unescape_entities(file_name))
content_type = meta_data.get('content-type', ('',))[0].strip() content_type, content_type_extra = meta_data.get('content-type', ('', {}))
try: content_type = content_type.strip()
charset = meta_data.get('content-type', (0, {}))[1].get('charset', None) charset = content_type_extra.get('charset')
except:
charset = None
try: try:
content_length = int(meta_data.get('content-length')[0]) content_length = int(meta_data.get('content-length')[0])
@ -194,7 +192,7 @@ class MultiPartParser(object):
try: try:
handler.new_file(field_name, file_name, handler.new_file(field_name, file_name,
content_type, content_length, content_type, content_length,
charset) charset, content_type_extra)
except StopFutureHandlers: except StopFutureHandlers:
break break

View File

@ -180,7 +180,11 @@ def encode_multipart(boundary, data):
def encode_file(boundary, key, file): def encode_file(boundary, key, file):
to_bytes = lambda s: force_bytes(s, settings.DEFAULT_CHARSET) to_bytes = lambda s: force_bytes(s, settings.DEFAULT_CHARSET)
if hasattr(file, 'content_type'):
content_type = file.content_type
else:
content_type = mimetypes.guess_type(file.name)[0] content_type = mimetypes.guess_type(file.name)[0]
if content_type is None: if content_type is None:
content_type = 'application/octet-stream' content_type = 'application/octet-stream'
return [ return [

View File

@ -30,6 +30,14 @@ In addition, the widgets now display a help message when the browser and
server time zone are different, to clarify how the value inserted in the field server time zone are different, to clarify how the value inserted in the field
will be interpreted. will be interpreted.
Minor features
~~~~~~~~~~~~~~
* The new :attr:`UploadedFile.content_type_extra
<django.core.files.uploadedfile.UploadedFile.content_type_extra>` attribute
contains extra parameters passed to the ``content-type`` header on a file
upload.
Backwards incompatible changes in 1.7 Backwards incompatible changes in 1.7
===================================== =====================================
@ -41,6 +49,14 @@ Backwards incompatible changes in 1.7
deprecation timeline for a given feature, its removal may appear as a deprecation timeline for a given feature, its removal may appear as a
backwards incompatible change. backwards incompatible change.
Miscellaneous
~~~~~~~~~~~~~
* The :meth:`django.core.files.uploadhandler.FileUploadHandler.new_file()`
method is now passed an additional ``content_type_extra`` parameter. If you
have a custom :class:`~django.core.files.uploadhandler.FileUploadHandler`
that implements ``new_file()``, be sure it accepts this new parameter.
Features deprecated in 1.7 Features deprecated in 1.7
========================== ==========================

View File

@ -240,6 +240,18 @@ In addition to those inherited from :class:`~django.core.files.File`, all
need to validate that the file contains the content that the content-type need to validate that the file contains the content that the content-type
header claims -- "trust but verify." header claims -- "trust but verify."
.. attribute:: UploadedFile.content_type_extra
.. versionadded:: 1.7
A dictionary containing extra parameters passed to the ``content-type``
header. This is typically provided by services, such as Google App Engine,
that intercept and handle file uploads on your behalf. As a result your
handler may not receive the uploaded file content, but instead a URL or
other pointer to the file. (see `RFC 2388`_ section 5.3).
.. _RFC 2388: http://www.ietf.org/rfc/rfc2388.txt
.. attribute:: UploadedFile.charset .. attribute:: UploadedFile.charset
For :mimetype:`text/*` content-types, the character set (i.e. ``utf8``) For :mimetype:`text/*` content-types, the character set (i.e. ``utf8``)
@ -350,6 +362,10 @@ list::
Writing custom upload handlers Writing custom upload handlers
------------------------------ ------------------------------
.. currentmodule:: django.core.files.uploadhandler
.. class:: FileUploadHandler
All file upload handlers should be subclasses of All file upload handlers should be subclasses of
``django.core.files.uploadhandler.FileUploadHandler``. You can define upload ``django.core.files.uploadhandler.FileUploadHandler``. You can define upload
handlers wherever you wish. handlers wherever you wish.
@ -359,7 +375,8 @@ Required methods
Custom file upload handlers **must** define the following methods: Custom file upload handlers **must** define the following methods:
``FileUploadHandler.receive_data_chunk(self, raw_data, start)`` .. method:: FileUploadHandler.receive_data_chunk(self, raw_data, start)
Receives a "chunk" of data from the file upload. Receives a "chunk" of data from the file upload.
``raw_data`` is a byte string containing the uploaded data. ``raw_data`` is a byte string containing the uploaded data.
@ -379,7 +396,8 @@ Custom file upload handlers **must** define the following methods:
If you raise a ``StopUpload`` or a ``SkipFile`` exception, the upload If you raise a ``StopUpload`` or a ``SkipFile`` exception, the upload
will abort or the file will be completely skipped. will abort or the file will be completely skipped.
``FileUploadHandler.file_complete(self, file_size)`` .. method:: FileUploadHandler.file_complete(self, file_size)
Called when a file has finished uploading. Called when a file has finished uploading.
The handler should return an ``UploadedFile`` object that will be stored The handler should return an ``UploadedFile`` object that will be stored
@ -392,7 +410,8 @@ Optional methods
Custom upload handlers may also define any of the following optional methods or Custom upload handlers may also define any of the following optional methods or
attributes: attributes:
``FileUploadHandler.chunk_size`` .. attribute:: FileUploadHandler.chunk_size
Size, in bytes, of the "chunks" Django should store into memory and feed Size, in bytes, of the "chunks" Django should store into memory and feed
into the handler. That is, this attribute controls the size of chunks into the handler. That is, this attribute controls the size of chunks
fed into ``FileUploadHandler.receive_data_chunk``. fed into ``FileUploadHandler.receive_data_chunk``.
@ -404,7 +423,8 @@ attributes:
The default is 64*2\ :sup:`10` bytes, or 64 KB. The default is 64*2\ :sup:`10` bytes, or 64 KB.
``FileUploadHandler.new_file(self, field_name, file_name, content_type, content_length, charset)`` .. method:: FileUploadHandler.new_file(self, field_name, file_name, content_type, content_length, charset, content_type_extra)
Callback signaling that a new file upload is starting. This is called Callback signaling that a new file upload is starting. This is called
before any data has been fed to any upload handlers. before any data has been fed to any upload handlers.
@ -421,13 +441,23 @@ attributes:
``charset`` is the character set (i.e. ``utf8``) given by the browser. ``charset`` is the character set (i.e. ``utf8``) given by the browser.
Like ``content_length``, this sometimes won't be provided. Like ``content_length``, this sometimes won't be provided.
``content_type_extra`` is extra information about the file from the
``content-type`` header. See :attr:`UploadedFile.content_type_extra
<django.core.files.uploadedfile.UploadedFile.content_type_extra>`.
This method may raise a ``StopFutureHandlers`` exception to prevent This method may raise a ``StopFutureHandlers`` exception to prevent
future handlers from handling this file. future handlers from handling this file.
``FileUploadHandler.upload_complete(self)`` .. versionadded:: 1.7
The ``content_type_extra`` parameter was added.
.. method:: FileUploadHandler.upload_complete(self)
Callback signaling that the entire upload (all files) has completed. Callback signaling that the entire upload (all files) has completed.
``FileUploadHandler.handle_raw_input(self, input_data, META, content_length, boundary, encoding)`` .. method:: FileUploadHandler.handle_raw_input(self, input_data, META, content_length, boundary, encoding)
Allows the handler to completely override the parsing of the raw Allows the handler to completely override the parsing of the raw
HTTP input. HTTP input.

View File

@ -187,6 +187,27 @@ class FileUploadTests(TestCase):
got = json.loads(self.client.request(**r).content.decode('utf-8')) got = json.loads(self.client.request(**r).content.decode('utf-8'))
self.assertTrue(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file'])) self.assertTrue(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file']))
def test_content_type_extra(self):
"""Uploaded files may have content type parameters available."""
tdir = tempfile.gettempdir()
no_content_type = tempfile.NamedTemporaryFile(suffix=".ctype_extra", dir=tdir)
no_content_type.write(b'something')
no_content_type.seek(0)
simple_file = tempfile.NamedTemporaryFile(suffix=".ctype_extra", dir=tdir)
simple_file.write(b'something')
simple_file.seek(0)
simple_file.content_type = 'text/plain; test-key=test_value'
response = self.client.post('/file_uploads/echo_content_type_extra/', {
'no_content_type': no_content_type,
'simple_file': simple_file,
})
received = json.loads(response.content.decode('utf-8'))
self.assertEqual(received['no_content_type'], {})
self.assertEqual(received['simple_file'], {'test-key': 'test_value'})
def test_truncated_multipart_handled_gracefully(self): def test_truncated_multipart_handled_gracefully(self):
""" """
If passed an incomplete multipart message, MultiPartParser does not If passed an incomplete multipart message, MultiPartParser does not

View File

@ -10,6 +10,7 @@ urlpatterns = patterns('',
(r'^verify/$', views.file_upload_view_verify), (r'^verify/$', views.file_upload_view_verify),
(r'^unicode_name/$', views.file_upload_unicode_name), (r'^unicode_name/$', views.file_upload_unicode_name),
(r'^echo/$', views.file_upload_echo), (r'^echo/$', views.file_upload_echo),
(r'^echo_content_type_extra/$', views.file_upload_content_type_extra),
(r'^echo_content/$', views.file_upload_echo_content), (r'^echo_content/$', views.file_upload_echo_content),
(r'^quota/$', views.file_upload_quota), (r'^quota/$', views.file_upload_quota),
(r'^quota/broken/$', views.file_upload_quota_broken), (r'^quota/broken/$', views.file_upload_quota_broken),

View File

@ -7,7 +7,7 @@ import os
from django.core.files.uploadedfile import UploadedFile from django.core.files.uploadedfile import UploadedFile
from django.http import HttpResponse, HttpResponseServerError from django.http import HttpResponse, HttpResponseServerError
from django.utils import six from django.utils import six
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes, smart_str
from .models import FileModel from .models import FileModel
from .tests import UNICODE_FILENAME, UPLOAD_TO from .tests import UNICODE_FILENAME, UPLOAD_TO
@ -136,3 +136,14 @@ def file_upload_filename_case_view(request):
obj = FileModel() obj = FileModel()
obj.testfile.save(file.name, file) obj.testfile.save(file.name, file)
return HttpResponse('%d' % obj.pk) return HttpResponse('%d' % obj.pk)
def file_upload_content_type_extra(request):
"""
Simple view to echo back extra content-type parameters.
"""
params = {}
for file_name, uploadedfile in request.FILES.items():
params[file_name] = dict([
(k, smart_str(v)) for k, v in uploadedfile.content_type_extra.items()
])
return HttpResponse(json.dumps(params))