mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-03 05:04:31 +03:00
commit
642970a1b8
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user