Caricamento di file Django Rest Framework


98

Sto usando Django Rest Framework e AngularJs per caricare un file. Il mio file di visualizzazione ha questo aspetto:

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)

Poiché l'ultima riga del metodo di post dovrebbe restituire tutti i dati, ho diverse domande:

  • come controllare se c'è qualcosa request.FILES?
  • come serializzare il campo del file?
  • come dovrei usare il parser?

8
SOLO UNA NOTA PER I MOD: Django è stato aggiornato notevolmente dal 2013. Quindi se qualcun altro posta la stessa domanda ora. PER FAVORE, non abbatterli ^ _ ^.
Jessi

Che ne dici di Base64?
Hojat Modaresi,

Risposte:


67

Usa FileUploadParser , è tutto nella richiesta. Usa invece un metodo put, troverai un esempio nella documentazione :)

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 perché qui è stato utilizzato il metodo PUT invece del POST?
Md. Tanvir Raihan

8
ciao @pleasedontbelong, se sta creando un nuovo record, sarebbe invece POST? e funzionerà ancora con FileUploadParser?
Nuttynibbles

1
@pleasedontbelong RTan fa una bella domanda. La lettura di RFC-2616 fornisce una sottigliezza di cui non ero a conoscenza fino ad ora. "La differenza fondamentale tra le richieste POST e PUT si riflette nel diverso significato di Request-URI. L'URI in una richiesta POST identifica la risorsa che gestirà l'entità inclusa. Quella risorsa potrebbe essere un processo di accettazione dei dati, un gateway a qualche altro protocollo, o un'entità separata che accetta annotazioni. Al contrario, l'URI in una richiesta PUT identifica l'entità allegata alla richiesta "
dudeman

2
Perché FileUploadParser? "FileUploadParser è per l'utilizzo con client nativi che possono caricare il file come richiesta di dati non elaborati. Per i caricamenti basati sul Web o per i client nativi con supporto di caricamento in più parti, è necessario utilizzare invece il parser MultiPartParser." Non sembra una buona opzione in generale. Inoltre, non vedo caricamenti di file che richiedono un trattamento particolare .
x-yuri

3
Al secondo @ x-yuri, DRF si lamenta del fatto che l'intestazione Content-Disposition sia vuota quando uso FileUploadParser. MultiPartParser è molto più semplice, poiché presuppone semplicemente che il nome del file sia il nome del file specificato nei campi del modulo.
David Zwart,

74

Sto usando lo stesso stack e stavo anche cercando un esempio di caricamento di file, ma il mio caso è più semplice poiché utilizzo ModelViewSet invece di APIView. La chiave si è rivelata essere l'hook pre_save. Ho finito per usarlo insieme al modulo di caricamento file angolare in questo modo:

# 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 è deprecato in drf 3.x
Guy S

Dalla mia esperienza, non è necessario alcun trattamento speciale per i campi dei file.
x-yuri

@ Guy-S, perform_create, perform_update, perform_destroy metodi sostituiscono la vecchia versione 2.x metodi pre_save, post_save, pre_delete e post_delete, che non sono più disponibili: django-rest-framework.org/api-guide/generic-views / # metodi
Rufat

37

Finalmente sono in grado di caricare l'immagine usando Django. Ecco il mio codice di lavoro

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

richiesta di ricciolo da caricare

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

14
perché destination.close () è posizionato all'interno del ciclo for?
makerj

12
Sembra che sarebbe meglio usare with open('/Users/Username/' + up_file.name, 'wb+') as destination:e rimuovere completamente la chiusura
Chuck Wilbur

È più semplice da usare ModelViewSet. Inoltre, molto probabilmente lo hanno implementato meglio.
x-yuri

Mi sono affidato a questa risposta per tutto il giorno ... fino a quando ho scoperto che quando vuoi caricare più file, non FileUploadParserè necessario, ma MultiPartParser!
Olivier Pons

13

Dopo aver trascorso 1 giorno su questo, ho capito che ...

Per qualcuno che ha bisogno di caricare un file e inviare alcuni dati, non esiste un modo diretto per farlo funzionare. C'è un problema aperto nelle specifiche API JSON per questo. Una possibilità che ho visto è quella di utilizzare multipart/relatedcome mostrato qui , ma penso che sia molto difficile implementarlo in drf.

