71

I am leaning how to upload file in Django, and here I encounter a should-be-trivial problem, with the error:

The submitted data was not a file. Check the encoding type on the form.

Below is the detail.


Note: I also looked at Django Rest Framework ImageField, and I tried

serializer = ImageSerializer(data=request.data, files=request.FILES)

but I get

TypeError: __init__() got an unexpected keyword argument 'files'


I have a Image model which I would like to interact with via Django REST framework:

models.py

class Image(models.Model):
    image = models.ImageField(upload_to='item_images')
    owner = models.ForeignKey(
        User, related_name='uploaded_item_images',
        blank=False,
    )
    time_created = models.DateTimeField(auto_now_add=True)

serializers.py

class ImageSerializer(serializers.ModelSerializer):
    image = serializers.ImageField(
        max_length=None, use_url=True,
    )

    class Meta:
        model = Image
        fields = ("id", 'image', 'owner', 'time_created', )

settings.py

'DEFAULT_PARSER_CLASSES': (
    'rest_framework.parsers.JSONParser',
    'rest_framework.parsers.FormParser',
    'rest_framework.parsers.MultiPartParser',
),

The front end (using AngularJS and angular-restmod or $resource) send JSON data with owner and image of the form:

Input:

{"owner": 5, "image": "data:image/jpeg;base64,/9j/4QqdRXhpZgAATU0A..."}

In the backend, request.data shows

{u'owner': 5, u'image': u'data:image/jpeg;base64,/9j/4QqdRXhpZgAATU0AKgAAA..."}

But then ImageSerializer(data=request.data).errors shows the error

ReturnDict([('image', [u'The submitted data was not a file. Check the encoding type on the form.'])])

I wonder what I should do to fix the error?


EDIT: JS part

The related front end codes consists of two parts: a angular-file-dnd directive (available here) to drop the file onto the page and angular-restmod, which provides CRUD operations:

<!-- The template: according to angular-file-dnd, -->
<!-- it will store the dropped image into variable $scope.image -->
<div file-dropzone="[image/png, image/jpeg, image/gif]" file="image" class='method' data-max-file-size="3" file-name="imageFileName">
  <div layout='row' layout-align='center'>
    <i class="fa fa-upload" style='font-size:50px;'></i>
  </div>
  <div class='text-large'>Drap & drop your photo here</div>
</div>



# A simple `Image` `model` to perform `POST`
$scope.image_resource = Image.$build();

$scope.upload = function() {
  console.log("uploading");
  $scope.image_resource.image = $scope.image;
  $scope.image_resource.owner = Auth.get_profile().user_id;
  return $scope.image_resource.$save();
};


An update concerning the problem: right now I switched to using ng-file-upload, which sends image data in proper format.

Community
  • 1
  • 1
Lelouch
  • 2,111
  • 2
  • 23
  • 33

2 Answers2

92

The problem that you are hitting is that Django REST framework expects files to be uploaded as multipart form data, through the standard file upload methods. This is typically a file field, but the JavaScript Blob object also works for AJAX.

You are looking to upload the files using a base64 encoded string, instead of the raw file, which is not supported by default. There are implementations of a Base64ImageField out there, but the most promising one came by a pull request.

Since these were mostly designed for Django REST framework 2.x, I've improved upon the one from the pull request and created one that should be compatible with DRF 3.

serializers.py

from rest_framework import serializers    

class Base64ImageField(serializers.ImageField):
    """
    A Django REST framework field for handling image-uploads through raw post data.
    It uses base64 for encoding and decoding the contents of the file.

    Heavily based on
    https://github.com/tomchristie/django-rest-framework/pull/1268

    Updated for Django REST framework 3.
    """

    def to_internal_value(self, data):
        from django.core.files.base import ContentFile
        import base64
        import six
        import uuid

        # Check if this is a base64 string
        if isinstance(data, six.string_types):
            # Check if the base64 string is in the "data:" format
            if 'data:' in data and ';base64,' in data:
                # Break out the header from the base64 content
                header, data = data.split(';base64,')

            # Try to decode the file. Return validation error if it fails.
            try:
                decoded_file = base64.b64decode(data)
            except TypeError:
                self.fail('invalid_image')

            # Generate file name:
            file_name = str(uuid.uuid4())[:12] # 12 characters are more than enough.
            # Get the file name extension:
            file_extension = self.get_file_extension(file_name, decoded_file)

            complete_file_name = "%s.%s" % (file_name, file_extension, )

            data = ContentFile(decoded_file, name=complete_file_name)

        return super(Base64ImageField, self).to_internal_value(data)

    def get_file_extension(self, file_name, decoded_file):
        import imghdr

        extension = imghdr.what(file_name, decoded_file)
        extension = "jpg" if extension == "jpeg" else extension

        return extension

This should be used in replacement of the standard ImageField provided by Django REST framework. So your serializer would become

class ImageSerializer(serializers.ModelSerializer):
    image = Base64ImageField(
        max_length=None, use_url=True,
    )

    class Meta:
        model = Image
        fields = ("id", 'image', 'owner', 'time_created', )

This should allow you to either specify a base64-encoded string, or the standard Blob object that Django REST framework typically expects.

Community
  • 1
  • 1
Kevin Brown-Silva
  • 40,873
  • 40
  • 203
  • 237
  • 1
    Thank you! For the moment, I tried a few images, but getting `'Upload a valid image. The file you uploaded was either not an image or a corrupted image.`. Maybe it's the problem in the front end, I'll try other frontend handling. But I wonder if there is a place to test online whether a base64-enconded string is valid? – Lelouch Jan 20 '15 at 02:41
  • Fantastic, it now works! Uploaded files are now sitting in the desired directory. Thank you! – Lelouch Jan 20 '15 at 02:59
  • 4
    Another version of this appears to be on PyPI now: https://github.com/Hipo/drf-extra-fields – Danilo Bargen Feb 27 '15 at 19:00
  • Works great on post requests, but on put requests, only "Raw content" is saved – xtrinch Aug 10 '16 at 14:09
  • I try add your solution of work with base64 encoded field and I get this output: Exception Value: Incorrect padding Exception Location: /home/bgarcial/.virtualenvs/fuupbol2/lib/python3.5/base64.py in b64decode, line 88 // Hee more details, just in case https://cldup.com/8EoH_qyquM.png – bgarcial Oct 28 '16 at 12:28
  • How should `to_representation` look like in this case? – 0leg Dec 28 '16 at 11:56
  • This solution seems to ignore argument `required=False` while declaring field `image = Base64ImageField(required=False, max_length=None, use_url=True, allow_empty_file=True, allow_null=True, )` because validation fails saying the field is required – juankysmith Mar 21 '17 at 10:53
  • @Kevin Brown Thanks a million ton brother !! I spent whole night to resolve it and finally I resolved it. :) :D – CrazyGeek Apr 09 '17 at 02:39
  • This one can be applied not only in serializer but for directly assign the method to the imagefield when creating an object. but you have to alter this to a simple function to work. nice work! – Shift 'n Tab Apr 20 '17 at 01:13
  • @juankysmith were you able to solve your problem? I am facing the exact same issue. It fails even when I `allow_null=True` in the serializer. – Sadan A. May 10 '18 at 16:09
  • Hi Why am i getting error that no module named `PIL` while upload file like this `image "blob:http://localhost:3000/ced70330-2afb-d848-a9d5-9091907d33ed"` from react frontedn @SadanA. @CrazyGeek @KevinBrown – Piotr Sep 11 '20 at 09:46
  • Hello @Kevin, can you please check my issue?? I have the same issue https://stackoverflow.com/questions/68002738/having-issue-in-handling-multipart-form-data-in-django-rest-framework – Reactoo Jun 21 '21 at 04:28
  • how can i modify the serializer to also except svg files? – EliyaMelamed Nov 19 '21 at 17:17
