Загрузка файла Django Rest Framework
Я использую 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
?
- как сериализовать поле файла?
- Как использовать парсер?
Ответы
Ответ 1
Используйте 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)
Ответ 2
Я использую тот же стек и также ищу пример загрузки файла, но мой случай проще, поскольку я использую 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]
});
};
});
Ответ 3
Наконец, я могу загрузить изображение с помощью 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()
# ...
# 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 "[email protected];type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload
Ответ 4
Я решил эту проблему с 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 это то, что я хочу:
- ПОЛУЧИТЬ:
Когда я запускаю конечную точку GET, мне нужны все вышеуказанные поля для каждого загруженного файла.
- СООБЩЕНИЕ:
Но для пользователя, чтобы создать/загрузить файл, почему он должен беспокоиться о пропуске всех этих полей. Она может просто загрузить файл, а затем, я полагаю, сериализатор может получить остальные поля из загруженного файла.
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)
Просмотр для справки:
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
Ответ 5
Потратив на это 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.
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-контент в значениях.
Пример запроса в post man для обоих случаев: case 1
,
Дело 2 ![case2]()
Ответ 6
В django-rest-framework данные запроса анализируются с помощью Parsers
.
http://www.django-rest-framework.org/api-guide/parsers/
По умолчанию django-rest-framework принимает класс парсера JSONParser
. Он будет анализировать данные в json. поэтому файлы не будут разбираться с ним.
Если мы хотим, чтобы файлы обрабатывались вместе с другими данными, мы должны использовать один из нижеперечисленных классов парсера.
FormParser
MultiPartParser
FileUploadParser
Ответ 7
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)
Ответ 8
def post(self,request):
serializer = ProductSerializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
Ответ 9
Я хотел бы написать еще один вариант, который мне кажется более чистым и более легким в обслуживании. Мы будем использовать 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. Указывая аргумент "method = ['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; имя файла = "something.csv".
curl -v -u user:pass http://example.com/posts/uploader/ --upload-file ./something.csv --header "Content-type:text/csv"