Django Rest Framework文件上传


98

我正在使用Django Rest Framework和AngularJs上传文件。我的视图文件如下所示:

class ProductList(APIView):
    authentication_classes = (authentication.TokenAuthentication,)
    def get(self,request):
        if request.user.is_authenticated(): 
            userCompanyId = request.user.get_profile().companyId
            products = Product.objects.filter(company = userCompanyId)
            serializer = ProductSerializer(products,many=True)
            return Response(serializer.data)

    def post(self,request):
        serializer = ProductSerializer(data=request.DATA, files=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(data=request.DATA)

由于post方法的最后一行应返回所有数据,因此我有几个问题:

  • 如何检查里面是否有东西request.FILES
  • 如何序列化文件字段?
  • 我应该如何使用解析器?

8
提示:Django自2013年以来已进行了重大升级。因此,如果其他人现在发布相同的问题。请不要将它们击落^ _ ^。
Jessi '18

Base64呢?
Hojat Modaresi

Answers:


67

使用FileUploadParser,一切都在请求中。改用put方法,您会在docs中找到一个示例:)

class FileUploadView(views.APIView):
    parser_classes = (FileUploadParser,)

    def put(self, request, filename, format=None):
        file_obj = request.FILES['file']
        # do some stuff with uploaded file
        return Response(status=204)

12
@pleasedont属于为什么在这里使用了PUT方法而不是POST?
坦维尔·雷汉(Tanvir Raihan)女士

8
您好@pleasedontbelong,如果它正在创建新记录,它将改为POST吗?并且它仍然可以与FileUploadParser一起使用吗?
nuttynibbles

1
@pleasedontbelong RTan提出了一个很好的问题。阅读RFC-2616提供了一个微妙的知识,直到现在我才意识到。“ POST和PUT请求之间的根本区别体现在Request-URI的不同含义。POST请求中的URI标识了将处理封闭实体的资源。该资源可能是数据接受过程,即网关到其他协议或接受注释的单独实体。相反,PUT请求中的URI标识了该请求所包含的实体”
dudeman

2
为什么选择FileUploadParser?“ FileUploadParser用于可将文件作为原始数据请求上传的本机客户端。对于基于Web的上载或具有分段上传支持的本机客户端,应改为使用MultiPartParser解析器。” 一般而言,这似乎不是一个好选择。而且,我认为文件上传不需要任何特殊处理
x-yuri

3
对于第二个@ x-yuri,DRF抱怨当我使用FileUploadParser时Content-Disposition标头为空。MultiPartParser更加简单,因为它只是假设文件名是“表单”字段中的给定文件名。
David Zwart,

74

我使用的是同一堆栈,也在寻找文件上传的示例,但是由于我使用ModelViewSet而不是APIView,因此情况更简单。原来的钥匙是pre_save钩子。我最终将其与angular-file-upload模块一起使用,如下所示:

# Django
class ExperimentViewSet(ModelViewSet):
    queryset = Experiment.objects.all()
    serializer_class = ExperimentSerializer

    def pre_save(self, obj):
        obj.samplesheet = self.request.FILES.get('file')

class Experiment(Model):
    notes = TextField(blank=True)
    samplesheet = FileField(blank=True, default='')
    user = ForeignKey(User, related_name='experiments')

class ExperimentSerializer(ModelSerializer):
    class Meta:
        model = Experiment
        fields = ('id', 'notes', 'samplesheet', 'user')

// AngularJS
controller('UploadExperimentCtrl', function($scope, $upload) {
    $scope.submit = function(files, exp) {
        $upload.upload({
            url: '/api/experiments/' + exp.id + '/',
            method: 'PUT',
            data: {user: exp.user.id},
            file: files[0]
        });
    };
});

11
pre_save在drf 3.x中已弃用
Guy S

从我的经验,无需特殊治疗需要的文件字段。
x-yuri

@ Guy-S,perform_create,perform_update,perform_destroy方法替换了不再可用的旧版本2.x pre_save,post_save,pre_delete和post_delete方法:django-rest-framework.org/api-guide/generic-views /#methods
Rufat