Infine quello che avevo implementato era inviare la richiesta come formdata. Dovresti inviare ogni file come file e tutti gli altri dati come testo. Ora per inviare i dati come testo hai due scelte. caso 1) puoi inviare ogni dato come coppia di valori chiave o caso 2) puoi avere una singola chiave chiamata dati e inviare l'intero json come stringa in valore.

Il primo metodo funzionerebbe immediatamente se si dispone di campi semplici, ma sarà un problema se si dispone di serializzazioni nidificate. Il parser multiparte non sarà in grado di analizzare i campi nidificati.

Di seguito fornisco l'implementazione per entrambi i casi

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 -> non sono necessarie modifiche speciali, non mostrare il mio serializzatore qui perché è troppo lungo a causa dell'implementazione del 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'

Ora, se stai seguendo il primo metodo e invii solo dati non Json come coppie di valori chiave, non hai bisogno di una classe parser personalizzata. DRF'd MultipartParser farà il lavoro. Ma per il secondo caso o se hai serializzatori annidati (come ho mostrato) avrai bisogno di un parser personalizzato come mostrato di seguito.

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)

Questo serializzatore analizzerebbe fondamentalmente qualsiasi contenuto JSON nei valori.

L'esempio di richiesta in post man per entrambi i casi: caso 1 caso 1,

Caso 2 case2


Preferisco evitare il caso 2. La creazione di un record di database per richiesta dovrebbe andare bene la maggior parte delle volte.
x-yuri

molto utile grazie mille. Ma non capisco, perché stai convertendo i dati di Dict in QueryDict nel parser? Nel mio caso in Django, i normali dati del dizionario funzionano perfettamente senza essere convertiti.
Metehan Gülaç

Ho provato uno scenario diverso usando la risposta che hai menzionato e funziona correttamente. puoi guardare la mia risposta .
Metehan Gülaç

7

Ho risolto questo problema con ModelViewSet e ModelSerializer. Spero che questo aiuti la comunità.

Preferisco anche avere la convalida e il login Object-> JSON (e viceversa) nel serializzatore stesso piuttosto che nelle viste.

Comprendiamolo con l'esempio.

Dì, voglio creare FileUploader API. Dove memorizzerà campi come id, file_path, file_name, size, owner ecc nel database. Vedere il modello di esempio di seguito:

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)

Ora, per le API questo è quello che voglio:

  1. OTTENERE:

Quando avvio l'endpoint GET, voglio tutti i campi sopra per ogni file caricato.

  1. INVIARE:

Ma per l'utente per creare / caricare file, perché deve preoccuparsi di passare tutti questi campi. Può semplicemente caricare il file e quindi, suppongo, il serializzatore può ottenere il resto dei campi dal FILE caricato.

Searilizer: Domanda: Ho creato sotto il serializzatore per servire al mio scopo. Ma non sono sicuro che sia il modo giusto per implementarlo.

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 per riferimento:

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

Quale logica di convalida FileUploaderSerializer.validatecontiene il metodo?
x-yuri

7

Dalla mia esperienza, non è necessario fare nulla di particolare sui campi file, basta dirgli di utilizzare il campo file:

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 sei pronto per caricare i file:

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

Aggiungi -F field=valueper ogni campo extra del tuo modello. E non dimenticare di aggiungere l'autenticazione.


4

Se qualcuno è interessato all'esempio più semplice con ModelViewset per Django Rest Framework.

Il modello è,

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'

Il serializzatore,

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

E la vista è,

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

Test in Postman,

inserisci qui la descrizione dell'immagine


E come potremmo inviare la richiesta utilizzando ajax. Cos'è effettivamente imageUrl?
Eduard Grigoryev

imageUrl è il file nella richiesta.
Sadat

0

Nella richiesta django-rest-framework i dati vengono analizzati dal Parsers.
http://www.django-rest-framework.org/api-guide/parsers/

Per impostazione predefinita, django-rest-framework accetta la classe parser JSONParser. Analizzerà i dati in json. quindi, i file non verranno analizzati con esso.
Se vogliamo che i file vengano analizzati insieme ad altri dati, dovremmo usare una delle seguenti classi di parser.

FormParser
MultiPartParser
FileUploadParser

