Завантаження файлу Django Rest Framework


98

Я використовую 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 ?
  • як серіалізувати поле файлу?
  • як мені використовувати парсер?

8
ПРОСТО ПРИМІТКА ДО MODS: 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?
nuttynibbles

1
@pleasedontbelong RTan задає досить гарне запитання. Читання RFC-2616 забезпечує тонкощі, про які я досі не знав. "Принципова різниця між запитами POST і PUT відображається в різному значенні запиту-URI. URI у запиті POST визначає ресурс, який буде обробляти закриту сутність. Цей ресурс може бути процесом прийому даних, шлюзом до іншого протоколу або до окремого об'єкта, який приймає анотації. На відміну від цього, URI у запиті PUT ідентифікує об'єкт, укладений із запитом "
dudeman

2
Чому FileUploadParser? "FileUploadParser призначений для використання з власними клієнтами, які можуть завантажувати файл як запит необроблених даних. Для завантаження через Інтернет або для власних клієнтів із підтримкою багаточастинного завантаження слід замість цього використовувати синтаксичний аналізатор MultiPartParser." Взагалі не здається хорошим варіантом. Більше того, я не бачу, що завантаження файлів потребує якогось конкретного лікування .
x-yuri

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-yuri

@ Guy-S, Виконати_створити, Виконати_оновлення, Виконати_деструй замінюють стару версію версії 2.x pre_save, post_save, pre_delete і post_delete, які вже недоступні: django-rest-framework.org/api-guide/generic-views / # методи
Руфат

37

Нарешті, я можу завантажити зображення за допомогою 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 -X POST -S -H -u "admin:password" -F "file=@img.jpg;type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload

14
чому destination.close () розміщується всередині циклу for?
makerj

12
Здається, було б краще використовувати with open('/Users/Username/' + up_file.name, 'wb+') as destination:і повністю видалити закриття
Чак Вілбур,

Це простіше у використанні ModelViewSet. Крім того, вони, швидше за все, реалізували це краще.
x-yuri

Я покладався на цю відповідь цілий день ... поки не виявив, що коли ви хочете завантажити кілька файлів, це не FileUploadParserпотрібно, але MultiPartParser!
Олів’є Понс

13

Провівши 1 день на це, я зрозумів, що ...

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

Нарешті, що я реалізував, це надіслати запит як formdata. Ви б надсилали кожен файл як файл, а всі інші дані - як текст. Тепер для надсилання даних у вигляді тексту у вас є два варіанти. випадок 1) ви можете надіслати кожну інформацію як пару значень ключа або випадок 2) ви можете мати один ключ, який називається даними, і надіслати цілий 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.

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 у значеннях.

Приклад запиту в поштовій службі для обох випадків: випадок 1 випадок 1,

Випадок 2 випадок2


Я волів би уникати випадку 2. Створення одного запису бази даних для кожного запиту повинно бути нормальним більшість часу.
x-yuri

дуже корисно велике спасибі. Але я не розумію, чому ви перетворюєте дані dict в QueryDict в парсер? У моєму випадку в Django звичайні дані словника чудово працюють без перетворення.
Метехан Гюлач

Я спробував інший сценарій, використовуючи відповідь, яку ви згадали, і він працює успішно. Ви можете подивитися на мою відповідь .
Метехан Гюлач,

7

Я вирішив цю проблему за допомогою ModelViewSet та ModelSerializer. Сподіваюся, це допоможе громаді.

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

Давайте зрозуміємо це на прикладі.

Скажімо, я хочу створити API FileUploader. Де він буде зберігати в базі даних такі поля як 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. ПОСТ:

Але для того, щоб користувач міг створювати / завантажувати файл, чому він повинен турбуватися про передачу всіх цих полів. Вона може просто завантажити файл, а потім, я припускаю, серіалізатор може отримати решту полів із завантаженого ФАЙЛУ.

Сеаризатор: Питання: Я створив серіалізатор нижче, щоб задовольнити свою мету. Але не впевнений, чи це правильний спосіб його реалізації.

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)

Перегляд для довідки:

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-yuri

7

З мого досвіду, вам не потрібно робити що-небудь конкретно щодо полів файлів, ви просто скажете йому використовувати поле файлу:

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для кожного додаткового поля, яке має ваша модель. І не забудьте додати аутентифікацію.


4

Якщо когось цікавить найпростіший приклад з ModelViewset for 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 - це файл у запиті.
садат

0

У запиті django-rest-framework дані аналізуються Parsers.
http://www.django-rest-framework.org/api-guide/parsers/

За замовчуванням django-rest-framework приймає клас синтаксичного аналізатора JSONParser. Він проаналізує дані на json. отже, файли не будуть проаналізовані з ним.
Якщо ми хочемо, щоб файли були проаналізовані разом з іншими даними, ми повинні використовувати один із наведених нижче класів синтаксичного аналізатора.

FormParser
MultiPartParser
FileUploadParser

На поточній версії ФПІ 3.8.2, він буде розбирати за замовчуванням application/json, application/x-www-form-urlencodedі multipart/form-data.
ліквідні

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

Я хотів би написати ще один варіант, який, на мою думку, є чистішим і простішим в обслуговуванні. Ми будемо використовувати defaultRouter, щоб додати 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_ з основним (класом) синтаксичними аналізаторами перед тим, як отримати доступ до парсерів методу завантаження.

Тепер нам потрібно сказати Django, як перейти до цього методу та де можна реалізувати в наших URL-адресах. Саме тоді ми додаємо фіксовану URL-адресу (прості цілі). Ця URL-адреса прийме аргумент "ім'я файлу", який буде передано в методі пізніше. Нам потрібно передати цей метод "uploader", вказавши протокол http ('PUT') у списку до методу PostsViewSet.as_view.

Коли ми сідаємо за наступною адресою

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

він буде очікувати запит PUT із заголовками із зазначенням "Content-Type" та Content-Disposition: вкладення; ім'я файлу = "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-yuri

@ x-yuri, сказавши "CSV - це файл", і питання в тому; Як перевірити, чи є дані у запиті? За допомогою цього методу ви знайдете дані в request.data. _data = request.data через 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папку та додано запис db з ідентифікатором та ім'ям файлу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.