#python #django #rest #http-put #django-1.3
#python #django #rest #http-put #django-1.3
Вопрос:
Я реализую интерфейс в стиле REST и хотел бы иметь возможность создавать файлы (через загрузку) с помощью HTTP-запроса PUT. Я хотел бы создать либо TemporaryUploadedFile
, либо a InMemoryUploadedFile
, которые я мог бы затем передать моему существующему FileField
и .save()
объекту, который является частью модели, тем самым сохранив файл.
Я не совсем уверен в том, как обрабатывать часть загрузки файла. В частности, это запрос put, к которому у меня нет доступа, request.FILES
поскольку он не существует в PUT
запросе.
Итак, несколько вопросов:
- Могу ли я использовать существующую функциональность в
HttpRequest
классе, в частности ту часть, которая обрабатывает загрузку файлов? Я знаю, что прямойPUT
не является составным MIME-запросом, поэтому я так не думаю, но спросить стоит. - Как я могу определить mime-тип отправляемого файла? Если я все правильно понял, тело PUT — это просто файл без прелюдии. Поэтому я требую, чтобы пользователь указывал тип mime в своих заголовках?
- Как мне распространить это на большие объемы данных? Я не хочу считывать все это в память, поскольку это крайне неэффективно. В идеале я бы делал то, что делает
TemporaryUploadFile
и связанный с ним код — писал бы его по частям за раз?
Я взглянул на этот пример кода, который заставляет Django обрабатывать его PUT
как POST
запрос. Если я все правильно понял, он будет обрабатывать только данные в кодировке формы. Это REST, поэтому лучшим решением было бы не предполагать, что данные, закодированные в форме, будут существовать. Тем не менее, я рад услышать соответствующий совет по использованию mime (не multipart) каким-либо образом (но загрузка должна содержать только один файл).
Django 1.3 является приемлемым. Таким образом, я могу либо что-то сделать с помощью request.raw_post_data
or request.read()
(или, альтернативно, какой-либо другой лучший метод доступа). Есть идеи?
Ответ №1:
Django 1.3 является приемлемым. Таким образом, я могу либо что-то сделать с request.raw_post_data, либо request.read() (или, альтернативно, какой-либо другой лучший метод доступа). Есть идеи?
Вы не хотите касаться request.raw_post_data
— это подразумевает чтение всего тела запроса в память, которая, если вы говорите о загрузке файлов, может быть очень большой, так что request.read()
вот как поступить. Вы также можете сделать это с Django <= 1.2, но это означает, что нужно покопаться в HttpRequest
, чтобы выяснить, как правильно использовать частные интерфейсы, и это реальная проблема, чтобы затем убедиться, что ваш код также будет совместим с Django > = 1.3.
Я бы предположил, что то, что вы хотите сделать, это повторить существующие части поведения при загрузке файлов MultiPartParser
класса:
- Извлеките обработчики загрузки из
request.upload_handlers
(которые по умолчанию будутMemoryFileUploadHandler
amp;TemporaryFileUploadHandler
) - Определите длину содержимого запроса (выполните поиск по Content-Length в
HttpRequest
илиMultiPartParser
, чтобы увидеть правильный способ сделать это.) - Определите имя файла загружаемого файла, либо разрешив клиенту указать это, используя последнюю часть пути URL-адреса, либо разрешив клиенту указать это в части «filename=»
Content-Disposition
заголовка. - Для каждого обработчика вызывайте
handler.new_file
соответствующие аргументы (имитируя имя поля) - Прочитайте тело запроса по частям, используя
request.read()
и вызываяhandler.receive_data_chunk()
для каждого фрагмента. - Для каждого вызова обработчика
handler.file_complete()
, и если он возвращает значение, это загруженный файл.
Как я могу определить mime-тип отправляемого файла? Если я все правильно понял, тело PUT — это просто файл без прелюдии. Поэтому я требую, чтобы пользователь указывал тип mime в своих заголовках?
Либо позвольте клиенту указать это в заголовке Content-Type, либо используйте модуль mimetype в python, чтобы угадать тип носителя.
Мне было бы интересно узнать, как у вас с этим дела — это то, что я собирался изучить сам, было бы здорово, если бы вы могли прокомментировать, чтобы сообщить мне, как это происходит!
Отредактировано Ninefingers в соответствии с запросом, это то, что я сделал, и полностью основано на приведенном выше и исходном коде django.
upload_handlers = request.upload_handlers
content_type = str(request.META.get('CONTENT_TYPE', ""))
content_length = int(request.META.get('CONTENT_LENGTH', 0))
if content_type == "":
return HttpResponse(status=400)
if content_length == 0:
# both returned 0
return HttpResponse(status=400)
content_type = content_type.split(";")[0].strip()
try:
charset = content_type.split(";")[1].strip()
except IndexError:
charset = ""
# we can get the file name via the path, we don't actually
file_name = path.split("/")[-1:][0]
field_name = file_name
Поскольку я определяю здесь API, кроссбраузерная поддержка не вызывает беспокойства. Что касается моего протокола, неверное предоставление информации является некорректным запросом. Я сомневаюсь, хочу ли я сказать image/jpeg; charset=binary
или я собираюсь разрешить несуществующие кодировки. В любом случае, я действительно устанавливаю настройку Content-Type
как ответственность на стороне клиента.
Аналогично, для моего протокола передается имя файла. Я не уверен, для чего предназначен field_name
параметр, и источник не дал много подсказок.
То, что происходит ниже, на самом деле намного проще, чем кажется. Вы спрашиваете каждый обработчик, будет ли он обрабатывать необработанный ввод. Как утверждает автор вышеупомянутого, у вас есть MemoryFileUploadHandler
amp; TemporaryFileUploadHandler
по умолчанию. Ну, оказывается, MemoryFileUploadHandler
будет, когда его попросят создать new_file
, решать, будет ли он обрабатывать файл или нет (на основе различных настроек). Если он решит, что это произойдет, он выдает исключение, в противном случае он не создаст файл и позволит другому обработчику взять управление на себя.
Я не уверен, какова была цель counters
, но я сохранил ее из исходного кода. Остальное должно быть простым.
counters = [0]*len(upload_handlers)
for handler in upload_handlers:
result = handler.handle_raw_input("",request.META,content_length,"","")
for handler in upload_handlers:
try:
handler.new_file(field_name, file_name,
content_type, content_length, charset)
except StopFutureHandlers:
break
for i, handler in enumerate(upload_handlers):
while True:
chunk = request.read(handler.chunk_size)
if chunk:
handler.receive_data_chunk(chunk, counters[i])
counters[i] = len(chunk)
else:
# no chunk
break
for i, handler in enumerate(upload_handlers):
file_obj = handler.file_complete(counters[i])
if not file_obj:
# some indication this didn't work?
return HttpResponse(status=500)
else:
# handle file obj!
Комментарии:
1. 1 спасибо, звучит так, как будто это может сработать. Я попробую, когда вернусь к работе, и сообщу о результатах.
2. это сработало как по волшебству, я отредактирую это в вашем ответе в конце дня.
3. как и было запрошено, я отредактировал свой код в вашем ответе. Если у вас есть какие-либо улучшения, не стесняйтесь редактировать их в. Я не хотел отвечать на свой собственный вопрос работой, основанной на вашей, отсюда и редактирование, а не публикация.
4. @Ninefingers Спасибо за ваш пример кода. Я пытаюсь разобраться с загрузкой через PUT с помощью Django. Я не совсем понимаю, как клиент предоставляет имя файла — будет ли PUT чем-то вроде /upload/SOMEFILENAME.EXT, и вы получите это имя файла с file_name = path.split(«/»)[-1:][0] ? Вы начали комментировать этот блок в своем коде, но я не думаю, что он закончен. Я предполагаю, что вы не передаете его через «filename =» часть заголовка Content-Disposition, верно? Большое спасибо за вашу помощь.
5. @n.evermind Ах да, извините за это. В своем коде я использовал API на основе REST, так что имя файла является частью URL-адреса — например, я бы ПОМЕСТИЛ в
/path/to/file.txt
. Таким образом, мне не нужно указывать имя файла в заголовках HTTP. Однако, если бы вы выполняли запрос PUT, чтобы сказать, что/files/upload
вы бы так и сделали — вы можете получить расположение содержимого, я думаю, с помощьюrequest.META.get("Content-Disposition", None)
, а затем выполнить поиск по немуfilename=(P?<name>.*)
в качестве регулярного выражения — результатом должно быть совпадение по имени. Это не укладывается у меня в голове и может потребовать некоторой настройки — надеюсь, это поможет.
Ответ №2:
Более новые версии Django позволяют обрабатывать это намного проще благодаря https://gist.github.com/g00fy-/1161423
Я изменил данное решение следующим образом:
if request.content_type.startswith('multipart'):
put, files = request.parse_file_upload(request.META, request)
request.FILES.update(files)
request.PUT = put.dict()
else:
request.PUT = QueryDict(request.body).dict()
чтобы иметь возможность доступа к файлам и другим данным, как в POST. Вы можете удалить вызовы .dict()
, если хотите, чтобы ваши данные были доступны только для чтения.
Комментарии:
1. Самый простой ответ 👌
Ответ №3:
Я столкнулся с этой проблемой во время работы с Django 2.2 и искал что-то, что просто работало для загрузки файла с помощью запроса PUT.
from django.http import QueryDict
from django.http.multipartparser import MultiValueDict
from django.core.files.uploadhandler import (
SkipFile,
StopFutureHandlers,
StopUpload,
)
class PutUploadMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
method = request.META.get("REQUEST_METHOD", "").upper()
if method == "PUT":
self.handle_PUT(request)
return self.get_response(request)
def handle_PUT(self, request):
content_type = str(request.META.get("CONTENT_TYPE", ""))
content_length = int(request.META.get("CONTENT_LENGTH", 0))
file_name = request.path.split("/")[-1:][0]
field_name = file_name
content_type_extra = None
if content_type == "":
return HttpResponse(status=400)
if content_length == 0:
# both returned 0
return HttpResponse(status=400)
content_type = content_type.split(";")[0].strip()
try:
charset = content_type.split(";")[1].strip()
except IndexError:
charset = ""
upload_handlers = request.upload_handlers
for handler in upload_handlers:
result = handler.handle_raw_input(
request.body,
request.META,
content_length,
boundary=None,
encoding=None,
)
counters = [0] * len(upload_handlers)
for handler in upload_handlers:
try:
handler.new_file(
field_name,
file_name,
content_type,
content_length,
charset,
content_type_extra,
)
except StopFutureHandlers:
break
for chunk in request:
for i, handler in enumerate(upload_handlers):
chunk_length = len(chunk)
chunk = handler.receive_data_chunk(chunk, counters[i])
counters[i] = chunk_length
if chunk is None:
# Don't continue if the chunk received by
# the handler is None.
break
for i, handler in enumerate(upload_handlers):
file_obj = handler.file_complete(counters[i])
if file_obj:
# If it returns a file object, then set the files dict.
request.FILES.appendlist(file_name, file_obj)
break
any(handler.upload_complete() for handler in upload_handlers)