Как мне обработать загрузку файла с помощью запроса PUT в Django?

#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 класса:

  1. Извлеките обработчики загрузки из request.upload_handlers (которые по умолчанию будут MemoryFileUploadHandler amp; TemporaryFileUploadHandler )
  2. Определите длину содержимого запроса (выполните поиск по Content-Length в HttpRequest или MultiPartParser , чтобы увидеть правильный способ сделать это.)
  3. Определите имя файла загружаемого файла, либо разрешив клиенту указать это, используя последнюю часть пути URL-адреса, либо разрешив клиенту указать это в части «filename=» Content-Disposition заголовка.
  4. Для каждого обработчика вызывайте handler.new_file соответствующие аргументы (имитируя имя поля)
  5. Прочитайте тело запроса по частям, используя request.read() и вызывая handler.receive_data_chunk() для каждого фрагмента.
  6. Для каждого вызова обработчика 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)