Téléchargement de fichiers Django Rest Framework


98

J'utilise Django Rest Framework et AngularJs pour télécharger un fichier. Mon fichier de vue ressemble à ceci:

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)

Comme la dernière ligne de la méthode de publication doit renvoyer toutes les données, j'ai plusieurs questions:

  • comment vérifier s'il y a quelque chose dedans request.FILES?
  • comment sérialiser le champ de fichier?
  • comment utiliser l'analyseur?

8
JUSTE UNE NOTE AUX MODS: Django s'est considérablement amélioré depuis 2013. Donc, si quelqu'un d'autre posait la même question maintenant. VEUILLEZ ne pas les abattre ^ _ ^.
Jessi

Et Base64?
Hojat Modaresi le

Réponses:


67

Utilisez FileUploadParser , tout est dans la requête. Utilisez plutôt une méthode put, vous trouverez un exemple dans la documentation :)

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 pourquoi la méthode PUT a été utilisée ici au lieu de POST?
Md. Tanvir Raihan

8
salut @pleasedontbelong, s'il crée un nouvel enregistrement, serait-ce POST à ​​la place? et fonctionnera-t-il toujours avec FileUploadParser?
nuttynibbles

1
@pleasedontbelong RTan pose une très bonne question. La lecture de la RFC-2616 apporte une subtilité dont je n'étais pas conscient jusqu'à présent. "La différence fondamentale entre les demandes POST et PUT se reflète dans la signification différente de l'URI de demande. L'URI dans une demande POST identifie la ressource qui gérera l'entité incluse. Cette ressource peut être un processus acceptant des données, une passerelle à un autre protocole ou à une entité distincte qui accepte les annotations. En revanche, l'URI dans une demande PUT identifie l'entité jointe à la demande "
dudeman

2
Pourquoi FileUploadParser? "Le FileUploadParser est destiné à être utilisé avec des clients natifs qui peuvent télécharger le fichier en tant que demande de données brutes. Pour les téléchargements basés sur le Web ou pour les clients natifs avec prise en charge du téléchargement en plusieurs parties, vous devez utiliser l'analyseur MultiPartParser à la place." Cela ne semble pas être une bonne option en général. De plus, je ne vois pas les téléchargements de fichiers nécessitant un traitement particulier .
x-yuri

3
Pour seconder @ x-yuri, DRF se plaint que l'en-tête Content-Disposition est vide lorsque j'utilise FileUploadParser. MultiPartParser est beaucoup plus simple, car il suppose simplement que le nom de fichier est le nom de fichier donné dans les champs du formulaire.
David Zwart

74

J'utilise la même pile et je cherchais également un exemple de téléchargement de fichier, mais mon cas est plus simple puisque j'utilise ModelViewSet au lieu d'APIView. La clé s'est avérée être le hook pre_save. J'ai fini par l'utiliser avec le module angular-file-upload comme ceci:

# 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 est obsolète dans drf 3.x
Guy S

D'après mon expérience, aucun traitement spécial n'est nécessaire pour les champs de fichiers.
x-yuri

@ Les méthodes Guy-S, perform_create, perform_update, perform_destroy remplacent les méthodes à l'ancienne version 2.x pre_save, post_save, pre_delete et post_delete, qui ne sont plus disponibles: django-rest-framework.org/api-guide/generic-views / # methods
Rufat

37

Enfin, je suis capable de télécharger une image en utilisant Django. Voici mon code de travail

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 demande de téléchargement

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

14
pourquoi destination.close () est placé à l'intérieur de la boucle for?
makerj

12
Il semble qu'il serait préférable d'utiliser with open('/Users/Username/' + up_file.name, 'wb+') as destination:et de supprimer complètement la clôture
Chuck Wilbur

C'est plus simple à utiliser ModelViewSet. En outre, ils l'ont probablement mieux mis en œuvre.
x-yuri

Je me fie à cette réponse depuis toute la journée ... jusqu'à ce que je trouve que lorsque vous souhaitez télécharger plusieurs fichiers, ce n'est pas FileUploadParsernécessaire, mais MultiPartParser!
Olivier Pons

13

Après avoir passé 1 jour là-dessus, j'ai compris que ...

Pour quelqu'un qui a besoin de télécharger un fichier et d'envoyer des données, il n'y a pas de moyen direct de le faire fonctionner. Il y a un problème ouvert dans les spécifications de l'API json pour cela. Une possibilité que j'ai vue est d'utiliser multipart/relatedcomme indiqué ici , mais je pense que c'est très difficile de l'implémenter dans drf.

