Upload do arquivo Django Rest Framework

97

Estou usando Django Rest Framework e AngularJs para fazer upload de um arquivo. Meu arquivo de visualização é assim:

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)

Como a última linha do método post deve retornar todos os dados, tenho várias perguntas:

  • como verificar se há algo dentro request.FILES?
  • como serializar o campo do arquivo?
  • como devo usar o analisador?
Pawan
fonte
8
APENAS UMA NOTA PARA OS MODS: Django atualizou muito desde 2013. Então, se alguém postar a mesma pergunta agora. POR FAVOR, não os derrube ^ _ ^.
Jessi
Que tal Base64?
Hojat Modaresi

Respostas:

67

Use o FileUploadParser , está tudo na solicitação. Em vez disso, use um método put. Você encontrará um exemplo na documentação :)

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)
satisfeito por pertencer
fonte
12
@pleasedontbelong porque o método PUT foi usado aqui em vez do POST?
Md. Tanvir Raihan
7
oi @pleasedontbelong, se estiver criando um novo registro, será POST? e ainda funcionará com FileUploadParser?
nuttynibbles
1
@pleasedontbelong RTan faz uma pergunta muito boa. A leitura da RFC-2616 fornece uma sutileza da qual eu não estava ciente até agora. "A diferença fundamental entre as solicitações POST e PUT se reflete no significado diferente do URI de Solicitação. O URI em uma solicitação POST identifica o recurso que tratará a entidade fechada. Esse recurso pode ser um processo de aceitação de dados, um gateway a algum outro protocolo, ou uma entidade separada que aceita anotações. Em contraste, o URI em uma solicitação PUT identifica a entidade incluída com a solicitação "
cara
2
Por que FileUploadParser? "O FileUploadParser é para uso com clientes nativos que podem fazer upload do arquivo como uma solicitação de dados brutos. Para uploads baseados na web ou para clientes nativos com suporte para upload de várias partes, você deve usar o analisador MultiPartParser." Geralmente não parece uma boa opção. Além do mais, não vejo uploads de arquivos que necessitem de um tratamento específico .
x-yuri
3
Em segundo lugar, @ x-yuri, DRF reclama sobre o cabeçalho Content-Disposition estar vazio quando eu uso o FileUploadParser. MultiPartParser é muito mais simples, pois apenas assume que o nome do arquivo é o nome fornecido nos campos do formulário.
David Zwart
74

Estou usando a mesma pilha e também procurando um exemplo de upload de arquivo, mas meu caso é mais simples, pois uso o ModelViewSet em vez do APIView. A chave acabou sendo o gancho pre_save. Acabei usando junto com o módulo de upload de arquivo angular assim:

# 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]
        });
    };
});
ybendana
fonte
11
pre_save foi descontinuado no drf 3.x
Guy S
Pela minha experiência, nenhum tratamento especial é necessário para os campos de arquivo.
x-yuri
@ Guy-S, perform_create, perform_update, métodos perform_destroy substituem os métodos pre_save, post_save, pre_delete e post_delete da versão 2.x do estilo antigo, que não estão mais disponíveis: django-rest-framework.org/api-guide/generic-views / # métodos
Rufat
36

Finalmente, consigo fazer upload de imagens usando Django. Aqui está meu código de trabalho

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())

pedido curl para fazer upload

curl -X POST -S -H -u "admin:password" -F "[email protected];type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload
Vipul J
fonte
14
por que destination.close () é colocado dentro do loop for?
makerj
12
Parece que seria melhor usar with open('/Users/Username/' + up_file.name, 'wb+') as destination:e remover totalmente o fechamento
Chuck Wilbur de
É mais simples de usar ModelViewSet. Além disso, eles provavelmente o implementaram melhor.
x-yuri
Tenho contado com esta resposta o dia todo ... até descobrir que quando você deseja fazer upload de vários arquivos, não FileUploadParseré necessário, mas MultiPartParser!
Olivier Pons
13

Depois de passar 1 dia nisso, descobri que ...

Para alguém que precisa fazer upload de um arquivo e enviar alguns dados, não há uma maneira direta de fazer isso funcionar. Há um problema aberto nas especificações da API JS para isso. Uma possibilidade que vi é usar multipart/relatedcomo mostrado aqui , mas acho muito difícil implementá-lo no DRF.

