Carga de archivos de Django Rest Framework

98

Estoy usando Django Rest Framework y AngularJs para cargar un archivo. Mi archivo de vista se ve así:

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 la última línea del método de publicación debería devolver todos los datos, tengo varias preguntas:

  • cómo comprobar si hay algo en request.FILES ?
  • ¿Cómo serializar el campo del archivo?
  • ¿Cómo debo usar el analizador?
Pawan
fuente
8
SOLO UNA NOTA PARA LOS MODS: Django se ha actualizado enormemente desde 2013. Entonces, si alguien más publica la misma pregunta ahora. POR FAVOR, no los derribe ^ _ ^.
Jessi
¿Qué tal Base64?
Hojat Modaresi

Respuestas:

67

Utilice FileUploadParser , todo está en la solicitud. Use un método put en su lugar, encontrará un ejemplo en los documentos :)

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)
complacido
fuente
12
@pleasedontbelong ¿por qué se ha utilizado el método PUT aquí en lugar de POST?
Md. Tanvir Raihan
8
hola @pleasedontbelong, si está creando un nuevo registro, ¿sería POST en su lugar? y ¿seguirá funcionando con FileUploadParser?
nuttynibbles
1
@pleasedontbelong RTan hace una muy buena pregunta. Leer RFC-2616 proporciona una sutileza que no conocía hasta ahora. "La diferencia fundamental entre las solicitudes POST y PUT se refleja en el significado diferente de Request-URI. El URI en una solicitud POST identifica el recurso que manejará la entidad adjunta. Ese recurso podría ser un proceso de aceptación de datos, una puerta de enlace a algún otro protocolo, o una entidad separada que acepta anotaciones. En contraste, el URI en una solicitud PUT identifica la entidad adjunta a la solicitud "
dudeman
2
¿Por qué FileUploadParser? "FileUploadParser es para uso con clientes nativos que pueden cargar el archivo como una solicitud de datos sin procesar. Para cargas basadas en web, o para clientes nativos con soporte de carga multiparte, debe usar el analizador MultiPartParser en su lugar". No parece una buena opción en general. Además, no veo que las cargas de archivos necesiten ningún tratamiento en particular .
x-yuri
3
Para el segundo @ x-yuri, DRF se queja de que el encabezado Content-Disposition está vacío cuando uso FileUploadParser. MultiPartParser es mucho más simple, ya que simplemente asume que el nombre de archivo es el nombre de archivo dado en los campos del formulario.
David Zwart
74

Estoy usando la misma pila y también estaba buscando un ejemplo de carga de archivos, pero mi caso es más simple ya que uso ModelViewSet en lugar de APIView. La clave resultó ser el gancho pre_save. Terminé usándolo junto con el módulo de carga de archivos angulares así:

# 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
fuente
11
pre_save está obsoleto en drf 3.x
Guy S
Desde mi experiencia, no se necesita un tratamiento especial para los campos de archivo.
x-yuri
Los métodos @ Guy-S, perform_create, perform_update, perform_destroy reemplazan los métodos pre_save, post_save, pre_delete y post_delete de la versión anterior 2.x, que ya no están disponibles: django-rest-framework.org/api-guide/generic-views / # métodos
Rufat
37

Finalmente puedo subir una imagen usando Django. Aquí está mi código de trabajo

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

solicitud curl para cargar

curl -X POST -S -H -u "admin:password" -F "[email protected];type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload
Vipul J
fuente
14
¿Por qué destination.close () se coloca dentro del bucle for?
makerj
12
Parece que sería mejor usar with open('/Users/Username/' + up_file.name, 'wb+') as destination:y eliminar el cierre por completo
Chuck Wilbur
Es más sencillo de usar ModelViewSet. Además, lo más probable es que lo hayan implementado mejor.
x-yuri
He estado confiando en este contestador durante todo el día ... hasta que descubrí que cuando desea cargar varios archivos, no FileUploadParseres necesario, MultiPartParser¡ sino !
Olivier Pons
13

Después de pasar 1 día en esto, descubrí que ...

Para alguien que necesita cargar un archivo y enviar algunos datos, no existe una forma directa de hacer que funcione. Hay un problema abierto en las especificaciones de la api json para esto. Una posibilidad que he visto es usarlo multipart/relatedcomo se muestra aquí , pero creo que es muy difícil implementarlo en drf.