Nella versione attuale del DRF 3.8.2, si analizzerà per default 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)

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

0

Vorrei scrivere un'altra opzione che ritengo più pulita e più facile da mantenere. Useremo il defaultRouter per aggiungere URL CRUD per il nostro set di visualizzazioni e aggiungeremo un altro URL fisso specificando la vista del caricatore all'interno dello stesso set di visualizzazioni.

**** 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 principale del progetto

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

.- LEGGIMI.

La magia accade quando aggiungiamo @action decorator al nostro metodo di classe "uploader". Specificando l'argomento "methods = ['put']", consentiamo solo le richieste PUT; perfetto per il caricamento di file.

Ho anche aggiunto l'argomento "parser_classes" per mostrare che puoi selezionare il parser che analizzerà il tuo contenuto. Ho aggiunto CSVParser dal pacchetto rest_framework_csv, per dimostrare come possiamo accettare solo alcuni tipi di file se questa funzionalità è richiesta, nel mio caso accetto solo "Content-Type: text / csv". Nota: se stai aggiungendo parser personalizzati, dovrai specificarli in parsers_classes nel ViewSet perché la richiesta confronterà il media_type consentito con i parser principali (classe) prima di accedere ai parser del metodo uploader.

Ora dobbiamo dire a Django come utilizzare questo metodo e dove può essere implementato nei nostri URL. È allora che aggiungiamo l'URL fisso (scopi semplici). Questo URL prenderà un argomento "filename" che verrà passato nel metodo in seguito. Dobbiamo passare questo metodo "uploader", specificando il protocollo http ('PUT') in un elenco al metodo PostsViewSet.as_view.

Quando atterriamo nel seguente URL

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

aspetterà una richiesta PUT con intestazioni che specificano "Content-Type" e Content-Disposition: attachment; nomefile = "qualcosa.csv".

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

Quindi suggerisci di caricare un file, quindi allegarlo a qualche record db. E se l'attaccamento non avviene mai per qualche motivo? Perché non farlo in un'unica richiesta? parser_classesnon è lì per limitare i file che possono essere caricati. Ti consente di decidere quali formati possono essere utilizzati per effettuare richieste. A pensarci bene, il modo in cui gestisci il caricamento ... sembra che tu stia inserendo i dati da CSV nel database. Non quello che ha chiesto OP.
x-yuri

@ x-yuri dicendo "un CSV è un file" e la domanda è; Come verificare se ci sono dati nella richiesta? Utilizzando questo metodo, troverai i dati in request.data. _data = request.data due PUT è in uso. Come hai detto, parser_classes sono lì per decidere quali formati POSSONO essere utilizzati per fare richiesta, quindi utilizzando qualsiasi altro formato che NON vuoi, verranno quindi esclusi aggiungendo un ulteriore livello di sicurezza. Quello che fai con i tuoi dati dipende da te. Usando "Try Except" puoi controllare se "il collegamento non avviene mai" anche se non ce n'è bisogno, non è quello che fa il codice. Questi sono fatti in 1 richiesta
Wolfgang Leon

0

Questo è l'approccio che ho applicato, spero che possa aiutare.

     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

Puoi generalizzare la risposta di @ Nithin per lavorare direttamente con il sistema serializzatore esistente di DRF generando una classe parser per analizzare campi specifici che vengono quindi inseriti direttamente nei serializzatori 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

Questo è usato come:

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

0

Se stai usando ModelViewSet, beh, in realtà hai finito! Gestisce ogni cosa per te! Hai solo bisogno di mettere il campo nel tuo ModelSerializer e impostarecontent-type=multipart/form-data; nel tuo client.

MA come sai non puoi inviare file in formato json. (quando il tipo di contenuto è impostato su application / json nel client). A meno che non utilizzi il formato Base64.

Quindi hai due scelte:

  • lasciare ModelViewSete ModelSerializergestire il lavoro e inviare la richiesta utilizzandocontent-type=multipart/form-data;
  • imposta il campo ModelSerializercome Base64ImageField (or) Base64FileFielde dì al tuo cliente di codificare il file Base64e imposta l'estensionecontent-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')

Invia una richiesta di post a api/filescon un tuo file allegato a un form-datacampo file. Il file verrà caricato nella /mediacartella e verrà aggiunto un record db con ID e nome file.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.