Por fim, o que implementei foi enviar a solicitação como formdata. Você enviaria cada arquivo como arquivo e todos os outros dados como texto. Agora, para enviar os dados como texto, você tem duas opções. caso 1) você pode enviar cada dado como par de valores-chave ou caso 2) você pode ter uma única chave chamada dados e enviar o json inteiro como string em valor.

O primeiro método funcionaria imediatamente se você tiver campos simples, mas será um problema se você tiver serializações aninhadas. O analisador multipartes não será capaz de analisar os campos aninhados.

Abaixo, estou fornecendo a implementação para ambos os casos

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 -> nenhuma mudança especial necessária, não mostrando meu serializador aqui como muito extenso devido à implementação gravável do campo 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'

Agora, se você estiver seguindo o primeiro método e apenas enviando dados não Json como pares de valores-chave, não precisará de uma classe de analisador customizada. O MultipartParser do DRF fará o trabalho. Mas para o segundo caso, ou se você tiver serializadores aninhados (como eu mostrei), você precisará do analisador personalizado conforme mostrado abaixo.

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)

Este serializador basicamente analisaria qualquer conteúdo json nos valores.

O exemplo de solicitação no correio para ambos os casos: caso 1 caso 1,

Caso 2 case2

Nithin
fonte
Prefiro evitar o caso 2. Criar um registro de banco de dados por solicitação deve ser adequado na maioria das vezes.
x-yuri
muito útil, muito obrigado. Mas eu não entendo, por que você está convertendo dados de dicionário em QueryDict no analisador? No meu caso no Django, os dados normais do dicionário funcionam perfeitamente sem conversão.
Metehan Gülaç
Tentei um cenário diferente usando a resposta que você mencionou e funcionou com sucesso. você pode olhar minha resposta .
Metehan Gülaç
7

Resolvi esse problema com ModelViewSet e ModelSerializer. Espero que isso ajude a comunidade.

Eu também prefiro ter validação e login de Objeto-> JSON (e vice-versa) no próprio serializador em vez de em visualizações.

Vamos entender por exemplo.

Digamos, eu quero criar a API FileUploader. Onde ele irá armazenar campos como id, file_path, file_name, size, owner etc no banco de dados. Veja o modelo de amostra abaixo:

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)

Agora, para APIs, isso é o que eu quero:

  1. OBTER:

Quando eu disparar o endpoint GET, quero todos os campos acima para cada arquivo carregado.

  1. POSTAR:

Mas para o usuário criar / fazer upload de arquivo, por que ela precisa se preocupar em passar todos esses campos. Ela pode simplesmente fazer upload do arquivo e, então, suponho que o serializador pode obter o restante dos campos de FILE carregado.

Searilizer: Pergunta: Eu criei o serializador abaixo para servir ao meu propósito. Mas não tenho certeza se é a maneira certa de implementá-lo.

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)

Viewset para referência:

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
Jadav Bheda
fonte
Que lógica de validação o FileUploaderSerializer.validatemétodo contém?
x-yuri
7

Pela minha experiência, você não precisa fazer nada em particular sobre os campos do arquivo, apenas diga a ele para usar o campo do arquivo:

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

e você está pronto para fazer upload de arquivos:

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

Adicione -F field=valuepara cada campo extra que seu modelo possui. E não se esqueça de adicionar autenticação.

x-yuri
fonte
4

Se alguém estiver interessado no exemplo mais fácil com ModelViewset for Django Rest Framework.

O modelo é,

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'

O serializador,

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

E a visão é,

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

Teste no Postman,

insira a descrição da imagem aqui

sadat
fonte
0

No django-rest-framework, os dados do pedido são analisados ​​pelo Parsers.
http://www.django-rest-framework.org/api-guide/parsers/

Por padrão, o django-rest-framework leva classe de analisador JSONParser. Ele analisará os dados em json. portanto, os arquivos não serão analisados ​​com ele.
Se quisermos que os arquivos sejam analisados ​​junto com outros dados, devemos usar uma das classes de analisador abaixo.