Finalmente lo que había implementado era enviar la solicitud como formdata. Enviaría cada archivo como archivo y todos los demás datos como texto. Ahora, para enviar los datos como texto, tiene dos opciones. caso 1) puede enviar cada dato como par clave-valor o caso 2) puede tener una sola clave llamada datos y enviar todo el json como cadena en valor.

El primer método funcionaría de inmediato si tiene campos simples, pero será un problema si tiene serializaciones anidadas. El analizador multiparte no podrá analizar los campos anidados.

A continuación, proporciono la implementación para ambos 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 -> no se necesitan cambios especiales, no muestra mi serializador aquí porque es demasiado largo debido a la implementación de campo ManyToMany que se puede escribir.

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'

Ahora, si está siguiendo el primer método y solo envía datos que no son de Json como pares clave-valor, no necesita una clase de analizador personalizada. DRF'd MultipartParser hará el trabajo. Pero para el segundo caso o si tiene serializadores anidados (como he mostrado), necesitará un analizador personalizado como se muestra a continuación.

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 básicamente analizaría cualquier contenido json en los valores.

El ejemplo de solicitud en post man para ambos casos: caso 1 caso 1 ,

Caso 2 caso2

Nithin
fuente
Prefiero evitar el caso 2. Crear un registro de base de datos por solicitud debería estar bien la mayor parte del tiempo.
x-yuri
muy útil muchas gracias. Pero no entiendo, ¿por qué está convirtiendo datos dict a QueryDict en el analizador? En mi caso en Django, los datos del diccionario normal funcionan perfectamente sin convertir.
Metehan Gülaç
Probé un escenario diferente usando la respuesta que mencionaste y funciona correctamente. puedes mirar mi respuesta .
Metehan Gülaç
7

Resolví este problema con ModelViewSet y ModelSerializer. Espero que esto ayude a la comunidad.

También prefiero tener validación y Object-> JSON (y viceversa) en el serializador en lugar de en las vistas.

Entendamos con el ejemplo.

Diga, quiero crear FileUploader API. Donde almacenará campos como id, file_path, file_name, size, owner, etc.en la base de datos. Vea el modelo de muestra a continuación:

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)

Ahora, para las API, esto es lo que quiero:

  1. OBTENER:

Cuando disparo el punto final GET, quiero todos los campos anteriores para cada archivo cargado.

  1. ENVIAR:

Pero para que el usuario cree / cargue un archivo, tiene que preocuparse por pasar todos estos campos. Ella puede simplemente cargar el archivo y luego, supongo, el serializador puede obtener el resto de los campos del ARCHIVO cargado.

Searilizer: Pregunta: Creé el siguiente serializador para cumplir mi propósito. Pero no estoy seguro de si es la forma correcta de 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 para referencia:

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
fuente
¿Qué lógica de validación FileUploaderSerializer.validatecontiene el método?
x-yuri
7

Desde mi experiencia, no necesita hacer nada en particular sobre los campos de archivo, solo dígale que use el campo de archivo:

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

y ya está listo para cargar archivos:

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

Agregue -F field=valuepor cada campo adicional que tenga su modelo. Y no olvide agregar autenticación.

x-yuri
fuente
4

Si alguien está interesado en el ejemplo más fácil con ModelViewset para Django Rest Framework.

El modelo es,

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'

El serializador,

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

Y la vista es,

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

Prueba en cartero,

ingrese la descripción de la imagen aquí

sadat
fuente
¿Y cómo podríamos enviar la solicitud usando ajax? ¿Qué es imageUrl en realidad?
Eduard Grigoryev
imageUrl es el archivo de la solicitud.
sábado
0

En django-rest-framework, los datos de la solicitud son analizados por Parsers.
http://www.django-rest-framework.org/api-guide/parsers/

De forma predeterminada, django-rest-framework toma la clase de analizador JSONParser. Analizará los datos en json. por lo tanto, los archivos no se analizarán con él.
Si queremos que los archivos se analicen junto con otros datos, debemos usar una de las clases de analizador siguientes.

FormParser
MultiPartParser
FileUploadParser
anjaneyulubatta505
fuente
En la versión actual del DRF 3.8.2, se analizará de manera predeterminada application/json, application/x-www-form-urlencodedy 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
fuente
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
fuente
0

