Python 3.x Tornado:stream\u request\u body输出的mime类型

Python 3.x Tornado:stream\u request\u body输出的mime类型,python-3.x,mime-types,tornado,content-type,Python 3.x,Mime Types,Tornado,Content Type,我正在使用tornado.web.stream\u request\u body(tornado v4.0.1)保存post数据,但似乎保存的文件的mime类型仍然是application/octet stream 我需要知道上传文件的mime类型,这是我的代码片段 @stream_request_body class MainHandler(tornado.web.RequestHandler): def post(self): pass def prepar

我正在使用
tornado.web.stream\u request\u body
(tornado v4.0.1)保存post数据,但似乎保存的文件的mime类型仍然是
application/octet stream

我需要知道上传文件的mime类型,这是我的代码片段

@stream_request_body
class MainHandler(tornado.web.RequestHandler):
    def post(self):
        pass

    def prepare(self):
        self.temp_file = tempfile.NamedTemporaryFile(delete=False)

    def data_received(self, chunk):
        self.temp_file.write(chunk)
额外信息; 使用
curl
--数据二进制
参数正确保存文件

curl-v-XPOST--data binary@example.pdf localhost:8888/

但是浏览器上载带有
多部分/表单数据
或带有参数
-d
的curl不起作用。 当我用文本编辑器查看保存的文件时,我在第一行看到http post参数

-----------------------------192365691191582744435855330
Content-Disposition: form-data; name="upload"; filename="example.mp3"
Content-Type: audio/mpeg

...
unreadable binary data

有什么想法吗?

在stream\u request\u body模式下,您可以获得客户端上传的原始body,而不需要创建self.request.arguments或self.request.files的处理。这是一个多部分/表单数据包装器(不是http头,尽管它们看起来很相似);您需要对此进行解析,以获取文件名和嵌入的数据。我认为标准库的电子邮件包中与mime相关的工具可能是将其保存到文件中后解析它的最佳方法。

更新:我为Python3和Tornado 4.0+创建了一个包,可以与PyPi一起安装:

我知道前面的答案已经被接受了,但我也遇到了同样的问题,我可以为Python 3提供一个完整的模块——我们称之为post_streamer——它可以将任何流解析为任何请求的部分,而不需要使用太多内存

#!/usr/bin/env python3
"""Post data streamer for tornadoweb 4.0"""
import os
import re
import random
import tempfile

class SizeLimitError(Exception):
    pass