5

I ran in the same problem few days ago. Here is my django rest framework view to handle file uploading

views.py

class PhotoUploadView(APIView):
    parser_classes = (FileUploadParser,)

    def post(self, request):
        user = self.request.user
        if not user:
            return Response(status=status.HTTP_403_FORBIDDEN)
        profile  = None
        data     = None
        photo    = None

        file_form = FileUploadForm(request.POST,request.FILES)
        if file_form.is_valid():
            photo = request.FILES['file']
        else:
            return Response(ajax_response(file_form),status=status.HTTP_406_NOT_ACCEPTABLE)

        try:
            profile = Organizer.objects.get(user=user)
            profile.photo = photo
            profile.save()
            data    = OrganizersSerializer(profile).data
        except Organizer.DoesNotExist:
            profile = Student.objects.get(user=user)
            profile.photo = photo
            profile.save()
            data    = StudentsSerializer(profile).data

        return Response(data)

In front-end, I used angular-file-upload lib.

Here is my file input

<div ng-file-drop="" ng-file-select="" ng-model="organizer.photo" class="drop-box" drag-over-class="{accept:'dragover', reject:'dragover-err', delay:100}" ng-multiple="false" allow-dir="true" accept="image/*">
                                    Drop Images or PDFs<div>here</div>
</div>

And here is my upload service

main.js

(function () {
  'use strict';

  angular
    .module('trulii.utils.services')
    .factory('UploadFile', UploadFile);

  UploadFile.$inject = ['$cookies', '$http','$upload','$window','Authentication'];

  /**
  * @namespace Authentication
  * @returns {Factory}
  */
  function UploadFile($cookies, $http,$upload,$window,Authentication) {
    /**
    * @name UploadFile
    * @desc The Factory to be returned
    */


    var UploadFile = {
      upload_file: upload_file,
    };
    
    return UploadFile;


    function upload_file(file) {


      return $upload.upload({
        url: '/api/users/upload/photo/', // upload.php script, node.js route, or servlet url
        //method: 'POST' or 'PUT',
        //headers: {'Authorization': 'xxx'}, // only for html5
        //withCredentials: true,
        file: file, // single file or a list of files. list is only for html5
        //fileName: 'doc.jpg' or ['1.jpg', '2.jpg', ...] // to modify the name of the file(s)
        //fileFormDataName: myFile, // file formData name ('Content-Disposition'), server side request form name
                                    // could be a list of names for multiple files (html5). Default is 'file'
        //formDataAppender: function(formData, key, val){}  // customize how data is added to the formData. 
                                                            // See #40#issuecomment-28612000 for sample code

      })

    }


  }



})();
Community
  • 1
  • 1
levi
  • 22,001
  • 7
  • 73
  • 74
  • 1
    The original question is using a base64-encoded image string to upload the file, instead of the browser-based file object. This probably works for the general case though, when you need to upload files asynchronously using the standard `Blob` objects. – Kevin Brown-Silva Jan 20 '15 at 03:02
  • 1
    @levi: Thank you! your solution with `angular-file-upload` is probably more flexible, which I should try out in the future. For the moment I think I'll implement @KevinBrown's solution. – Lelouch Jan 20 '15 at 03:10
  • 2
    When you write `file_form = FileUploadForm(request.POST,request.FILES)`, where does `FileUploadForm` come from ? – Link14 Mar 30 '16 at 05:33
  • Could you give more information about the `FileUploadForm()` I get not module suggestion from that – Édouard Lopez Apr 24 '17 at 15:10
  • @levi when file_form is not valid you do a Response with file_form inside ajax_response fuction, where it comes from? – ANDRESMA May 24 '22 at 19:54