Загрузка файла Django Rest Framework

100

Я использую Django Rest Framework и AngularJs для загрузки файла. Мой файл просмотра выглядит так:

class ProductList(APIView):
    authentication_classes = (authentication.TokenAuthentication,)
    def get(self,request):
        if request.user.is_authenticated(): 
            userCompanyId = request.user.get_profile().companyId
            products = Product.objects.filter(company = userCompanyId)
            serializer = ProductSerializer(products,many=True)
            return Response(serializer.data)

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

Поскольку последняя строка метода post должна возвращать все данные, у меня есть несколько вопросов:

  • как проверить есть ли что request.FILES?
  • как сериализовать поле файла?
  • как мне использовать парсер?
Pawan
источник
13
ПРОСТО ЗАМЕЧАНИЕ К МОДАМ: Django значительно обновился с 2013 года. Так что, если кто-то еще опубликует тот же вопрос сейчас. ПОЖАЛУЙСТА, не сбивайте их ^ _ ^.
Джесси
Как насчет Base64?
Ходжат Модареси,

Ответы:

67

Используйте FileUploadParser , это все в запросе. Вместо этого используйте метод put, вы найдете пример в документации :)

class FileUploadView(views.APIView):
    parser_classes = (FileUploadParser,)

    def put(self, request, filename, format=None):
        file_obj = request.FILES['file']
        # do some stuff with uploaded file
        return Response(status=204)
доволен
источник
12
@pleasedontbelong, почему здесь используется метод PUT вместо POST?
Md. Tanvir Raihan
8
привет @pleasedontbelong, если он создает новую запись, будет ли это POST? и будет ли он работать с FileUploadParser?
chrizonline
1
@pleasedontbelong RTan задает довольно хороший вопрос. Чтение RFC-2616 дает тонкость, о которой я не знал до сих пор. «Принципиальное различие между запросами POST и PUT отражается в различном значении Request-URI. URI в запросе POST идентифицирует ресурс, который будет обрабатывать закрытый объект. Этот ресурс может быть процессом приема данных, шлюзом к какому-то другому протоколу или отдельному объекту, который принимает аннотации. Напротив, URI в запросе PUT идентифицирует объект, заключенный с запросом »
dudeman
3
Почему FileUploadParser? «FileUploadParser предназначен для использования с собственными клиентами, которые могут загружать файл как запрос необработанных данных. Для загрузки через Интернет или для собственных клиентов с поддержкой многостраничной загрузки вместо этого следует использовать синтаксический анализатор MultiPartParser». В целом не кажется хорошим вариантом. Более того, я не вижу, чтобы загрузка файлов требовала особого обращения .
x-
3
Во-вторых, @ x-yuri, DRF жалуется на то, что заголовок Content-Disposition пуст, когда я использую FileUploadParser. MultiPartParser намного проще, поскольку он просто предполагает, что имя файла является заданным именем файла в полях формы.
Дэвид Цварт
74

Я использую тот же стек и также искал пример загрузки файла, но мой случай проще, поскольку я использую ModelViewSet вместо APIView. Ключ оказался хук pre_save. В итоге я использовал его вместе с модулем angular-file-upload вот так:

# Django
class ExperimentViewSet(ModelViewSet):
    queryset = Experiment.objects.all()
    serializer_class = ExperimentSerializer

    def pre_save(self, obj):
        obj.samplesheet = self.request.FILES.get('file')

class Experiment(Model):
    notes = TextField(blank=True)
    samplesheet = FileField(blank=True, default='')
    user = ForeignKey(User, related_name='experiments')

class ExperimentSerializer(ModelSerializer):
    class Meta:
        model = Experiment
        fields = ('id', 'notes', 'samplesheet', 'user')

// AngularJS
controller('UploadExperimentCtrl', function($scope, $upload) {
    $scope.submit = function(files, exp) {
        $upload.upload({
            url: '/api/experiments/' + exp.id + '/',
            method: 'PUT',
            data: {user: exp.user.id},
            file: files[0]
        });
    };
});
Ибендана
источник
11
pre_save устарел в drf 3.x
Guy S
По моему опыту, для полей файлов не требуется специальной обработки .
x-
Методы @ Guy-S, perform_create, perform_update, perform_destroy заменяют методы pre_save, post_save, pre_delete и post_delete старой версии 2.x, которые больше не доступны: django-rest-framework.org/api-guide/generic-views / # методы
Руфат
38