Me gustaría escribir otra opción que creo que es más limpia y más fácil de mantener. Usaremos el defaultRouter para agregar URL CRUD para nuestro conjunto de vistas y agregaremos una URL fija más especificando la vista del cargador dentro del mismo conjunto de vistas.

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

URL principal del proyecto.

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

La magia sucede cuando agregamos @action decorator a nuestro método de clase 'uploader'. Al especificar el argumento "métodos = ['poner']", solo permitimos solicitudes PUT; perfecto para cargar archivos.

También agregué el argumento "parser_classes" para mostrar que puede seleccionar el analizador que analizará su contenido. Agregué CSVParser del paquete rest_framework_csv, para demostrar cómo podemos aceptar solo cierto tipo de archivos si se requiere esta funcionalidad, en mi caso solo acepto "Content-Type: text / csv". Nota: Si está agregando analizadores personalizados, deberá especificarlos en parsers_classes en ViewSet debido a que la solicitud comparará el media_type permitido con los analizadores principales (clase) antes de acceder a los analizadores del método de carga.

Ahora necesitamos decirle a Django cómo ir a este método y dónde se puede implementar en nuestras URL. Ahí es cuando agregamos la URL fija (propósitos simples). Esta URL tomará un argumento de "nombre de archivo" que se pasará en el método más adelante. Necesitamos pasar este método "uploader", especificando el protocolo http ('PUT') en una lista al método PostsViewSet.as_view.

Cuando aterrizamos en la siguiente URL

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

esperará una solicitud PUT con encabezados que especifiquen "Content-Type" y Content-Disposition: adjunto; nombre de archivo = "algo.csv".

curl -v -u user:pass http://example.com/posts/uploader/ --upload-file ./something.csv --header "Content-type:text/csv"
Wolfgang Leon
fuente
Por lo que sugiere cargar un archivo y luego adjuntarlo a algún registro de base de datos. ¿Qué pasa si adjuntar nunca ocurre por alguna razón? ¿Por qué no hacerlo en una sola solicitud? parser_classesno está ahí para limitar qué archivos se pueden cargar. Le permite decidir qué formatos se pueden usar para realizar solicitudes. Pensándolo bien, la forma en que maneja la carga ... parece que está poniendo datos de CSV en la base de datos. No es lo que pidió OP.
x-yuri
@ x-yuri diciendo "un CSV es un archivo" y la pregunta es; ¿Cómo comprobar si hay datos en la solicitud? Al usar este método, encontrará los datos en request.data. _data = request.data due PUT está siendo utilizado. Como dijiste, parser_classes está ahí para decidir qué formatos PUEDEN usarse para realizar solicitudes, por lo tanto, al usar cualquier otro formato que NO desees, se excluirá agregando una capa adicional de seguridad. Lo que hagas con tus datos depende de ti. Usando "Try Except" puede verificar si "nunca se adjunta" aunque no es necesario, eso no es lo que hace el código. Estos se hacen en 1 solicitud
Wolfgang Leon
0

Este es uno de los enfoques que he aplicado, espero que ayude.

     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
fuente
0

Puede generalizar la respuesta de @ Nithin para que funcione directamente con el sistema de serializador existente de DRF generando una clase de analizador para analizar campos específicos que luego se introducen directamente en los serializadores DRF estándar:

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

Esto se usa como:

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

Si está utilizando ModelViewSet, ¡ya ha terminado! ¡Se encarga de todo por ti! Solo necesita poner el campo en su ModelSerializer y configurarlo content-type=multipart/form-data;en su cliente.

PERO, como sabes, no puedes enviar archivos en formato json. (cuando el tipo de contenido se establece en application / json en su cliente). A menos que use el formato Base64.

Entonces tienes dos opciones:

  • dejar ModelViewSety ModelSerializermanejar el trabajo y enviar la solicitud usandocontent-type=multipart/form-data;
  • establezca el campo ModelSerializercomo Base64ImageField (or) Base64FileFieldy dígale a su cliente que codifique el archivo Base64y establezca elcontent-type=application/json
Hojat Modaresi
fuente
0

modelos.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')

Envíe una solicitud de publicación a api/filescon su archivo adjunto a un form-datacampo file. El archivo se cargará en la /mediacarpeta y se agregará un registro de base de datos con la identificación y el nombre del archivo.

Achala Dissanayake
fuente