class PostDataStreamer:
    """Parse a stream of multpart/form-data.

    Useful for request handlers decorated with tornado.web.stream_request_body"""
    SEP = b"\r\n"
    LSEP = len(SEP)
    PAT_HEADERVALUE = re.compile(r"""([^:]+):\s+([^\s;]+)(.*)""")
    PAT_HEADERPARAMS = re.compile(r""";\s*([^=]+)=\"(.*?)\"(.*)""")

    # Encoding for the header values. Only header name and parameters
    # will be decoded. Streamed data will remain binary.
    # This is required because multipart/form-data headers cannot
    # be parsed without a valid encoding.
    header_encoding = "UTF-8"

    def __init__(self, total, tmpdir=None):
        self.buf = b""
        self.dlen = None
        self.delimiter = None
        self.in_data = False
        self.headers = []
        self.parts = []
        self.total = total
        self.received = 0
        self.tmpdir = tmpdir

    def _get_raw_header(self,data):
        idx = data.find(self.SEP)
        if idx>=0:
            return (data[:idx], data[idx+self.LSEP:])
        else:
            return (None, data)

    def receive(self, chunk):
        self.received += len(chunk)
        self.on_progress()
        self.buf += chunk

        if not self.delimiter:
            self.delimiter, self.buf = self._get_raw_header(self.buf)
            if self.delimiter:
                self.delimiter+=self.SEP
                self.dlen = len(self.delimiter)
            elif len(self.buf)>1000:
                raise Exception("Cannot find multipart delimiter")
            else:
                return

        while True:
            if self.in_data:
                if (len(self.buf)>3*self.dlen):
                    idx = self.buf.find(self.SEP+self.delimiter)
                    if idx>=0:
                        self.feed_part(self.buf[:idx])
                        self.end_part()
                        self.buf = self.buf[idx+len(self.SEP+self.delimiter):]
                        self.in_data = False
                    else:
                        limit = len(self.buf)-2*self.dlen
                        self.feed_part(self.buf[:limit])
                        self.buf = self.buf[limit:]
                        return
                else:
                    return
            if not self.in_data:
                while True:
                    header, self.buf = self._get_raw_header(self.buf)
                    if header==b"":
                        assert(self.delimiter)
                        self.in_data = True
                        self.begin_part(self.headers)
                        self.headers = []
                        break
                    elif header:
                        self.headers.append(self.parse_header(header))
                    else:
                        # Header is None, not enough data yet
                        return

    def parse_header(self,header):
        header = header.decode(self.header_encoding)
        res = self.PAT_HEADERVALUE.match(header)
        if res:
            name,value,tail = res.groups()
            params = {}
            hdr = {"name":name,"value":value,"params":params}
            while True:
                res = self.PAT_HEADERPARAMS.match(tail)
                if not res:
                    break
                fname,fvalue,tail = res.groups()
                params[fname] = fvalue
            return hdr
        else:
            return {"value":header}

    def begin_part(self,headers):
        """Internal method called when a new part is started."""
        self.fout = tempfile.NamedTemporaryFile(
            dir=self.tmpdir, delete=False)
        self.part = {
            "headers":headers,
            "size":0,
            "tmpfile":self.fout
        }
        self.parts.append(self.part)

    def feed_part(self,data):
        """Internal method called when content is added to the current part."""
        self.fout.write(data)
        self.part["size"] += len(data)

    def end_part(self):
        """Internal method called when receiving the current part has finished."""
        # Will not close the file here, so we will be able to read later.
        #self.fout.close()            
        #self.fout.flush() This is not needed because we update part["size"]
        pass

    def finish_receive(self):
        """Call this after the last receive() call.

        You MUST call this before using the parts."""
        if self.in_data:
            idx = self.buf.rfind(self.SEP+self.delimiter[:-2])
            if idx>0:
                self.feed_part(self.buf[:idx])
            self.end_part()

    def release_parts(self):
        """Call this to remove the temporary files."""
        for part in self.parts:
            part["tmpfile"].close()
            os.unlink(part["tmpfile"].name)

    def get_part_payload(self, part):
        """Return the contents of a part.

        Warning: do not use this for big files!"""
        fsource = part["tmpfile"]
        fsource.seek(0)
        return fsource.read()

    def get_part_ct_params(self, part):
        """Get content-disposition parameters.

        If there is no content-disposition header then it returns an
        empty list."""
        for header in part["headers"]:
            if header.get("name","").lower().strip()=="content-disposition":
                return header.get("params",[])
        return []

    def get_part_ct_param(self, part, pname, defval=None):
        """Get parameter for a part.

        @param part: The part
        @param pname: Name of the parameter, case insensitive
        @param defval: Value to return when not found.
        """
        ct_params = self.get_part_ct_params(part)
        for name in ct_params:
            if name.lower().strip()==pname:
                return ct_params[name]
        return defval

    def get_part_name(self, part):
        """Get name of a part.

        When not given, returns None."""
        return self.get_part_ct_param(part, "name", None)

    def get_parts_by_name(self, pname):
        """Get a parts by name.

        @param pname: Name of the part. This is case sensitive!

        Attention! A form may have posted multiple values for the same
        name. So the return value of this method is a list of parts!"""
        res = []
        for part in self.parts:
            name = self.get_part_name(part)
            if name==pname:
                res.append(part)
        return res

    def get_values(self, fnames, size_limit=10*1024):
        """Return a dictionary of values for the given field names.

        @param fnames: A list of field names.
        @param size_limit: Maximum size of the value of a single field.
            If a field's size exceeds this then SizeLimitError is raised.

        Warning: do not use this for big file values.
        Warning: a form may have posted multiple values for a field name.
            This method returns the first available value for that name.
            To get all values, use the get_parts_by_name method.
        Tip: use get_nonfile_names() to get a list of field names
            that are not originally files.
        """
        res = {}
        for fname in fnames:
            parts = self.get_parts_by_name(fname)
            if not parts:
                raise KeyError("No such field: %s"%fname)
            size = parts[0]["size"]
            if size>size_limit:
                raise SizeLimitError("Part size=%s > limit=%s"%(size, limit))
            res[fname] = self.get_part_payload(parts[0])
        return res

    def get_nonfile_names(self):
        """Get a list of part names are originally not files.

        It examines the filename attribute of the content-disposition header.
        Be aware that these fields still may be huge in size."""
        res = []
        for part in self.parts:
            filename = self.get_part_ct_param(part, "filename", None)
            if filename is None:
                name = self.get_part_name(part)
                if name:
                    res.append(name)
        return res

    def examine(self):
        """Debugging method for examining received data."""
        print("============= structure =============")
        for idx,part in enumerate(self.parts):
            print("PART #",idx)
            print("    HEADERS")
            for header in part["headers"]:
                print("        ",repr(header.get("name","")),"=",repr(header.get("value","")))
                params = header.get("params",None)
                if params:
                    for pname in params:
                        print("            ",repr(pname),"=",repr(params[pname]))
            print("    DATA")
            print("        SIZE", part["size"])
            print("        LOCATION",part["tmpfile"].name)
            if part["size"]<80:
                print("        PAYLOAD:",repr(self.get_part_payload(part)))
            else:
                print("        PAYLOAD:","<too long...>")
        print("========== non-file values ==========")
        print(self.get_values(self.get_nonfile_names()))


    def on_progress(self):
        """Override this function to handle progress of receiving data."""
        pass # Received <self.received> of <self.total>
