如何在 Django 中通过 PUT 请求处理文件上传?

Posted

技术标签:

【中文标题】如何在 Django 中通过 PUT 请求处理文件上传?【英文标题】:How do I handle file upload via PUT request in Django? 【发布时间】:2011-08-09 13:45:26 【问题描述】:

我正在实现一个 REST 风格的界面,并希望能够通过 HTTP PUT 请求创建(通过上传)文件。我想创建一个TemporaryUploadedFileInMemoryUploadedFile,然后我可以将它们传递给我现有的FileField.save() 作为模型的一部分,从而存储文件。

我不太确定如何处理文件上传部分。具体来说,这是一个 put 请求,我无权访问 request.FILES,因为它不存在于 PUT 请求中。

所以,一些问题:

我能否利用HttpRequest 类中的现有功能,特别是处理文件上传的部分?我知道直接PUT 不是多部分 MIME 请求,所以我不这么认为,但值得一问。 如何推断发送内容的 MIME 类型?如果我没看错的话,PUT 主体就是没有前奏的文件。因此,我是否要求用户在其标题中指定 MIME 类型? 如何将此扩展到大量数据?我不想把它全部读入内存,因为那是非常低效的。理想情况下,我会做 TemporaryUploadFile 和相关代码所做的事情 - 一次写一部分?

我查看了this code sample,它诱使 Django 将PUT 作为POST 请求处理。如果我做对了,它只会处理表单编码数据。这是 REST,因此最好的解决方案是不假设存在表单编码数据。不过,我很高兴听到有关以某种方式使用 mime(而不是 multipart)的适当建议(但上传应该只包含一个文件)。

Django 1.3 是可以接受的。所以我可以用request.raw_post_datarequest.read() 做一些事情(或者其他更好的访问方法)。有什么想法吗?

【问题讨论】:

【参考方案1】:

Django 1.3 是可以接受的。所以我可以 要么做点什么 request.raw_post_data 或 request.read() (或者一些 其他更好的访问方法)。任何 想法?

您不想接触request.raw_post_data - 这意味着将整个请求正文读入内存,如果您正在谈论文件上传可能会非常大,所以request.read() 是要走的路.您也可以使用 Django HttpRequest 中挖掘以找出使用私有接口的正确方法,然后确保您的代码也将与 Django 兼容是一个真正的拖累>= 1.3。

我建议你要做的是复制existing file upload behaviour parts of the MultiPartParser class:

    request.upload_handlers 检索上传处理程序(默认为MemoryFileUploadHandlerTemporaryFileUploadHandler) 确定请求的内容长度(在 HttpRequestMultiPartParser 中搜索 Content-Length 以查看执行此操作的正确方法。) 确定上传文件的文件名,方法是让客户端使用 url 的最后一个路径部分指定它,或者让客户端在 the Content-Disposition header 的“filename=”部分指定它。 对于每个处理程序,使用相关参数调用 handler.new_file(模拟字段名称) 使用request.read()分块读取请求正文,并为每个块调用handler.receive_data_chunk()。 对于每个处理程序调用handler.file_complete(),如果它返回一个值,那就是上传的文件。

我怎样才能推断出什么的 mime 类型 正在发送?如果我做对了,一个 PUT 正文只是没有的文件 序幕。因此,我是否要求 用户指定 MIME 类型 他们的标题?

要么让客户端在 Content-Type 标头中指定,要么使用python's mimetype module 猜测媒体类型。

我很想知道你是如何处理这件事的——这是我一直想要调查自己的事情,如果你能发表评论让我知道进展如何,那就太好了!


按要求由 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参数是干什么用的,来源也没有给出太多线索。

下面发生的事情实际上比看起来要简单得多。您询问每个处理程序是否会处理原始输入。作为上述状态的作者,默认情况下您有MemoryFileUploadHandlerTemporaryFileUploadHandler。好吧,事实证明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 谢谢,听起来好像可以。等我回来工作我会试一试,我会报告结果。 它就像一个魅力,我会在一天结束时将它编辑到你的答案中。 应要求,我已将我的代码编辑为您的答案。如果您有任何改进,请随时对其进行编辑。我不想用您的作品来回答我自己的问题,因此是编辑而不是帖子。 @Ninefingers 感谢您的示例代码。我正在尝试通过 PUT 与 Django 进行上传。我真的不明白客户端如何提供文件名 - PUT 是否会类似于 /upload/SOMEFILENAME.EXT 并且您将使用 file_name = path.split("/")[-1:] 获得此文件名[0] ?您在代码中开始对此块发表评论,但我认为它还没有完成。我假设您没有通过 Content-Disposition 标头的“filename =”部分传递它,对吗?非常感谢您的帮助。 @n.evermind 是的,很抱歉。在我的代码中,我使用的是基于 REST 的 API,因此文件名是 URL 的一部分 - 例如我会 PUT 到/path/to/file.txt。因此,我不需要在 HTTP 标头中指定文件名。但是,如果您正在执行 PUT 以说 /files/upload 您会 - 您可以使用 request.META.get("Content-Disposition", None) 获取我认为的内容配置,然后搜索 filename=(P?<name>\.*) 作为正则表达式 - 结果应该是命名匹配。这不是我的想法,可能需要一些调整 - 希望有帮助。【参考方案2】:

感谢https://gist.github.com/g00fy-/1161423,较新的 Django 版本可以更轻松地处理这个问题

我像这样修改了给定的解决方案:

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() 的调用。

【讨论】:

【参考方案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)

【讨论】:

以上是关于如何在 Django 中通过 PUT 请求处理文件上传?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 django 中通过电子邮件发送链接?

如何在 django 模板中通过模型名获取模型

如何在:Django 中通过 URL 传递带空格的变量

django -- 视图

如何在 Django 中通过 group by 获得额外的列?

Django:为啥当我在 django 中通过 popen 使用 Ghostscript 时会出现“找不到文件”错误