Наконец-то я могу загрузить изображение с помощью Django. Вот мой рабочий код

views.py

class FileUploadView(APIView):
    parser_classes = (FileUploadParser, )

    def post(self, request, format='jpg'):
        up_file = request.FILES['file']
        destination = open('/Users/Username/' + up_file.name, 'wb+')
        for chunk in up_file.chunks():
            destination.write(chunk)
        destination.close()  # File should be closed only after all chuns are added

        # ...
        # do some stuff with uploaded file
        # ...
        return Response(up_file.name, status.HTTP_201_CREATED)

urls.py

urlpatterns = patterns('', 
url(r'^imageUpload', views.FileUploadView.as_view())

curl запрос на загрузку

curl -X POST -S -H -u "admin:password" -F "file=@img.jpg;type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload
Vipul J
источник
14
почему destination.close () помещается внутри цикла for?
makerj
12
Кажется, было бы лучше использовать with open('/Users/Username/' + up_file.name, 'wb+') as destination:и полностью удалить закрытие
Чак Уилбур
Так проще в использовании ModelViewSet. Кроме того, они, скорее всего, реализовали это лучше.
x-
Я полагался на этот ответчик целый день ... пока не обнаружил, что когда вы хотите загрузить несколько файлов, это не FileUploadParserто, что нужно, но MultiPartParser!
Оливье Понс,
13

Потратив на это 1 день, я понял, что ...

Для тех, кому нужно загрузить файл и отправить некоторые данные, не существует прямого способа заставить его работать. Для этого есть открытая проблема в спецификациях json api. Одна из возможностей, которую я видел, - это использовать, multipart/relatedкак показано здесь , но я думаю, что это очень сложно реализовать в drf.

Наконец, я реализовал отправку запроса как formdata. Вы должны отправить каждый файл как файл, а все остальные данные как текст. Теперь для отправки данных в виде текста у вас есть два варианта. случай 1) вы можете отправлять каждые данные как пару значений ключа или случай 2) вы можете иметь один ключ с именем data и отправлять весь json как строку в значении.

Первый метод будет работать из коробки, если у вас есть простые поля, но возникнет проблема, если у вас есть вложенные сериализации. Многокомпонентный синтаксический анализатор не сможет анализировать вложенные поля.

Ниже я предоставляю реализацию для обоих случаев.

Models.py

