正确格式化multipart / form-data主体

时间:2012-11-22 14:24:25

标签: python http amazon-s3 mime multipartform-data

简介

背景

我正在编写一个脚本,使用RFC 2388中定义的multipart/form-data内容类型上传包含文件的内容。从长远来看,我正在尝试提供一个简单的Python脚本来执行uploads of binary packages for github,这涉及将类似表单的数据发送到Amazon S3。

相关

This question已经询问过如何做到这一点,但到目前为止还没有接受的答案,而且the more useful目前的两个答案都指向these recipes手动构建整个消息。我有点担心这种方法,特别是关于字符集和二进制内容。

还有this question,其currently highest-scoring answer建议MultipartPostHandler模块。但这与我提到的食谱没什么不同,因此我的担忧也适用于那些。

的关注

二进制内容

RFC 2388 Section 4.3明确指出除非另有说明,否则内容应为7位,因此可能需要Content-Transfer-Encoding header。这是否意味着我必须对Base64编码二进制文件内容?或者Content-Transfer-Encoding: 8bit对于任意文件是否足够?或者应该阅读Content-Transfer-Encoding: binary

标题字段的字符集

一般来说,标题字段,特别是filename标题字段,默认情况下仅为ASCII。我希望我的方法能够传递非ASCII文件名。我知道,对于我目前为github上传内容的应用程序,我可能不需要它,因为文件名是在一个单独的字段中给出的。但我希望我的代码可以重用,所以我宁愿以一致的方式编码文件名参数。 RFC 2388 Section 4.4建议RFC 2231中引入的格式,例如filename*=utf-8''t%C3%A4st.txt

我的方法

使用python库

由于multipart/form-data本质上是一种MIME类型,我认为应该可以使用标准python库中的email package来撰写我的帖子。特别是非ASCII头字段的相当复杂的处理是我想委托的。

到目前为止工作

所以我写了下面的代码:

#!/usr/bin/python3.2

import email.charset
import email.generator
import email.header
import email.mime.application
import email.mime.multipart
import email.mime.text
import io
import sys

class FormData(email.mime.multipart.MIMEMultipart):

    def __init__(self):
        email.mime.multipart.MIMEMultipart.__init__(self, 'form-data')

    def setText(self, name, value):
        part = email.mime.text.MIMEText(value, _charset='utf-8')
        part.add_header('Content-Disposition', 'form-data', name=name)
        self.attach(part)
        return part

    def setFile(self, name, value, filename, mimetype=None):
        part = email.mime.application.MIMEApplication(value)
        part.add_header('Content-Disposition', 'form-data',
                        name=name, filename=filename)
        if mimetype is not None:
            part.set_type(mimetype)
        self.attach(part)
        return part

    def http_body(self):
        b = io.BytesIO()
        gen = email.generator.BytesGenerator(b, False, 0)
        gen.flatten(self, False, '\r\n')
        b.write(b'\r\n')
        b = b.getvalue()
        pos = b.find(b'\r\n\r\n')
        assert pos >= 0
        return b[pos + 4:]

fd = FormData()
fd.setText('foo', 'bar')
fd.setText('täst', 'Täst')
fd.setFile('file', b'abcdef'*50, 'Täst.txt')
sys.stdout.buffer.write(fd.http_body())

结果如下:

--===============6469538197104697019==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: form-data; name="foo"

YmFy

--===============6469538197104697019==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: form-data; name*=utf-8''t%C3%A4st

VMOkc3Q=

--===============6469538197104697019==
Content-Type: application/octet-stream
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: form-data; name="file"; filename*=utf-8''T%C3%A4st.txt

YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj
ZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVm
YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj
ZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVm
YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj
ZGVmYWJjZGVmYWJjZGVm

--===============6469538197104697019==--

它确实可以很好地处理标题。二进制文件内容将获得base64编码,这可能是可以避免的,但它应该足够好。令我担心的是两者之间的文本字段。它们也是base64编码的。我认为根据标准,这应该足够好,但我宁愿在那里有明文,以防一些愚蠢的框架必须处理中间级别的数据,并且不知道Base64编码数据。

问题

  • 我可以在文本字段中使用8位数据并仍然符合规范吗?
  • 我是否可以使用电子邮件包将我的文本字段序列化为8位数据而无需额外编码?
  • 如果我必须坚持使用7位编码,我是否可以使用引用的printtable来处理那些编码比base64短的文本部分?
  • 我也可以避免二进制文件内容的base64编码吗?
  • 如果我可以避免,我应该将Content-Transfer-Encoding写为8bit还是binary
  • 如果我必须自己序列化身体,我怎么能单独使用email.header package来格式化标题值?email.utils.encode_rfc2231这样做。)
  • 是否有一些实现已经完成了我想要做的所有事情?

这些问题非常密切相关,可以概括为“你将如何实现这个”。在许多情况下,回答一个问题要么回答,要么废弃另一个问题。所以我希望你同意所有这些帖子都适合。

2 个答案:

答案 0 :(得分:2)

这是一个占位符答案,描述了我在等待某些问题的权威输入时所做的事情。如果它表明这种方法在至少一项设计决策中是错误的或不合适的,我会很乐意接受不同的答案。

Here是我过去根据自己的口味使用的代码。 我做出了以下决定:

  

我可以在文本字段中使用8位数据并仍然符合规范吗?

我决定这样做。至少对于这个应用程序,它确实有效。

  

我是否可以使用电子邮件包将我的文本字段序列化为8位数据而无需额外编码?

我找不到任何方法,所以我正在进行自己的序列化,就像我在此看到的所有other recipes一样。

  

我也可以避免二进制文件内容的base64编码吗?

简单地以二进制文件发送文件内容似乎运行得很好,至少在我的单个应用程序中。

  

如果可以避免,我应该将内容传输编码写为8位还是二进制?

正如RFC 2045 Section 2.8所述,8bit数据在CRLF对之间受到998个八位字节的行长度限制,我认为binary更通用,因此更合适的描述这里。

  

如果我必须自己序列化身体,我怎么能单独使用email.header包来格式化标题值?

正如已编辑到我的问题中,email.utils.encode_rfc2231对此非常有用。我首先尝试使用ascii进行编码,但是在非ascii数据或ascii字符的情况下使用该方法,这些字符在双引号字符串中被禁止。

  

是否有一些实现已经完成了我想要做的所有事情?

不是我知道的。但是,其他实现被邀请采用来自my code的想法。


编辑:

感谢this comment我现在意识到,对于标头使用RFC 2231并非普遍接受:HTML 5 forbids its use的当前草案。它也被视为cause problems in the wild。但由于POST标题并不总是对应于特定的HTML文档(例如,想想Web API),我不确定在这方面我是否也相信该草案。也许正确的方法是提供编码和未编码的名称,RFC 5987 Section 4.2建议的方式。但是RFC用于HTTP头,而multipart / form-data头在技术上是HTTP主体。因此RFC不适用,我不知道任何RFC会明确允许(或甚至鼓励)同时使用两个表单进行多部分/表单数据。

答案 1 :(得分:1)

您可能希望查看指向Send file using POST from a Python script库的Requests问题,该库正在成为http最常用的Python库。 如果您在那里找不到所有需要的功能并决定自己实施,我鼓励您将其贡献给这个项目。