File upload with Django REST Framework

2018-04-17 by Senko Rašić

Django REST Framework (DRF) is a popular and powerful framework for building APIs in Django. Although it advocates and supports building RESTful APIs out of the box, it's flexible enough to let the developer do whatever it wants.

One of the things many developers do with DRF is handle file uploads the same way they handled file uploads in the old days: via as multi-part form data. In fact, when you search for django rest framework how to upload file, more than half of the results use this approach.

Although this approach works, I would argue that sending such a request with encoded file in the body is not very RESTful. Instead, in this case I prefer my API to accept a POST or PUT request with a file being directly uploaded, with the request body being thefile content.

Binary file upload

DRF makes this approach easy with FileUploadParser. A parser in DRF is a component that takes a raw request from the client and parses it into parts. Examples of parsers include JSON parser (for JSON requests) and form parser (for HTML forms). FileUploadParser assumes the entire request body is a single file, which is exactly what we want.

Here's how we can use this approach and save content in a FileField in a Django model:

from rest_framework.exceptions import ParseError
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response
from rest_framework.views import APIView

class MyUploadView(APIView):
    parser_class = (FileUploadParser,)

    def put(self, request, format=None):
        if 'file' not in request.data:
            raise ParseError("Empty content")

        f = request.data['file']

        mymodel.my_file_field.save(f.name, f, save=True)
        return Response(status=status.HTTP_201_CREATED)

We override parser classes for the view, instructing DRF to only use FileUploadParser. If successful, it will store the uploaded content in the file item in the request data. Small content will be kept in memory, while larger uploads will automatically use temporary files on disk, as usual with Django and DRF, but this implementation detail is hidden so you don't have to worry about it.

In our view, we check that there is some content, and if not, raise a ParseError (which will result in HTTP 400 Bad Request response to the client). If everything is fine, we save the content to a FileField in our model.

In this example we used a PUT method, but we could have used POST just as well. Just note than when doing uplodas this way, the client needs to signal the content filename to the service. It can either do so by specifying a file name in the path (URI) itself, or add a Content-Disposition header to the request. The FileUploadParser documentation has a detailed description of both methods.

Deleting uploaded content

All this works great if we want to upload a file, but it'd also be nice if we could delete it using the DELETE HTTP method on the same endpoint. To do that, let's add a delete method to our view:

class MyUploadView(APIView):
    ...

    def delete(self, request, format=None):
        mymodel.my_file_field.delete(save=True)
        return Response(status=status.HTTP_204_NO_CONTENT)

Content validation

Although with this method we can't use Serializers to validate input, we still several ways to validate the uploaded content.

One method is to restrict the content type (as reported by the client during the upload) to allowed types. For example, if our file upload is actually image upload, we can accept only image types. The default parser accepts any file type so we'll subclass it to restrict the accepted types:

class ImageUploadParser(FileUploadParser):
    media_type = 'image/*'

class MyUploadView(APIView):
    parser_class = (ImageUploadParser,)
    ...

However this relies on the client to not lie about the uploaded content type. If we want to make sure the file type is correct, we can do the inspection ourselves. In case of image files, we can do that easily using the Pillow package (used internaly by Django, so if you use image fields you're already using Pillow).

from PIL import Image

class MyUploadView(APIView):
    parser_class = (ImageUploadParser,)

    def put(self, request, format=None):
        if 'file' not in request.data:
            raise ParseError("Empty content")

        f = request.data['file']

        try:
            img = Image.open(f)
            img.verify()
        except:
            raise ParseError("Unsupported image type")

        mymodel.my_file_field.save(f.name, f, save=True)
        return Response(status=status.HTTP_201_CREATED)

User-friendly API

Why go to all this trouble?

Besides the aesthetical argument about it looking more RESTful, this approach also makes it easier for client authors to use your API. Whether in a mobile app, JavaScript in a browser, or from another backend language, your users will have an easier time.

Author
Senko Rašić
We’re small, experienced and passionate team of web developers, doing custom app development and web consulting.