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、2018年

Base64はどうですか?
Hojat Modaresi

回答:


67

FileUploadParserを使用してください。すべてリクエストに含まれています。代わりにputメソッドを使用してください。例はドキュメントにあります:)

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
@pleasedontbelongなぜPOSTメソッドの代わりにPUTメソッドが使用されたのですか?
Md。Tanvir Raihan 2016年

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
2番目の@ x-yuriに、FileUploadParserを使用すると、DRFはContent-Dispositionヘッダーが空であると文句を言います。MultiPartParserは、フォームフィールドでファイル名が指定されたファイル名であると想定しているだけなので、はるかに単純です。
David Zwart

74

同じスタックを使用していて、ファイルのアップロードの例も探していましたが、APIViewの代わりにModelViewSetを使用しているので、私の場合はより簡単です。キーは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:クローズを完全に使用して削除する方が良いようです
チャックウィルバー

使用する方が簡単ModelViewSetです。また、彼らはおそらくそれをよりよく実装しました。
x-yuri

私はこの回答者に1日中頼っていました...複数のファイルをアップロードしたいときにFileUploadParserそれが必要であるとは思いませんでしたMultiPartParser
Olivier Pons

13

これに1日を費やした後、私はそれを理解しました...

ファイルをアップロードしてデータを送信する必要がある人にとって、それを機能させるための簡単な方法はありません。このためのjson api仕様には未解決の問題があります。私が見た1つの可能性は、ここにmultipart/related示すように使用することですにが、DRFに実装するのは非常に難しいと思います。

最後に実装したのは、リクエストをとして送信することでしたformdata。あなたは、各ファイルを送信したファイルやテキストなど他のすべてのデータ。データをテキストとして送信するには、2つの選択肢があります。ケース1)各データをキーと値のペアとして送信するか、ケース2)dataという単一のキーを使用して、json全体を値の文字列として送信できます。

最初の方法は、単純なフィールドがあればすぐに機能しますが、シリアライズをネストしている場合は問題になります。マルチパートパーサーは、ネストされたフィールドを解析できません。

以下では、両方のケースの実装を提供しています

Models.py

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フィールドの実装が原因で、シリアライザが長すぎるとしてここに表示されていません。

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がその役割を果たします。ただし、2番目のケースの場合、またはシリアライザがネストされている場合(私が示したように)、次に示すようにカスタムパーサーが必要になります。

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


ケース2は避けたいと思います。ほとんどの場合、リクエストごとに1つのデータベースレコードを作成することで問題ありません。
x-yuri

大変参考にさせていただきます。しかし、理解できません、なぜパーサーでdictデータをQueryDictに変換しているのですか?Djangoの場合、通常の辞書データは変換せずに完全に機能します。
MetehanGülaç

あなたが言った答えを使用して別のシナリオを試しましたが、それはうまくいきました。あなたは私の答えを見ることができます。
MetehanGülaç

7

ModelViewSetとModelSerializerでこの問題を解決しました。これがコミュニティに役立つことを願っています。

また、ビューではなくシリアライザ自体で検証とObject-> JSON(およびその逆)ログインを行うことを推奨します。

例でそれを理解しましょう。

たとえば、FileUploader APIを作成したいとします。データベースのid、file_path、file_name、size、ownerなどのフィールドを格納する場所。以下のサンプルモデルを参照してください。

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から残りのフィールドを取得できます。

シーラライザー: 質問:目的を果たすために、以下のシリアライザーを作成しました。しかし、それを実装する正しい方法かどうかはわかりません。

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

Django Rest FrameworkのModelViewsetを使用した最も簡単な例に興味がある場合。

モデルは、

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

Postmanでテストし、

ここに画像の説明を入力してください


そして、どうすればajaxを使用してリクエストを送信できますか?実際にimageUrlとは何ですか?
Eduard Grigoryev

imageUrlはリクエスト内のファイルです。
サダ

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-urlencoded、とmultipart/form-data
Liquidki 2018

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 URLを追加し、同じビューセット内のアップローダービューを指定するもう1つの固定URLを追加します。

**** 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),
]

.- README。

マジックは、@ actionデコレーターをクラスメソッド 'uploader'に追加すると発生します。"methods = ['put']"引数を指定すると、PUTリクエストのみが許可されます。ファイルのアップロードに最適です。

また、引数「parser_classes」を追加して、コンテンツを解析するパーサーを選択できることを示しています。rest_framework_csvパッケージからCSVParserを追加して、この機能が必要な場合に特定のタイプのファイルのみを受け入れる方法を示します。私の場合、「Content-Type:text / csv」のみを受け入れます。注:カスタムパーサーを追加する場合は、リクエストがアップローダーメソッドパーサーにアクセスする前に許可されたmedia_typeをメイン(クラス)パーサーと比較するため、ViewSetのparsers_classesでそれらを指定する必要があります。

次に、Djangoにこのメソッドに移動する方法と、URLのどこに実装できるかを伝える必要があります。これは、固定URLを追加するときです(単純な目的)。このURLは、後でメソッドに渡される「ファイル名」引数を取ります。このメソッドを "uploader"に渡し、リストでhttpプロトコル( 'PUT')を指定してPostsViewSet.as_viewメソッドに渡す必要があります。

次のURLに着陸した場合

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

「Content-Type」とContent-Disposition:添付ファイルを指定するヘッダーを含むPUTリクエストが必要です。filename = "something.csv"。

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

したがって、ファイルをアップロードしてから、それをいくつかのdbレコードに添付することをお勧めします。何らかの理由でアタッチが発生しない場合はどうなりますか?1つのリクエストでそれを行わないのはなぜですか?parser_classesアップロードできるファイルを制限するためのものではありません。リクエストの作成に使用できる形式を決定できます。考え直して、アップロードの処理方法... CSVからデータベースにデータを入れているようです。OPが要求したものではありません。
x-yuri

@ x-yuriは「CSVはファイルです」と言って質問します。リクエストにデータがあるかどうかを確認するにはどうすればよいですか?このメソッドを使用すると、request.dataにデータが見つかります。_data = request.data due PUTが使用されています。あなたが言ったように、parser_classesは、要求を行うために使用できるフォーマットを決定するためにあります。したがって、不要な他のフォーマットを使用することによって除外され、セキュリティの層が追加されます。あなたがあなたのデータをどうするかはあなた次第です。「Try Except」を使用すると、「アタッチが発生しない」かどうかを確認できます。必要がない場合、それはコードが行うことではありません。これらは1つの要求で作成されます
Wolfgang Leon

0

これは私が採用したアプローチの1つであり、うまくいけば役立つと思います。

     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

@Nithinの回答を一般化して、特定のフィールドを解析するパーサークラスを生成し、標準のDRFシリアライザーに直接フィードすることにより、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形式を使用しない限り。

したがって、2つの選択肢があります。

  • ジョブを任せModelViewSetModelSerializer処理し、次を使用してリクエストを送信しますcontent-type=multipart/form-data;
  • ModelSerializerasにフィールドを設定し、Base64ImageField (or) Base64FileFieldクライアントにファイルをエンコードしてBase64content-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フォルダーにアップロードされ、dbレコードがIDとファイル名とともに追加されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.