Django REST框架上传图片:"提交的数据不是文件"

71

我正在学习如何在Django中上传文件,但遇到了一个本应简单的问题,错误信息为:

提交的数据不是文件。请检查表单的编码类型。

以下是详细信息。


注意: 我也看了Django Rest框架ImageField,并尝试了

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

但是我收到以下错误信息:

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


我有一个Image模型,我想通过Django REST框架与其交互:

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)

序列化器.py

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

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

设置.py

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

前端使用AngularJS和angular-restmod$resource,发送带有表单中的ownerimageJSON数据:

输入:

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

在后端,request.data 显示

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

但是ImageSerializer(data=request.data).errors显示错误

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

我在思考如何修复这个错误?


编辑:JS部分

相关的前端代码包括两部分:一个angular-file-dnd指令(可在这里找到),用于将文件拖放到页面上,以及提供 CRUD 操作的 angular-restmod

<!-- 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();
};
关于问题的最新更新:现在我改用ng-file-upload来发送图像数据,以正确的格式发送。

你能否更新你的代码,包括用于POST数据的表单和任何相关的js? - Jamie Counsell
@JamieCounsell:我已经添加了上面的代码。不过我没有使用<form>标签。 - Lelouch
@Lelouch: "image": "data:image/jpeg;base64,/9j/4QqdRXhpZgAATU0A..." 这是哪种文件格式? - the_unknown_spirit
@Lelouch,你能分享给我这个问题的最终解决方案吗? - KitKit
2个回答

92
你遇到的问题是Django REST框架期望以多部分表单数据的形式上传文件,通过标准的文件上传方法。这通常是一个file字段,但JavaScript Blob对象也可以用于AJAX。请注意保留HTML标签。
你想使用base64编码字符串上传文件,而不是原始文件,这默认情况下不受支持。有一些Base64ImageField实现, 但最有前途的是通过拉取请求实现的
由于这些大多是为Django REST框架2.x设计的,我改进了拉取请求中的一个并创建了一个应该与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

这应该用来替换 Django REST framework 提供的标准 ImageField。因此,您的序列化器将变为:
class ImageSerializer(serializers.ModelSerializer):
    image = Base64ImageField(
        max_length=None, use_url=True,
    )

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

这将使您能够指定一个base64编码的字符串或Django REST框架通常期望的标准Blob对象。

1
谢谢!目前我尝试了几张图片,但是出现了“上传有效图像。您上传的文件不是图像或已损坏。”的错误提示。也许这是前端的问题,我会尝试其他前端处理方式。但我想知道是否有在线测试base64编码字符串是否有效的地方? - Lelouch
4
现在似乎在PyPI上有另一个版本:https://github.com/Hipo/drf-extra-fields - Danilo Bargen
在处理POST请求时表现良好,但在处理PUT请求时,只有“原始内容”被保存。 - xtrinch
@Kevin Brown 非常感谢你兄弟!!我整晚都在解决这个问题,最终我解决了。 :) :D - CrazyGeek
这个方法不仅可以应用于序列化器中,还可以直接将该方法分配给创建对象时的图像字段。但是你必须将其改为一个简单的函数才能正常工作。干得好! - Shift 'n Tab
显示剩余8条评论

5

几天前,我遇到了同样的问题。这是我的Django Rest Framework视图,用于处理文件上传。

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)

在前端,我使用了angular-file-upload库。

这是我的文件输入:

<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

      })

    }


  }



})();

1
原始问题是使用base64编码的图像字符串来上传文件,而不是基于浏览器的文件对象。尽管如此,在需要异步上传使用标准的Blob对象时,这可能适用于一般情况。 - Kevin Brown-Silva
1
@levi:谢谢你!你使用 angular-file-upload 的解决方案可能更加灵活,我将来应该尝试一下。但是目前我想实现 @KevinBrown 的解决方案。 - Lelouch
2
当你写file_form = FileUploadForm(request.POST,request.FILES)时,FileUploadForm是从哪里来的? - Link14
你能提供有关 FileUploadForm() 的更多信息吗?我没有从中得到模块建议。 - Édouard Lopez
@levi 当 file_form 不合法时,您会在 ajax_response 函数中使用带有 file_form 的 Response,它是从哪里来的? - ANDRESMA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接