Enfin, ce que j'avais mis en œuvre était d'envoyer la demande sous forme de fichier formdata. Vous enverriez chaque fichier sous forme de fichier et toutes les autres données sous forme de texte. Maintenant, pour envoyer les données sous forme de texte, vous avez deux choix. cas 1) vous pouvez envoyer chaque donnée sous forme de paire clé / valeur ou cas 2) vous pouvez avoir une seule clé appelée data et envoyer le json entier sous forme de chaîne de valeur.

La première méthode fonctionnerait hors de la boîte si vous avez des champs simples, mais sera un problème si vous avez des sérialisations imbriquées. L'analyseur en plusieurs parties ne pourra pas analyser les champs imbriqués.

Ci-dessous, je fournis la mise en œuvre pour les deux cas

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 -> aucune modification spéciale n'est nécessaire, ne montrant pas mon sérialiseur ici comme étant trop long à cause de l'implémentation du champ ManyToMany inscriptible.

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'

Maintenant, si vous suivez la première méthode et n'envoyez que des données non Json sous forme de paires clé / valeur, vous n'avez pas besoin d'une classe d'analyseur personnalisée. DRF'd MultipartParser fera le travail. Mais pour le deuxième cas ou si vous avez des sérialiseurs imbriqués (comme je l'ai montré), vous aurez besoin d'un analyseur personnalisé comme indiqué ci-dessous.

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)

Ce sérialiseur analyserait essentiellement tout contenu json dans les valeurs.

L'exemple de requête dans post man pour les deux cas: cas 1 cas 1,

Cas 2 cas2


Je préfère éviter le cas 2. La création d'un enregistrement de base de données par demande devrait être bien la plupart du temps.
x-yuri

très utile merci beaucoup. Mais je ne comprends pas, pourquoi vous convertissez des données dict en QueryDict dans l'analyseur? Dans mon cas sous Django, les données de dictionnaire normales fonctionnent parfaitement sans conversion.
Metehan Gülaç le

J'ai essayé un scénario différent en utilisant la réponse que vous avez mentionnée et cela fonctionne avec succès. vous pouvez regarder ma réponse .
Metehan Gülaç le

7

J'ai résolu ce problème avec ModelViewSet et ModelSerializer. J'espère que cela aidera la communauté.

Je préfère également avoir la validation et la connexion Object-> JSON (et vice-versa) dans le sérialiseur lui-même plutôt que dans les vues.

Permet de comprendre par exemple.

Dites, je veux créer l'API FileUploader. Où il stockera des champs tels que id, chemin_fichier, nom_fichier, taille, propriétaire, etc. dans la base de données. Voir l'exemple de modèle ci-dessous:

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)

Maintenant, pour les API, c'est ce que je veux:

  1. AVOIR:

Lorsque je déclenche le point de terminaison GET, je veux tous les champs ci-dessus pour chaque fichier téléchargé.

  1. PUBLIER:

Mais pour que l'utilisateur crée / télécharge un fichier, pourquoi elle doit s'inquiéter de passer tous ces champs. Elle peut simplement télécharger le fichier et ensuite, je suppose, le sérialiseur peut obtenir le reste des champs du FICHIER téléchargé.

Searilizer: Question: J'ai créé ci-dessous le sérialiseur pour servir mon objectif. Mais je ne sais pas si c'est la bonne façon de le mettre en œuvre.

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)

Ensemble de vues pour référence:

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

Quelle logique de validation la FileUploaderSerializer.validateméthode contient-elle?
x-yuri

7

D'après mon expérience, vous n'avez rien à faire de particulier concernant les champs de fichier, vous lui dites simplement d'utiliser le champ de fichier:

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

et vous êtes prêt à télécharger des fichiers:

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

Ajoutez -F field=valuepour chaque champ supplémentaire de votre modèle. Et n'oubliez pas d'ajouter l'authentification.


4

Si quelqu'un est intéressé par l'exemple le plus simple avec ModelViewset pour Django Rest Framework.

Le modèle est,

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'

Le sérialiseur,

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

Et la vue est,

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

Test chez Postman,

entrez la description de l'image ici


Et comment pourrions-nous envoyer la demande en utilisant ajax. Qu'est-ce que imageUrl?
Eduard Grigoryev le

l'imageUrl est le fichier de la requête.
sadat le

0

Dans la requête django-rest-framework, les données sont analysées par le Parsers.
http://www.django-rest-framework.org/api-guide/parsers/

Par défaut, django-rest-framework prend la classe parser JSONParser. Il analysera les données dans json. ainsi, les fichiers ne seront pas analysés avec.
Si nous voulons que les fichiers soient analysés avec d'autres données, nous devons utiliser l'une des classes d'analyseurs ci-dessous.

FormParser
MultiPartParser
FileUploadParser