class Posts(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    caption = models.TextField(max_length=1000)
    media = models.ImageField(blank=True, default="", upload_to="posts/")
    tags = models.ManyToManyField('Tags', related_name='posts')

serializers.py -> никаких особых изменений не требуется, мой сериализатор здесь не показан, поскольку он слишком длинный из-за возможности записи ManyToMany Field.

views.py

class PostsViewset(viewsets.ModelViewSet):
    serializer_class = PostsSerializer
    #parser_classes = (MultipartJsonParser, parsers.JSONParser) use this if you have simple key value pair as data with no nested serializers
    #parser_classes = (parsers.MultipartParser, parsers.JSONParser) use this if you want to parse json in the key value pair data sent
    queryset = Posts.objects.all()
    lookup_field = 'id'

Теперь, если вы следуете первому методу и отправляете только данные, отличные от Json, в виде пар ключ-значение, вам не нужен специальный класс парсера. DRF'd MultipartParser выполнит эту работу. Но для второго случая или если у вас есть вложенные сериализаторы (как я показал), вам понадобится собственный парсер, как показано ниже.

utils.py

from django.http import QueryDict
import json
from rest_framework import parsers

class MultipartJsonParser(parsers.MultiPartParser):

    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = {}

        # for case1 with nested serializers
        # parse each field with json
        for key, value in result.data.items():
            if type(value) != str:
                data[key] = value
                continue
            if '{' in value or "[" in value:
                try:
                    data[key] = json.loads(value)
                except ValueError:
                    data[key] = value
            else:
                data[key] = value

        # for case 2
        # find the data field and parse it
        data = json.loads(result.data["data"])

        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

Этот сериализатор в основном анализирует любое содержимое json в значениях.

Пример запроса в почтовом ящике для обоих случаев: case 1 Случай 1,

Случай 2 case2

Нитин
источник
Я бы предпочел избежать случая 2. Создание одной записи в базе данных для каждого запроса в большинстве случаев подойдет.
x-
очень полезно, большое спасибо. Но я не понимаю, почему вы конвертируете данные dict в QueryDict в парсере? В моем случае в Django нормальные данные словаря работают без конвертации.
Metehan Gülaç,
Я пробовал другой сценарий, используя упомянутый вами ответ, и он успешно работает. вы можете посмотреть мой ответ .
Metehan Gülaç,
если это работает, stackoverflow.com/questions/64547729/… тоже должен работать, но это не так.
sadat
9

По моему опыту, вам не нужно делать ничего особенного с полями файла, вы просто указываете ему использовать поле файла:

from rest_framework import routers, serializers, viewsets

class Photo(django.db.models.Model):
    file = django.db.models.ImageField()

    def __str__(self):
        return self.file.name

class PhotoSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Photo
        fields = ('id', 'file')   # <-- HERE

class PhotoViewSet(viewsets.ModelViewSet):
    queryset = models.Photo.objects.all()
    serializer_class = PhotoSerializer

router = routers.DefaultRouter()
router.register(r'photos', PhotoViewSet)

api_urlpatterns = ([
    url('', include(router.urls)),
], 'api')
urlpatterns += [
    url(r'^api/', include(api_urlpatterns)),
]

и вы готовы загружать файлы:

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

Добавьте -F field=valueдля каждого дополнительного поля вашей модели. И не забудьте добавить аутентификацию.

x-yuri
источник
7

Я решил эту проблему с помощью ModelViewSet и ModelSerializer. Надеюсь, это поможет сообществу.

Я также предпочитаю иметь валидацию и логин Object-> JSON (и наоборот) в самом сериализаторе, а не в представлениях.

Давайте разберемся на примере.

Скажем, я хочу создать FileUploader API. Где он будет хранить такие поля, как id, file_path, file_name, size, owner и т. Д. В базе данных. См. Образец модели ниже:

class FileUploader(models.Model):
    file = models.FileField()
    name = models.CharField(max_length=100) #name is filename without extension
    version = models.IntegerField(default=0)
    upload_date = models.DateTimeField(auto_now=True, db_index=True)
    owner = models.ForeignKey('auth.User', related_name='uploaded_files')
    size = models.IntegerField(default=0)

Теперь для API это то, что я хочу:

  1. ПОЛУЧИТЬ:

Когда я запускаю конечную точку GET, мне нужны все указанные выше поля для каждого загруженного файла.

  1. ПОЧТА:

Но для того, чтобы пользователь мог создать / загрузить файл, почему он должен беспокоиться о передаче всех этих полей. Она может просто загрузить файл, а затем, я полагаю, сериализатор сможет получить остальные поля из загруженного ФАЙЛА.

Searilizer: Вопрос: Я создал сериализатор ниже для моей цели. Но не уверен, что это правильный способ его реализовать.

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 для справки:

class FileUploaderViewSet(viewsets.ModelViewSet):
    serializer_class = FileUploaderSerializer
    parser_classes = (MultiPartParser, FormParser,)

    # overriding default query set
    queryset = LayerFile.objects.all()

    def get_queryset(self, *args, **kwargs):
        qs = super(FileUploaderViewSet, self).get_queryset(*args, **kwargs)
        qs = qs.filter(owner=self.request.user)
        return qs
Джадав Бхеда
источник
Какую логику проверки FileUploaderSerializer.validateсодержит метод?
x-
6

Если кого-то интересует самый простой пример с ModelViewset для Django Rest Framework.

Модель,

class MyModel(models.Model):
    name = models.CharField(db_column='name', max_length=200, blank=False, null=False, unique=True)
    imageUrl = models.FileField(db_column='image_url', blank=True, null=True, upload_to='images/')

    class Meta:
        managed = True
        db_table = 'MyModel'

Сериализатор,

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

И вид,

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

Тест в почтальоне,

введите описание изображения здесь

садат
источник
И как мы могли отправить запрос с помощью ajax. Что такое imageUrl на самом деле?
Эдуард Григорьев
imageUrl - это файл в запросе.
sadat
1
def post(self,request):
        serializer = ProductSerializer(data=request.DATA, files=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
Сайед Файзан
источник
0

В django-rest-framework данные запроса анализируются с помощью Parsers.
http://www.django-rest-framework.org/api-guide/parsers/

По умолчанию django-rest-framework принимает класс парсера JSONParser. Он проанализирует данные в json. поэтому файлы не будут анализироваться с его помощью.
Если мы хотим, чтобы файлы анализировались вместе с другими данными, мы должны использовать один из следующих классов парсеров.

FormParser
MultiPartParser
FileUploadParser
anjaneyulubatta505
источник
На текущей версии ФПИ 3.8.2, он будет разбирать по умолчанию application/json, application/x-www-form-urlencodedи 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

Я хотел бы написать еще один вариант, который, как мне кажется, чище и проще в обслуживании. Мы будем использовать defaultRouter для добавления URL-адресов CRUD для нашего набора представлений, и мы добавим еще один фиксированный URL-адрес, определяющий представление загрузчика в том же наборе представлений.

**** views.py 

from rest_framework import viewsets, serializers
from rest_framework.decorators import action, parser_classes
from rest_framework.parsers import JSONParser, MultiPartParser
from rest_framework.response import Response
from rest_framework_csv.parsers import CSVParser
from posts.models import Post
from posts.serializers import PostSerializer     


class PostsViewSet(viewsets.ModelViewSet):

    queryset = Post.objects.all()
    serializer_class = PostSerializer 
    parser_classes = (JSONParser, MultiPartParser, CSVParser)


    @action(detail=False, methods=['put'], name='Uploader View', parser_classes=[CSVParser],)
    def uploader(self, request, filename, format=None):
        # Parsed data will be returned within the request object by accessing 'data' attr  
        _data = request.data

        return Response(status=204)

Основной urls.py проекта

**** urls.py 

from rest_framework import routers
from posts.views import PostsViewSet


router = routers.DefaultRouter()
router.register(r'posts', PostsViewSet)

urlpatterns = [
    url(r'^posts/uploader/(?P<filename>[^/]+)$', PostsViewSet.as_view({'put': 'uploader'}), name='posts_uploader')
    url(r'^', include(router.urls), name='root-api'),
    url('admin/', admin.site.urls),
]

.- ПРОЧТИ МЕНЯ.

Волшебство происходит, когда мы добавляем декоратор @action к нашему методу класса 'uploader'. Указав аргумент "methods = ['put']", мы разрешаем только запросы PUT; идеально подходит для загрузки файлов.

Я также добавил аргумент "parser_classes", чтобы показать, что вы можете выбрать парсер, который будет анализировать ваш контент. Я добавил CSVParser из пакета rest_framework_csv, чтобы продемонстрировать, как мы можем принимать файлы только определенного типа, если эта функция требуется, в моем случае я принимаю только «Content-Type: text / csv». Примечание. Если вы добавляете собственные синтаксические анализаторы, вам необходимо указать их в parsers_classes в ViewSet, так как запрос будет сравнивать разрешенный media_type с основными (классовыми) синтаксическими анализаторами перед доступом к синтаксическим анализаторам методов загрузчика.

Теперь нам нужно указать Django, как перейти к этому методу и где его можно реализовать в наших URL-адресах. Вот когда мы добавляем фиксированный URL (Простые цели). Этот URL-адрес примет аргумент «имя файла», который будет передан в метод позже. Нам нужно передать этот метод «загрузчик», указав протокол http ('PUT') в списке методу PostsViewSet.as_view.

Когда мы попадаем на следующий URL

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

он будет ожидать запроса PUT с заголовками, указывающими «Content-Type» и Content-Disposition: attachment; filename = "something.csv".

curl -v -u user:pass http://example.com/posts/uploader/ --upload-file ./something.csv --header "Content-type:text/csv"
Вольфганг Леон
источник
Итак, вы предлагаете загрузить файл, а затем прикрепить его к какой-либо записи в БД. Что, если по какой-то причине прикрепления не происходит? Почему бы не сделать это в одном запросе? parser_classesнет ограничений на то, какие файлы могут быть загружены. Это позволяет вам решить, какие форматы можно использовать для отправки запросов. Если подумать, то, как вы обрабатываете загрузку ... кажется, что вы помещаете данные из CSV в базу данных. Не то, что спрашивал ОП.
x-
@ x-yuri, говоря «CSV - это файл», и вопрос в том, что; Как проверить, есть ли в запросе данные? Используя этот метод, вы найдете данные в request.data. _data = request.data due PUT используется. Как вы сказали, классы parser_classes нужны для того, чтобы решить, какие форматы МОГУТ использоваться для запроса, следовательно, используя любой другой формат, который вам НЕ нужен, будут исключены, добавив дополнительный уровень безопасности. Что делать со своими данными - решать вам. Используя «Попробовать, кроме», вы можете проверить, не происходит ли «прикрепление никогда», хотя в этом нет необходимости, это не то, что делает код. Это сделано в 1 запросе
Вольфганг Леон
0

Это один из подходов, которые я применил, надеюсь, он поможет.

     class Model_File_update(APIView):
         parser_classes = (MultiPartParser, FormParser)
         permission_classes = [IsAuthenticated]  # it will check if the user is authenticated or not
         authentication_classes = [JSONWebTokenAuthentication]  # it will authenticate the person by JSON web token

         def put(self, request):
            id = request.GET.get('id')
            obj = Model.objects.get(id=id)
            serializer = Model_Upload_Serializer(obj, data=request.data)
            if serializer.is_valid():
               serializer.save()
               return Response(serializer.data, status=200)
            else:
               return Response(serializer.errors, status=400)
Харшит Триведи
источник
0

Вы можете обобщить ответ @Nithin для работы непосредственно с существующей системой сериализатора DRF, создав класс парсера для анализа определенных полей, которые затем подаются непосредственно в стандартные сериализаторы DRF:

from django.http import QueryDict
import json
from rest_framework import parsers


def gen_MultipartJsonParser(json_fields):
    class MultipartJsonParser(parsers.MultiPartParser):

        def parse(self, stream, media_type=None, parser_context=None):
            result = super().parse(
                stream,
                media_type=media_type,
                parser_context=parser_context
            )
            data = {}
            # find the data field and parse it
            qdict = QueryDict('', mutable=True)
            for json_field in json_fields:
                json_data = result.data.get(json_field, None)
                if not json_data:
                    continue
                data = json.loads(json_data)
                if type(data) == list:
                    for d in data:
                        qdict.update({json_field: d})
                else:
                    qdict.update({json_field: data})

            return parsers.DataAndFiles(qdict, result.files)

    return MultipartJsonParser

Это используется как:

class MyFileViewSet(ModelViewSet):
    parser_classes = [gen_MultipartJsonParser(['tags', 'permissions'])]
    #                                           ^^^^^^^^^^^^^^^^^^^
    #                              Fields that need to be further JSON parsed
    ....
Росс Роджерс
источник
0

Если вы используете ModelViewSet, на самом деле все готово! Он все сделает за вас! Вам просто нужно поместить поле в свой ModelSerializer и установить его content-type=multipart/form-data;в своем клиенте.

НО, как вы знаете, вы не можете отправлять файлы в формате json. (когда в вашем клиенте для типа содержимого установлено значение application / json). Если вы не используете формат Base64.

Итак, у вас есть два варианта:

  • позвольте ModelViewSetи ModelSerializerобработать задание и отправить запрос, используяcontent-type=multipart/form-data;
  • установите поле ModelSerializerкак Base64ImageField (or) Base64FileFieldи сообщите своему клиенту, чтобы он закодировал файл Base64и установилcontent-type=application/json
Ходжат Модареси
источник
0

models.py

from django.db import models

import uuid

class File(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    file = models.FileField(blank=False, null=False)
    
    def __str__(self):
        return self.file.name

serializers.py

from rest_framework import serializers
from .models import File

class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = File
        fields = "__all__"

views.py

from django.shortcuts import render
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status

from .serializers import FileSerializer


class FileUploadView(APIView):
    permission_classes = []
    parser_class = (FileUploadParser,)

    def post(self, request, *args, **kwargs):

      file_serializer = FileSerializer(data=request.data)

      if file_serializer.is_valid():
          file_serializer.save()
          return Response(file_serializer.data, status=status.HTTP_201_CREATED)
      else:
          return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

urls.py

from apps.files import views as FileViews

urlpatterns = [
    path('api/files', FileViews.FileUploadView.as_view()),
]

settings.py

# file uload parameters
MEDIA_URL =  '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

Отправьте запрос на публикацию api/filesс прикрепленным к form-dataполю файлом file. Файл будет загружен в /mediaпапку, и будет добавлена ​​запись базы данных с идентификатором и именем файла.

Ачала Диссанаяке
источник