python HTTPServer 实现文件上传下载

本文介绍如何使用Python的HTTPServer创建一个简单的网页应用,实现在iPad和Windows设备间通过文件表单上传与下载文件。涉及multipart/form-data编码、AJAX异步上传及服务器端接收和处理文件操作。
摘要由CSDN通过智能技术生成

最近用ipad和windows互传文件时没有发现简单的工具,就用python的HTTPServer写个简单的网页应用用于互传文件吧。

上传文件

Client 端

上传文件使用表单。

    <form id="uploadForm" action="/upload" enctype="multipart/form-data" method="post" onsubmit="return submitFile()">
        <div><input type="file" name="file" multiple></div>
        <div><input type="submit" value="upload"> </div>
    </form>

表单 HTML标签enctype属性的含义

  • application/x-www-form-urlencoded:在发送前编码所有字符(默认)

  • multipart/form-data:不对字符编码

  • text/plain:空格转换为"+"号,但不对特殊字符编码。

multipart/form-data是将文件以二进制的形式上传,这样可以实现多种类型的文件上传。

为了发出post 请求时不跳转到post地址,使用 ajax 异步post请求,也就是onsubmit="return submitFile()"的作用。submitFile return false 表示不继续接下来的处理,拦截了默认的跳转逻辑。

submitFile 定义如下。

        function submitFile() {
            files = $('#uploadForm')[0].file.files
            for (i = 0; i < files.length; i++) {
                $.ajax({
                    url: "/upload?fileName=" + encodeURIComponent(files[i].name),
                    type: "POST",
                    data: files[i],
                    success: function (data) {
                        console.info("success", data);
                    },
                    error: function (data) {
                        console.warn("fail", data);
                    },
                    processData: false,
                    contentType: "multipart/form-data"
                });
            }
            return false;
        }

这里直接把File 对象赋给data 来发送二进制数据,每个文件发送post请求上传。
如果直接使用data: new FormData($('#uploadForm')[0]) ,会导致上传后的文件附带一些不期望的数据,post报文示例如下:

POST /xxx HTTP/1.1
Host: hello.app
Connection: keep-alive
Content-Length: 3695
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://hello.app
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIZDrYHwuf2VJdpHw
Referer: http://hello.app/formtest.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

------WebKitFormBoundaryIZDrYHwuf2VJdpHw
Content-Disposition: form-data; name="name"

"hello world"
------WebKitFormBoundaryIZDrYHwuf2VJdpHw
Content-Disposition: form-data; name="file"; filename="temp.png"
Content-Type: image/png

