python HTTPServer 实现文件上传下载

文章目录

    • 上传文件
      • Client 端
      • Server 端
    • 下载文件
    • 完整代码
    • 参考资料

最近用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('

%s

'
% 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('
  • %s
  • '
    % (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>
        
        <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(
                    '', self.get_directory('.') + '')
                # 里面需要传入二进制数据,用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'

    File Not Found

    '
    ) 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('

    %s

    '
    % 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('
  • %s
  • '
    % (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的解决办法

    你可能感兴趣的:(Software,javascript,python,HttpServer)