37

最后,我可以使用Django上传图片。这是我的工作代码

views.py

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

    def post(self, request, format='jpg'):
        up_file = request.FILES['file']
        destination = open('/Users/Username/' + up_file.name, 'wb+')
        for chunk in up_file.chunks():
            destination.write(chunk)
        destination.close()  # File should be closed only after all chuns are added

        # ...
        # do some stuff with uploaded file
        # ...
        return Response(up_file.name, status.HTTP_201_CREATED)

urls.py

urlpatterns = patterns('', 
url(r'^imageUpload', views.FileUploadView.as_view())

卷曲请求上传

curl -X POST -S -H -u "admin:password" -F "file=@img.jpg;type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload

14
为什么destination.close()放在for循环的内部?
makerj

12
似乎最好使用with open('/Users/Username/' + up_file.name, 'wb+') as destination:并完全关闭封盖
Chuck Wilbur

使用起来更简单ModelViewSet。同样,他们很可能会更好地实施它。
x-yuri

我整天都依靠这个应答器……直到我发现要上传多个文件时,这不是FileUploadParser必需的,而是MultiPartParser
奥利维尔·庞斯

13

在此上花了1天后,我发现...

对于需要上载文件并发送一些数据的人,没有直接的前进方式可以使它工作。json api规范中有一个未解决的问题。一种可能性是我见过在使用multipart/related这里,但是我认为很难在drf中实现它。

最后,我实现的是将请求发送为formdata。您将每个文件作为文件发送,所有其他数据作为文本发送。现在,以文本形式发送数据有两种选择。情况1)您可以将每个数据作为键值对发送,或情况2)您可以有一个名为data的键,并将整个json作为值字符串发送。

如果您具有简单的字段,则第一种方法开箱即用,但如果嵌套了序列化,则将是一个问题。多部分解析器将无法解析嵌套字段。

下面我提供两种情况的实现

型号