.PNG
.
...
IHDR...
..........Y../..,+|.$aIk.v...G?...P.P,,...m..e.2....v.7.	pHYs...%...%.IR$....|IDAT(.cTT....................:.?.......}.(.Pd`A..V...L...?..#.....4.o..LS.....W.d.?...A8..LS...(.u.......D.b......b.....o&..;..<.1......IEND.B`.
------WebKitFormBoundaryIZDrYHwuf2VJdpHw
Content-Disposition: form-data; name="submit"

submit
------WebKitFormBoundaryIZDrYHwuf2VJdpHw--

根据boundary 定义的随机字符串------WebKitFormBoundaryIZDrYHwuf2VJdpHw-- ,正文被分割为几个部分,每个部分与表单中的内容一一对应。

每部分内容,还会由Content-Disposition: form-data; name="name"这样的字符串指定内容与名字。对于文件内容,有额外的两个字段filename=“temp.png”‘和Content-Type: image/png,文件的内容就直接附加在后面。

Server 端

    def do_POST(self):
        self.send_response(200)
        self.end_headers()

        self.parse_query()
        remainbytes = int(self.headers['content-length'])

        fileName = self.queries['fileName'][0]
        if not fileName:
            print("fail to find fn")
            return (False, "Can't find out file name...")

        print(fileName)
        try:
            out = open(fileName, 'wb')
        except IOError:
            return (False, "Can't create file to write, do you have permission to write?")

        out.write(self.rfile.read(remainbytes))
        print('finish')
        out.close()

    def parse_query(self):
        self.queryString = urllib.parse.unquote(self.path.split('?', 1)[1])
        self.queries = urllib.parse.parse_qs(self.queryString)
        print(self.queries)

下载文件

列举目录文件并拼接到返回的html文件中

    def get_directory(self, path) -> str:
        try:
            list = os.listdir(path)
        except OSError:
            self.send_error(
                HTTPStatus.NOT_FOUND,
                "No permission to list directory")
            return None
        list.sort(key=lambda a: a.lower())
        r = []
        displaypath = os.path.abspath(path)
        title = 'Directory listing for %s' % displaypath
        r.append('<h1>%s</h1>' % title)

        for name in list:
            fullname = os.path.join(path, name)
            displayname = linkname = name
            # Append / for directories or @ for symbolic links
            if os.path.isdir(fullname):
                displayname = name + "/"
                linkname = name + "/"
            if os.path.islink(fullname):
                displayname = name + "@"
                # Note: a link to a directory displays with @ and links with /
            r.append('<li><a href="%s">%s</a></li>' % (linkname, displayname))
        # print(''.join(r))
        return ''.join(r)

完整代码

index.html

<!DOCTYPE html>
<html>

<head>
    <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.6.0.js" type="text/javascript"></script>
    <!-- <script src="./jquery-3.6.0.js" type="text/javascript"></script> -->
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
</head>

<body>
    <form id="uploadForm" action="/upload" enctype="multipart/form-data" method="post" onsubmit="return submitFile()">
        <div><input type="file" name="file" multiple></div>
        <br>
        <div><input type="submit" value="upload"> </div>
    </form>
    <script>
        function submitFile() {
            // formData = new FormData($('#uploadForm')[0])
            files = $('#uploadForm')[0].file.files
            for (i = 0; i < files.length; i++) {
                $.ajax({
                    url: "/upload?fileName=" + encodeURIComponent(files[i].name),
                    type: "POST",
                    data: files[i],
                    success: function (data) {
                        console.info("success", data);
                    },
                    error: function (data) {
                        console.warn("fail", data);
                    },
                    processData: false,
                    contentType: "multipart/form-data",
                    // contentType: "application/octet-stream"
                });
            }
            return false;
        }
    </script>
</body>

</html>

server.py

from http.server import HTTPServer, BaseHTTPRequestHandler
import os
import urllib
from http import HTTPStatus

# ip, port config
host = ('192.168.0.108', 8888)


class Resquest(BaseHTTPRequestHandler):
    def do_GET(self):
        print(self.path)
        if self.path == '/':
            self.send_response(200)
            self.end_headers()
            f = open("index.html", 'r')
            content = f.read()
            content = content.replace(
                '</body>', self.get_directory('.') + '</body>')
            # 里面需要传入二进制数据,用encode()函数转换为二进制数据
            self.wfile.write(content.encode())
        else:
            try:
                path = urllib.parse.unquote(self.path[1:])
                f = open(path, 'rb')
                self.send_response(200)
                self.end_headers()
                self.wfile.write(f.read())
            except FileNotFoundError:
                self.send_response(404)
                self.end_headers()
                self.wfile.write(b'<h1>File Not Found</h1>')

    def do_POST(self):
        self.send_response(200)
        self.end_headers()

        self.parse_query()
        remainbytes = int(self.headers['content-length'])

        fileName = self.queries['fileName'][0]
        if not fileName:
            print("fail to find fn")
            return (False, "Can't find out file name...")

        print(fileName)
        try:
            out = open(fileName, 'wb')
        except IOError:
            return (False, "Can't create file to write, do you have permission to write?")

        out.write(self.rfile.read(remainbytes))
        print('finish')
        out.close()

    def parse_query(self):
        self.queryString = urllib.parse.unquote(self.path.split('?', 1)[1])
        self.queries = urllib.parse.parse_qs(self.queryString)
        print(self.queries)

    def get_directory(self, path) -> str:
        try:
            list = os.listdir(path)
        except OSError:
            self.send_error(
                HTTPStatus.NOT_FOUND,
                "No permission to list directory")
            return None
        list.sort(key=lambda a: a.lower())
        r = []
        displaypath = os.path.abspath(path)
        title = 'Directory listing for %s' % displaypath
        r.append('<h1>%s</h1>' % title)

        for name in list:
            fullname = os.path.join(path, name)
            displayname = linkname = name
            # Append / for directories or @ for symbolic links
            if os.path.isdir(fullname):
                displayname = name + "/"
                linkname = name + "/"
            if os.path.islink(fullname):
                displayname = name + "@"
                # Note: a link to a directory displays with @ and links with /
            r.append('<li><a href="%s">%s</a></li>' % (linkname, displayname))
        # print(''.join(r))
        return ''.join(r)


if __name__ == '__main__':
    server = HTTPServer(host, Resquest)
    print("Starting server, listen at: %s:%s" % host)
    server.serve_forever()

参考资料

[1]: 为什么上传文件的表单需要设置enctype=“multipart/form-data”
[2]: 发送二进制数据
[3]: WebKitFormBoundary的解决办法

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值