Sur la version actuelle de DRF 3.8.2, il analysera par défaut application/json, application/x-www-form-urlencodedet 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)

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

0

J'aimerais écrire une autre option qui me semble plus propre et plus facile à entretenir. Nous utiliserons le defaultRouter pour ajouter des URL CRUD pour notre ensemble de vues et nous ajouterons une autre URL fixe spécifiant la vue de téléchargement dans le même ensemble de vues.

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

Principales urls.py du projet

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

.- LISEZ-MOI.

La magie se produit lorsque nous ajoutons @action decorator à notre méthode de classe 'uploader'. En spécifiant l'argument "methods = ['put']", nous n'autorisons que les requêtes PUT; parfait pour le téléchargement de fichiers.

J'ai également ajouté l'argument "parser_classes" pour montrer que vous pouvez sélectionner l'analyseur qui analysera votre contenu. J'ai ajouté CSVParser à partir du package rest_framework_csv, pour montrer comment nous pouvons accepter uniquement certains types de fichiers si cette fonctionnalité est requise, dans mon cas, j'accepte uniquement "Content-Type: text / csv". Remarque: Si vous ajoutez des analyseurs personnalisés, vous devrez les spécifier dans parsers_classes dans le ViewSet car la requête comparera le media_type autorisé avec les analyseurs principaux (de classe) avant d'accéder aux analyseurs de méthode de téléchargement.

Nous devons maintenant dire à Django comment accéder à cette méthode et où peut être implémentée dans nos URL. C'est à ce moment que nous ajoutons l'url fixe (à des fins simples). Cette URL prendra un argument "filename" qui sera passé dans la méthode plus tard. Nous devons passer cette méthode "uploader", en spécifiant le protocole http ('PUT') dans une liste à la méthode PostsViewSet.as_view.

Lorsque nous atterrissons dans l'url suivante

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

il attendra une requête PUT avec des en-têtes spécifiant "Content-Type" et Content-Disposition: attachment; filename = "quelque chose.csv".

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

Vous suggérez donc de télécharger un fichier, puis de le joindre à un enregistrement de base de données. Et si l'attachement n'arrive jamais pour une raison quelconque? Pourquoi ne pas le faire en une seule demande? parser_classesn'est pas là pour limiter les fichiers pouvant être téléchargés. Cela vous permet de décider quels formats peuvent être utilisés pour faire des demandes. Après réflexion, la façon dont vous gérez le téléchargement ... il semble que vous mettez des données CSV dans une base de données. Pas ce que OP a demandé.
x-yuri

@ x-yuri en disant "un CSV est un fichier" et la question est; Comment vérifier s'il y a des données dans la demande? En utilisant cette méthode, vous trouverez les données dans request.data. _data = request.data due PUT est utilisé. Comme vous l'avez dit, les parser_classes sont là pour décider quels formats PEUVENT être utilisés pour faire une demande, donc en utilisant tout autre format que vous NE voulez PAS, sera alors exclu en ajoutant une couche supplémentaire de sécurité. Ce que vous faites de vos données dépend de vous. En utilisant "Try Except", vous pouvez vérifier si "l'attachement ne se produit jamais" alors que ce n'est pas nécessaire, ce n'est pas ce que fait le code. Celles-ci sont faites en 1 demande
Wolfgang Leon

0

C'est celle de l'approche que j'ai appliquée, j'espère qu'elle aidera.

     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

Vous pouvez généraliser la réponse de @ Nithin pour travailler directement avec le système de sérialiseur existant de DRF en générant une classe d'analyseur pour analyser des champs spécifiques qui sont ensuite introduits directement dans les sérialiseurs DRF standard:

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

Ceci est utilisé comme:

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

0

Si vous utilisez ModelViewSet, vous avez terminé! Il gère tout pour vous! Il vous suffit de mettre le champ dans votre ModelSerializer et de le définir content-type=multipart/form-data;dans votre client.

MAIS comme vous le savez, vous ne pouvez pas envoyer de fichiers au format json. (lorsque content-type est défini sur application / json dans votre client). Sauf si vous utilisez le format Base64.

Vous avez donc deux choix:

  • laissez ModelViewSetet ModelSerializergérez le travail et envoyez la demande en utilisantcontent-type=multipart/form-data;
  • définissez le champ ModelSerializercomme Base64ImageField (or) Base64FileFieldet indiquez à votre client d'encoder le fichier Base64et de définir lecontent-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')

Envoyez une demande de publication à api/filesavec votre fichier joint à un form-datachamp file. Le fichier sera téléchargé dans le /mediadossier et un enregistrement de base de données sera ajouté avec l'identifiant et le nom du fichier.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.