FormParser
MultiPartParser
FileUploadParser
anjaneyulubatta505
fonte
Na versão atual do DRF 3.8.2, ele irá analisar por padrão application/json, application/x-www-form-urlencodede multipart/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)
Sidhu Munagala
fonte
0
def post(self,request):
        serializer = ProductSerializer(data=request.DATA, files=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
Syed Faizan
fonte
0

Gostaria de escrever outra opção que considero mais limpa e fácil de manter. Estaremos usando o defaultRouter para adicionar urls CRUD para nosso conjunto de visualizações e adicionaremos mais um url fixo especificando a visualização do uploader dentro do mesmo conjunto de visualizações.

**** 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 principal do projeto

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

.- LEIA-ME.

A mágica acontece quando adicionamos @action decorator ao nosso método de classe 'uploader'. Ao especificar o argumento "methods = ['put']", estamos permitindo apenas solicitações PUT; perfeito para upload de arquivos.

Também adicionei o argumento "parser_classes" para mostrar que você pode selecionar o analisador que analisará seu conteúdo. Eu adicionei CSVParser do pacote rest_framework_csv, para demonstrar como podemos aceitar apenas certos tipos de arquivos se essa funcionalidade for necessária, no meu caso, estou aceitando apenas "Content-Type: text / csv". Nota: Se você estiver adicionando analisadores personalizados, você precisará especificá-los em parsers_classes no ViewSet porque a solicitação irá comparar o media_type permitido com os analisadores principais (classe) antes de acessar os analisadores do método uploader.

Agora precisamos dizer ao Django como ir para esse método e onde ele pode ser implementado em nossos urls. É quando adicionamos o url fixo (fins simples). Este Url terá um argumento "nome do arquivo" que será passado no método posteriormente. Precisamos passar este método "uploader", especificando o protocolo http ('PUT') em uma lista para o método PostsViewSet.as_view.

Quando pousamos no seguinte url

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

ele irá esperar uma solicitação PUT com cabeçalhos especificando "Content-Type" e Content-Disposition: attachment; filename = "alguma coisa.csv".

curl -v -u user:pass http://example.com/posts/uploader/ --upload-file ./something.csv --header "Content-type:text/csv"
Wolfgang Leon
fonte
Portanto, você sugere enviar um arquivo e, em seguida, anexá-lo a algum registro db. E se o apego nunca acontecer por algum motivo? Por que não fazer isso em um pedido? parser_classesnão existe para limitar quais arquivos podem ser carregados. Permite que você decida quais formatos podem ser usados ​​para fazer solicitações. Pensando bem, a maneira como você lida com o upload ... parece que você está colocando dados do CSV no banco de dados. Não foi o que OP perguntou.
x-yuri
@ x-yuri dizendo "um CSV é um arquivo" e a pergunta é; Como verificar se há dados na solicitação? Usando este método, você encontrará os dados em request.data. _data = request.data due PUT está sendo usado. Como você disse, parser_classes existem para decidir quais formatos PODEM ser usados ​​para fazer a solicitação, portanto, usando qualquer outro formato que você NÃO queira, será então excluído adicionando uma camada extra de segurança. O que você faz com seus dados é com você. Usando "Try Except" você pode verificar se "anexar nunca acontece" embora não haja necessidade disso, não é isso que o código faz. Isso é feito em 1 solicitação
Wolfgang Leon
0

Esta é a abordagem que apliquei, espero que ajude.

     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)
Harshit Trivedi
fonte
0

Você pode generalizar a resposta do @Nithin para trabalhar diretamente com o sistema serializador existente do DRF, gerando uma classe de analisador para analisar campos específicos que são alimentados diretamente nos serializadores DRF padrão:

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

Isso é usado como:

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

Se você estiver usando ModelViewSet, na verdade você terminou! Ele lida com todas as coisas para você! Você só precisa colocar o campo em seu ModelSerializer e definir content-type=multipart/form-data;em seu cliente.

MAS como você sabe não é possível enviar arquivos no formato json. (quando o tipo de conteúdo é definido como application / json em seu cliente). A menos que você use o formato Base64.

Então você tem duas opções:

  • deixe ModelViewSete ModelSerializercuide do trabalho e envie a solicitação usandocontent-type=multipart/form-data;
  • defina o campo ModelSerializercomo Base64ImageField (or) Base64FileFielde diga ao seu cliente para codificar o arquivo para Base64e definir ocontent-type=application/json
Hojat Modaresi
fonte
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')

Envie uma solicitação de postagem para api/filescom seu arquivo anexado a um form-datacampo file. O arquivo será carregado para a /mediapasta e um registro db será adicionado com o id e o nome do arquivo.

Achala Dissanayake
fonte