class Posts(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    caption = models.TextField(max_length=1000)
    media = models.ImageField(blank=True, default="", upload_to="posts/")
    tags = models.ManyToManyField('Tags', related_name='posts')

serializers.py->无需特殊更改,由于可写的ManyToMany Field含义,此处没有显示我的序列化程序太长。

views.py

class PostsViewset(viewsets.ModelViewSet):
    serializer_class = PostsSerializer
    #parser_classes = (MultipartJsonParser, parsers.JSONParser) use this if you have simple key value pair as data with no nested serializers
    #parser_classes = (parsers.MultipartParser, parsers.JSONParser) use this if you want to parse json in the key value pair data sent
    queryset = Posts.objects.all()
    lookup_field = 'id'

现在,如果您遵循第一种方法,并且仅将非Json数据作为键值对发送,则不需要自定义解析器类。DRF的MultipartParser将完成这项工作。但是对于第二种情况,或者如果您有嵌套的序列化器(如我所示),则需要自定义解析器,如下所示。

utils.py

from django.http import QueryDict
import json
from rest_framework import parsers

class MultipartJsonParser(parsers.MultiPartParser):

    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = {}

        # for case1 with nested serializers
        # parse each field with json
        for key, value in result.data.items():
            if type(value) != str:
                data[key] = value
                continue
            if '{' in value or "[" in value:
                try:
                    data[key] = json.loads(value)
                except ValueError:
                    data[key] = value
            else:
                data[key] = value

        # for case 2
        # find the data field and parse it
        data = json.loads(result.data["data"])

        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

该序列化程序将基本上解析值中的所有json内容。

邮递员在两种情况下的请求示例:情况1 情况1

情况二 案例2


我宁愿避免情况2。在大多数情况下,每个请求创建一个数据库记录应该没问题。
x-yuri

非常有帮助,非常感谢。但是我不明白,为什么要在解析器中将dict数据转换为QueryDict?以Django为例,普通字典数据无需转换即可完美运行。
MetehanGülaç

我使用您提到的答案尝试了另一种方案,该方案成功运行。你可以看看我的回答
MetehanGülaç

7

我用ModelViewSet和ModelSerializer解决了这个问题。希望这对社区有所帮助。

我还希望在序列化程序本身而不是视图中进行验证和Object-> JSON(反之亦然)登录。

让我们通过示例来了解它。

说,我想创建FileUploader API。它将在其中存储ID,file_path,file_name,大小,所有者等字段的位置。请参阅下面的示例模型:

class FileUploader(models.Model):
    file = models.FileField()
    name = models.CharField(max_length=100) #name is filename without extension
    version = models.IntegerField(default=0)
    upload_date = models.DateTimeField(auto_now=True, db_index=True)
    owner = models.ForeignKey('auth.User', related_name='uploaded_files')
    size = models.IntegerField(default=0)

现在,对于API,这就是我想要的:

  1. 得到:

当我触发GET端点时,我希望每个上传文件都具有以上所有字段。

  1. 开机自检:

但是对于用户创建/上传文件而言,为什么她不得不担心传递所有这些字段。她可以仅上传文件,然后,我想,序列化程序可以从上传的FILE中获取其余字段。

Searilizer: 问题:我在序列化器下面创建了我的目的。但是不确定它是否是实现它的正确方法。

class FileUploaderSerializer(serializers.ModelSerializer):
    # overwrite = serializers.BooleanField()
    class Meta:
        model = FileUploader
        fields = ('file','name','version','upload_date', 'size')
        read_only_fields = ('name','version','owner','upload_date', 'size')

   def validate(self, validated_data):
        validated_data['owner'] = self.context['request'].user
        validated_data['name'] = os.path.splitext(validated_data['file'].name)[0]
        validated_data['size'] = validated_data['file'].size
        #other validation logic
        return validated_data

    def create(self, validated_data):
        return FileUploader.objects.create(**validated_data)

参考视图集:

class FileUploaderViewSet(viewsets.ModelViewSet):
    serializer_class = FileUploaderSerializer
    parser_classes = (MultiPartParser, FormParser,)

    # overriding default query set
    queryset = LayerFile.objects.all()

    def get_queryset(self, *args, **kwargs):
        qs = super(FileUploaderViewSet, self).get_queryset(*args, **kwargs)
        qs = qs.filter(owner=self.request.user)
        return qs

FileUploaderSerializer.validate方法包含哪些验证逻辑?
x-yuri

7

根据我的经验,您不需要对文件字段做任何特别的事情,只需告诉它利用文件字段即可:

from rest_framework import routers, serializers, viewsets

class Photo(django.db.models.Model):
    file = django.db.models.ImageField()

    def __str__(self):
        return self.file.name

class PhotoSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Photo
        fields = ('id', 'file')   # <-- HERE

class PhotoViewSet(viewsets.ModelViewSet):
    queryset = models.Photo.objects.all()
    serializer_class = PhotoSerializer

router = routers.DefaultRouter()
router.register(r'photos', PhotoViewSet)

api_urlpatterns = ([
    url('', include(router.urls)),
], 'api')
urlpatterns += [
    url(r'^api/', include(api_urlpatterns)),
]

您就可以上传文件了:

curl -sS http://example.com/api/photos/ -F 'file=@/path/to/file'

-F field=value为模型具有的每个其他字段添加。并且不要忘记添加身份验证。


4

如果有人对ModelViewset for Django Rest Framework最简单的示例感兴趣。

该模型是

class MyModel(models.Model):
    name = models.CharField(db_column='name', max_length=200, blank=False, null=False, unique=True)
    imageUrl = models.FileField(db_column='image_url', blank=True, null=True, upload_to='images/')

    class Meta:
        managed = True
        db_table = 'MyModel'

序列化器

class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = "__all__"

观点是,

class MyModelView(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer

在邮递员中测试

在此处输入图片说明


以及我们如何使用ajax发送请求。实际上,imageUrl是什么?
Eduard Grigoryev

imageUrl是请求中的文件。
sadat

0

在django-rest-framework中,请求数据由解析Parsers
http://www.django-rest-framework.org/api-guide/parsers/

默认情况下,django-rest-framework采用解析器类JSONParser。它将数据解析为json。因此,文件将不会被解析。
如果我们希望文件与其他数据一起被解析,则应使用以下解析器类之一。

FormParser
MultiPartParser
FileUploadParser

在当前版本的DRF 3.8.2上,它将默认进行解析 application/jsonapplication/x-www-form-urlencodedmultipart/form-data
liquidki

0
    from rest_framework import status
    from rest_framework.response import Response
    class FileUpload(APIView):
         def put(request):
             try:
                file = request.FILES['filename']
                #now upload to s3 bucket or your media file
             except Exception as e:
                   print e
                   return Response(status, 
                           status.HTTP_500_INTERNAL_SERVER_ERROR)
             return Response(status, status.HTTP_200_OK)

0
def post(self,request):
        serializer = ProductSerializer(data=request.DATA, files=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

0

我想写一个我觉得更干净,更易于维护的选项。我们将使用defaultRouter为我们的视图集添加CRUD网址,并且还将添加一个固定的网址,以指定同一视图集内的上载器视图。

**** views.py 

from rest_framework import viewsets, serializers
from rest_framework.decorators import action, parser_classes
from rest_framework.parsers import JSONParser, MultiPartParser
from rest_framework.response import Response
from rest_framework_csv.parsers import CSVParser
from posts.models import Post
from posts.serializers import PostSerializer     


class PostsViewSet(viewsets.ModelViewSet):

    queryset = Post.objects.all()
    serializer_class = PostSerializer 
    parser_classes = (JSONParser, MultiPartParser, CSVParser)


    @action(detail=False, methods=['put'], name='Uploader View', parser_classes=[CSVParser],)
    def uploader(self, request, filename, format=None):
        # Parsed data will be returned within the request object by accessing 'data' attr  
        _data = request.data

        return Response(status=204)

项目的主要urls.py

**** urls.py 

from rest_framework import routers
from posts.views import PostsViewSet


router = routers.DefaultRouter()
router.register(r'posts', PostsViewSet)

urlpatterns = [
    url(r'^posts/uploader/(?P<filename>[^/]+)$', PostsViewSet.as_view({'put': 'uploader'}), name='posts_uploader')
    url(r'^', include(router.urls), name='root-api'),
    url('admin/', admin.site.urls),
]

.-自述文件。

当我们将@action装饰器添加到类方法'uploader'时,魔术发生了。通过指定“ methods = ['put']”参数,我们仅允许PUT请求;非常适合文件上传。

我还添加了参数“ parser_classes”,以显示可以选择将解析内容的解析器。我从rest_framework_csv包中添加了CSVParser,以演示如果需要此功能时我们如何仅接受某些类型的文件,在我的情况下,我仅接受“ Content-Type:text / csv”。注意:如果要添加自定义解析器,则需要在ViewSet的parsers_classes中指定它们,因为在访问上载器方法解析器之前,请求会将允许的media_type与主(类)解析器进行比较。

现在我们需要告诉Django如何使用此方法,以及可以在我们的url中实现的位置。那就是我们添加固定网址的时候(简单目的)。该网址将带有“文件名”参数,该参数稍后将在方法中传递。我们需要传递此方法“ uploader”,并在列表中指定http协议('PUT')到PostsViewSet.as_view方法。

当我们进入以下网址时

 http://example.com/posts/uploader/ 

它将期望一个带有标头的PUT请求,该标头指定“ Content-Type”和Content-Disposition:附件;filename =“ something.csv”。

curl -v -u user:pass http://example.com/posts/uploader/ --upload-file ./something.csv --header "Content-type:text/csv"

因此,建议您上传文件,然后将其附加到某些数据库记录中。如果由于某种原因连接从未发生怎么办?为什么不在一个请求中这样做?parser_classes没有限制可以上传的文件。它让您决定可以使用哪种格式进行请求。再次考虑,您处理上传的方式...似乎是将CSV中的数据放入数据库中。不是OP问的。
x-yuri

@ x-yuri说“ CSV是文件”,问题是;如何检查请求中是否有数据?通过使用此方法,您将在request.data中找到数据。_data = request.data,因为正在使用PUT。就像您说的那样,parser_classes可以决定可以使用哪种格式进行请求,因此,通过使用您不希望使用的任何其他格式,将被排除在外,从而增加了额外的安全性。您如何处理数据取决于您自己。使用“尝试除外”,您可以检查是否“从未发生连接”,而无需它,这不是代码所要做的。这些在1个请求提出
沃尔夫冈·莱昂

0

这是我已应用的一种方法,希望会对您有所帮助。

     class Model_File_update(APIView):
         parser_classes = (MultiPartParser, FormParser)
         permission_classes = [IsAuthenticated]  # it will check if the user is authenticated or not
         authentication_classes = [JSONWebTokenAuthentication]  # it will authenticate the person by JSON web token

         def put(self, request):
            id = request.GET.get('id')
            obj = Model.objects.get(id=id)
            serializer = Model_Upload_Serializer(obj, data=request.data)
            if serializer.is_valid():
               serializer.save()
               return Response(serializer.data, status=200)
            else:
               return Response(serializer.errors, status=400)

0

您可以通过生成解析器类来解析特定字段,然后将它们直接馈入标准DRF序列化器中,来概括@Nithin的答案以直接与DRF的现有序列化器系统一起工作:

from django.http import QueryDict
import json
from rest_framework import parsers


def gen_MultipartJsonParser(json_fields):
    class MultipartJsonParser(parsers.MultiPartParser):

        def parse(self, stream, media_type=None, parser_context=None):
            result = super().parse(
                stream,
                media_type=media_type,
                parser_context=parser_context
            )
            data = {}
            # find the data field and parse it
            qdict = QueryDict('', mutable=True)
            for json_field in json_fields:
                json_data = result.data.get(json_field, None)
                if not json_data:
                    continue
                data = json.loads(json_data)
                if type(data) == list:
                    for d in data:
                        qdict.update({json_field: d})
                else:
                    qdict.update({json_field: data})

            return parsers.DataAndFiles(qdict, result.files)

    return MultipartJsonParser

用法如下:

class MyFileViewSet(ModelViewSet):
    parser_classes = [gen_MultipartJsonParser(['tags', 'permissions'])]
    #                                           ^^^^^^^^^^^^^^^^^^^
    #                              Fields that need to be further JSON parsed
    ....

0

如果您正在使用ModelViewSet,那么实际上您已经做好了!它为您处理一切!您只需要将该字段放入ModelSerializer中并content-type=multipart/form-data;在客户端中进行设置即可。

但您知道您不能以json格式发送文件。(在客户端中将content-type设置为application / json时)。除非您使用Base64格式。

因此,您有两种选择:

  • ModelViewSetModelSerializer处理工作,并使用发送请求content-type=multipart/form-data;
  • 将字段设置ModelSerializer为,Base64ImageField (or) Base64FileField并告诉您的客户端将文件编码为Base64并设置content-type=application/json

0

models.py

from django.db import models

import uuid

class File(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    file = models.FileField(blank=False, null=False)
    
    def __str__(self):
        return self.file.name

serializers.py

from rest_framework import serializers
from .models import File

class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = File
        fields = "__all__"

views.py

from django.shortcuts import render
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status

from .serializers import FileSerializer


class FileUploadView(APIView):
    permission_classes = []
    parser_class = (FileUploadParser,)

    def post(self, request, *args, **kwargs):

      file_serializer = FileSerializer(data=request.data)

      if file_serializer.is_valid():
          file_serializer.save()
          return Response(file_serializer.data, status=status.HTTP_201_CREATED)
      else:
          return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

urls.py

from apps.files import views as FileViews

urlpatterns = [
    path('api/files', FileViews.FileUploadView.as_view()),
]

settings.py

# file uload parameters
MEDIA_URL =  '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

发送一个后发请求,api/files并将您的文件附加到form-data字段中file。该文件将被上载到/media文件夹,并且数据库记录将添加ID和文件名。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.