aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2013-05-07 05:09:09 -0700
committerTom Christie2013-05-07 05:09:09 -0700
commit642970a1b8e6ebadbbfc9da4d75fad1ec5da6747 (patch)
tree42f0654be36b606f41dbad5259e7b534f0aa350b
parent5356af8651fccacf5524add33569dd84d9e78646 (diff)
parent5faaba9c691851ec68e385cc87d6bce82e4d4853 (diff)
downloaddjango-rest-framework-642970a1b8e6ebadbbfc9da4d75fad1ec5da6747.tar.bz2
Merge pull request #806 from wronglink/master
Added FileUploadParser
-rw-r--r--docs/api-guide/parsers.md51
-rw-r--r--rest_framework/parsers.py83
-rw-r--r--rest_framework/tests/parsers.py33
3 files changed, 136 insertions, 31 deletions
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 49d59d10..20518647 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -102,6 +102,28 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
**.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
@@ -145,35 +167,6 @@ The following is an example plaintext parser that will populate the `request.DAT
"""
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
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 491acd68..27a0db65 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -6,9 +6,10 @@ on the request, such as form content or json encoded data.
"""
from __future__ import unicode_literals
from django.conf import settings
+from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict
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.exceptions import ParseError
from rest_framework.compat import six
@@ -205,3 +206,83 @@ class XMLParser(BaseParser):
pass
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
diff --git a/rest_framework/tests/parsers.py b/rest_framework/tests/parsers.py
index 539c5b44..7699e10c 100644
--- a/rest_framework/tests/parsers.py
+++ b/rest_framework/tests/parsers.py
@@ -1,10 +1,11 @@
from __future__ import unicode_literals
from rest_framework.compat import StringIO
from django import forms
+from django.core.files.uploadhandler import MemoryFileUploadHandler
from django.test import TestCase
from django.utils import unittest
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
import datetime
@@ -82,3 +83,33 @@ class TestXMLParser(TestCase):
parser = XMLParser()
data = parser.parse(self._complex_data_input)
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'))