如何在 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 请求创建(通过上传)文件。我想创建一个TemporaryUploadedFile
或InMemoryUploadedFile
,然后我可以将它们传递给我现有的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_data
或request.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
检索上传处理程序(默认为MemoryFileUploadHandler
和TemporaryFileUploadHandler
)
确定请求的内容长度(在 HttpRequest
或 MultiPartParser
中搜索 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
参数是干什么用的,来源也没有给出太多线索。
下面发生的事情实际上比看起来要简单得多。您询问每个处理程序是否会处理原始输入。作为上述状态的作者,默认情况下您有MemoryFileUploadHandler
和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 谢谢,听起来好像可以。等我回来工作我会试一试,我会报告结果。 它就像一个魅力,我会在一天结束时将它编辑到你的答案中。 应要求,我已将我的代码编辑为您的答案。如果您有任何改进,请随时对其进行编辑。我不想用您的作品来回答我自己的问题,因此是编辑而不是帖子。 @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 请求处理文件上传?的主要内容,如果未能解决你的问题,请参考以下文章