Merge pull request #806 from wronglink/master

Added FileUploadParser
This commit is contained in:
Tom Christie 2013-05-07 05:09:09 -07:00
commit 642970a1b8
3 changed files with 136 additions and 31 deletions

View File

@ -102,6 +102,28 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
**.media_type**: `multipart/form-data` **.media_type**: `multipart/form-data`
## FileUploadParser
Parses raw file upload content. Returns a `DataAndFiles` object. Since we expect the whole request body to be a file content `request.DATA` will be None, and `request.FILES` will contain the only one key `'file'` matching the uploaded file.
The `filename` property of uploaded file would be set to the result of `.get_filename()` method. By default it tries first to take it's value from the `filename` URL kwarg, and then from `Content-Disposition` HTTP header. You can implement other behaviour be overriding this method.
Note that since this parser's `media_type` matches every HTTP request it imposes restrictions on usage in combination with other parsers for the same API view.
Basic usage expamle:
class FileUploadView(views.APIView):
parser_classes = (FileUploadParser,)
def put(self, request, filename, format=None):
file_obj = request.FILES['file']
# ...
# do some staff with uploaded file
# ...
return Response(status=204)
**.media_type**: `*/*`
--- ---
# Custom parsers # Custom parsers
@ -145,35 +167,6 @@ The following is an example plaintext parser that will populate the `request.DAT
""" """
return stream.read() return stream.read()
## Uploading file content
If your custom parser needs to support file uploads, you may return a `DataAndFiles` object from the `.parse()` method. `DataAndFiles` should be instantiated with two arguments. The first argument will be used to populate the `request.DATA` property, and the second argument will be used to populate the `request.FILES` property.
For example:
class SimpleFileUploadParser(BaseParser):
"""
A naive raw file upload parser.
"""
media_type = '*/*' # Accept anything
def parse(self, stream, media_type=None, parser_context=None):
content = stream.read()
name = 'example.dat'
content_type = 'application/octet-stream'
size = len(content)
charset = 'utf-8'
# Write a temporary file based on the request content
temp = tempfile.NamedTemporaryFile(delete=False)
temp.write(content)
uploaded = UploadedFile(temp, name, content_type, size, charset)
# Return the uploaded file
data = {}
files = {name: uploaded}
return DataAndFiles(data, files)
--- ---
# Third party packages # Third party packages

View File

@ -6,9 +6,10 @@ on the request, such as form content or json encoded data.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from rest_framework.compat import yaml, etree from rest_framework.compat import yaml, etree
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.compat import six from rest_framework.compat import six
@ -205,3 +206,83 @@ class XMLParser(BaseParser):
pass pass
return value return value
class FileUploadParser(BaseParser):
"""
Parser for file upload data.
"""
media_type = '*/*'
def parse(self, stream, media_type=None, parser_context=None):
"""
Returns a DataAndFiles object.
`.data` will be None (we expect request body to be a file content).
`.files` will be a `QueryDict` containing one 'file' elemnt - a parsed file.
"""
parser_context = parser_context or {}
request = parser_context['request']
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
meta = request.META
upload_handlers = request.upload_handlers
filename = self.get_filename(stream, media_type, parser_context)
content_type = meta.get('HTTP_CONTENT_TYPE', meta.get('CONTENT_TYPE', ''))
try:
content_length = int(meta.get('HTTP_CONTENT_LENGTH', meta.get('CONTENT_LENGTH', 0)))
except (ValueError, TypeError):
content_length = None
# See if the handler will want to take care of the parsing.
for handler in upload_handlers:
result = handler.handle_raw_input(None,
meta,
content_length,
None,
encoding)
if result is not None:
return DataAndFiles(None, {'file': result[1]})
possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
chunk_size = min([2**31-4] + possible_sizes)
chunks = ChunkIter(stream, chunk_size)
counters = [0] * len(upload_handlers)
for handler in upload_handlers:
try:
handler.new_file(None, filename, content_type, content_length, encoding)
except StopFutureHandlers:
break
for chunk in chunks:
for i, handler in enumerate(upload_handlers):
chunk_length = len(chunk)
chunk = handler.receive_data_chunk(chunk, counters[i])
counters[i] += chunk_length
if chunk is None:
# If the chunk received by the handler is None, then don't continue.
break
for i, handler in enumerate(upload_handlers):
file_obj = handler.file_complete(counters[i])
if file_obj:
return DataAndFiles(None, {'file': file_obj})
raise ParseError("FileUpload parse error - none of upload handlers can handle the stream")
def get_filename(self, stream, media_type, parser_context):
"""
Detects the uploaded file name. First searches a 'filename' url kwarg.
Then tries to parse Content-Disposition header.
"""
try:
return parser_context['kwargs']['filename']
except KeyError:
pass
try:
meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'])
return disposition[1]['filename']
except (AttributeError, KeyError):
pass

View File

@ -1,10 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework.compat import StringIO from rest_framework.compat import StringIO
from django import forms from django import forms
from django.core.files.uploadhandler import MemoryFileUploadHandler
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework.compat import etree from rest_framework.compat import etree
from rest_framework.parsers import FormParser from rest_framework.parsers import FormParser, FileUploadParser
from rest_framework.parsers import XMLParser from rest_framework.parsers import XMLParser
import datetime import datetime
@ -82,3 +83,33 @@ class TestXMLParser(TestCase):
parser = XMLParser() parser = XMLParser()
data = parser.parse(self._complex_data_input) data = parser.parse(self._complex_data_input)
self.assertEqual(data, self._complex_data) self.assertEqual(data, self._complex_data)
class TestFileUploadParser(TestCase):
def setUp(self):
class MockRequest(object):
pass
from io import BytesIO
self.stream = BytesIO(
"Test text file".encode('utf-8')
)
request = MockRequest()
request.upload_handlers = (MemoryFileUploadHandler(),)
request.META = {
'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt'.encode('utf-8'),
'HTTP_CONTENT_LENGTH': 14,
}
self.parser_context = {'request': request, 'kwargs': {}}
def test_parse(self):
""" Make sure the `QueryDict` works OK """
parser = FileUploadParser()
self.stream.seek(0)
data_and_files = parser.parse(self.stream, None, self.parser_context)
file_obj = data_and_files.files['file']
self.assertEqual(file_obj._size, 14)
def test_get_filename(self):
parser = FileUploadParser()
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'file.txt'.encode('utf-8'))