#/usr/bin/env蟒蛇3
“”“TornadowWeb 4.0的Post数据流”“”
导入操作系统
进口稀土
随机输入
导入临时文件
类SizeLimitError(异常):
通过
类PostDataStreamer:
“”“解析多部分/表单数据流。
对于用tornado.web.stream\u请求\u正文“”修饰的请求处理程序非常有用
SEP=b“\r\n”
LSEP=len(九月)
PAT_HEADERVALUE=re.compile(r“”([^:]+):\s+([^\s;]+)(.*))
PAT_HEADERPARAMS=re.compile(r“”;\s*([^=]+)=\“(.*?\”(.*)”)
#标题值的编码。仅标题名称和参数
#将被解码。流式数据将保持二进制。
#这是必需的,因为多部分/表单数据头不能
#无法在没有有效编码的情况下解析。
标题编码=“UTF-8”
def uuu init uuuu(self、total、tmpdir=None):
self.buf=b“”
self.dlen=无
self.delimiter=None
self.in_data=False
self.headers=[]
self.parts=[]
self.total=总计
self.received=0
self.tmpdir=tmpdir
def_获取_原始_标题(自身、数据):
idx=data.find(self.SEP)
如果idx>=0:
返回(数据[:idx],数据[idx+self.LSEP:]
其他:
返回(无,数据)
def接收(自身、区块):
self.received+=len(块)
self.on_progress()
self.buf+=块
如果不是self.delimiter:
self.delimiter,self.buf=self.\u获取\u原始\u头(self.buf)
如果使用self.delimiter:
self.delimiter+=self.SEP
self.dlen=len(self.delimiter)
elif-len(自身buf)>1000:
引发异常(“找不到多部分分隔符”)
其他:
返回
尽管如此:
如果self.in_数据:
如果(len(self.buf)>3*self.dlen):
idx=self.buf.find(self.SEP+self.delimiter)
如果idx>=0:
self.feed_部分(self.buf[:idx])
self.end_part()
self.buf=self.buf[idx+len(self.SEP+self.delimiter):]
self.in_data=False
其他:
极限=len(self.buf)-2*self.dlen
自进给零件(自进给[:极限])
self.buf=self.buf[限制:]
返回
其他:
返回
如果不是self.in_数据:
尽管如此:
标头,self.buf=self.\u获取\u原始\u标头(self.buf)
如果标题==b“”:
断言(self.delimiter)
self.in_data=True
self.begin\u部分(self.headers)
self.headers=[]
打破
elif标题:
self.headers.append(self.parse_头(头))
其他:
#标题为“无”,数据不足
返回
def parse_标头(自身,标头):
header=header.decode(self.header\u编码)
res=self.PAT_HEADERVALUE.match(标题)
如果有的话:
名称、值、尾部=资源组()
参数={}
hdr={“name”:name,“value”:value,“params”:params}
尽管如此:
res=自身。PAT_头参数。匹配(尾)
如果不是,则:
打破
fname,fvalue,tail=res.groups()
params[fname]=fvalue
返回hdr
其他:
返回{“值”:头}
def begin_部件(自身、标题):
“”“启动新部件时调用的内部方法。”“”
self.fout=tempfile.NamedTemporaryFile(
dir=self.tmpdir,delete=False)
self.part={
“标题”:标题,
“大小”:0,
“tmpfile”:self.fout
}
self.parts.append(self.part)
def供给_部件(自身、数据):
“”“将内容添加到当前部分时调用的内部方法。”“”
自寻源写入(数据)
自身零件[“尺寸”]+=len(数据)
def end_部件(自身):
“”“接收当前部件时调用的内部方法已完成。”“”
#不会在此处关闭文件,因此我们可以稍后读取。
#self.fout.close()
#self.fout.flush()这是不需要的,因为我们更新了part[“siz
#!/usr/bin/env python3
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, Application, url, stream_request_body
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from post_streamer import PostDataStreamer
import sys

class MyPostDataStreamer(PostDataStreamer):
    percent = 0

    def on_progress(self):
        """Override this function to handle progress of receiving data."""
        if self.total:
            new_percent = self.received*100//self.total
            if new_percent != self.percent:
                self.percent = new_percent
                print("progress",new_percent)

@stream_request_body 
class StreamHandler(RequestHandler):
    def get(self):
        self.write('''<html><body>
<form method="POST" action="/" enctype="multipart/form-data">
File #1: <input name="file1" type="file"><br>
File #2: <input name="file2" type="file"><br>
File #3: <input name="file3" type="file"><br>
Other field 1: <input name="other1" type="text"><br>
Other field 2: <input name="other2" type="text"><br>
Other field 3: <input name="other3" type="text"><br>
<input type="submit">
</form>
</body></html>''')

    def post(self):
        try:
            #self.fout.close()
            self.ps.finish_receive()
            # Use parts here!
            self.set_header("Content-Type","text/plain")
            oout = sys.stdout
            try:
                sys.stdout = self
                self.ps.examine()
            finally:
                sys.stdout = oout
        finally:
            # Don't forget to release temporary files.
            self.ps.release_parts()

    def prepare(self):
        # TODO: get content length here?
        try:
            total = int(self.request.headers.get("Content-Length","0"))
        except:
            total = 0
        self.ps = MyPostDataStreamer(total) #,tmpdir="/tmp"
        #self.fout = open("raw_received.dat","wb+")

    def data_received(self, chunk):
        #self.fout.write(chunk)
        self.ps.receive(chunk)

def main():
    application = Application([
        url(r"/", StreamHandler),
    ])
    max_buffer_size = 4 * 1024**3 # 4GB
    http_server = HTTPServer(
        application,
        max_buffer_size=max_buffer_size,
    )
    http_server.listen(8888)
    IOLoop.instance().start()

main()