python3 http.server作为服务端,h5作为网页实现手机传输文件到PC

一、背景

把手机上的文件传到PC;
原因:手机存储太小
矛盾:
1.不想安装乱七八糟的软件;
2.不想安装手机助手,不想插USB;
3.不想用社交软件QQ、微信、钉钉等传文件;
那么能不能自己做一个简单工具呢
答案:可以,正好PC有python3环境作为服务端,手机端使用浏览器访问html上传文件。
结果:最终手机端用QQ浏览器尝试成功,一次上传200张图片都没有问题。

二、方法

文件

目录如下

[root@lh uploader]# tree 
└────css
    └────bootstrap.min.css
    └────demo.css
    └────font-awesome.css
    └────style.css
    └────webuploader.css
└────index.html    #主要的网页文件
└────script
    └────jquery-1.8.2.min.js
    └────Uploader.swf
    └────webuploader.custom.js
    └────webuploader.custom.min.js
    └────webuploader.fis.js
    └────webuploader.flashonly.js
    └────webuploader.flashonly.min.js
    └────webuploader.html5only.js
    └────webuploader.html5only.min.js
    └────webuploader.js
    └────webuploader.min.js
    └────webuploader.noimage.js
    └────webuploader.noimage.min.js
    └────webuploader.nolog.js
    └────webuploader.nolog.min.js
    └────webuploader.withoutimage.js
    └────webuploader.withoutimage.min.js
└────simple.py     #作为服务端的python程序

其中css和script中的文件从以下两个地址中下载抽取:

https://codeload.github.com/fex-team/webuploader/zip/refs/tags/0.1.5
https://fontawesome.dashgame.com/assets/font-awesome-4.7.0.zip

服务端

# -*- coding: utf-8 -*-
#!/usr/bin/env python3
"""Simple HTTP Server With Upload.

This module builds on BaseHTTPServer by implementing the standard GET
and HEAD requests in a fairly straightforward manner.

see: https://gist.github.com/UniIsland/3346170
"""
 
 
__version__ = "0.1"
__all__ = ["SimpleHTTPRequestHandler"]
 
import os
import posixpath
import http.server
import urllib.request, urllib.parse, urllib.error
import cgi
import shutil
import mimetypes
import re
from io import BytesIO
 
 
class SimpleHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
 
    """Simple HTTP request handler with GET/HEAD/POST commands.

    This serves files from the current directory and any of its
    subdirectories.  The MIME type for files is determined by
    calling the .guess_type() method. And can reveive file uploaded
    by client.

    The GET/HEAD/POST requests are identical except that the HEAD
    request omits the actual contents of the file.

    """
 
    def _send_cors_headers(self):
        """ Sets headers required for CORS """
        self.send_header('Content-type', 'application/json')
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "*")
        self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type")

    def do_GET(self):
        """Serve a GET request."""
        f = self.send_head()
        if f:
            self.copyfile(f, self.wfile)
            f.close()

    def do_OPTIONS(self):
        self.send_response(200)
        self._send_cors_headers()
        self.end_headers()

    def do_HEAD(self):
        """Serve a HEAD request."""
        f = self.send_head()
        if f:
            f.close()
 
    def do_POST(self):
        """Serve a POST request."""
        r, info = self.deal_post_data()
        print((r, info, "by: ", self.client_address))
        f = BytesIO()
        f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
        f.write(b"<html>
<title>Upload Result Page</title>
")
        f.write(b"<body>
<h2>Upload Result Page</h2>
")
        f.write(b"<hr>
")
        if r:
            f.write(b"<strong>Success:</strong>")
        else:
            f.write(b"<strong>Failed:</strong>")
        f.write(info.encode())
        f.write(("<br><a href="%s">back</a>" % self.headers['referer']).encode())
        f.write(b"<hr><small>Powerd By: bones7456, check new version at ")
        f.write(b"<a href="http://li2z.cn/?s=SimpleHTTPServerWithUpload">")
        f.write(b"here</a>.</small></body>
</html>
")
        length = f.tell()
        f.seek(0)
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(length))
        self.end_headers()
        if f:
            self.copyfile(f, self.wfile)
            f.close()
        
    def deal_post_data(self):
        content_type = self.headers['content-type']
        if not content_type:
            return (False, "Content-Type header doesn't contain boundary")
        boundary = content_type.split("=")[1].encode()
        remainbytes = int(self.headers['content-length'])
        line = self.rfile.readline()
        remainbytes -= len(line)
        if not boundary in line:
            return (False, "Content NOT begin with boundary")
        line = self.rfile.readline()
        remainbytes -= len(line)
        fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode())
        while not fn:
            line = self.rfile.readline()
            remainbytes -= len(line)
            fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode())
            if (remainbytes <= 0):
                return (False, "Can't find out file name...")
        path = self.translate_path(self.path)
        fn = os.path.join(path, fn[0])
        line = self.rfile.readline()
        remainbytes -= len(line)
        line = self.rfile.readline()
        remainbytes -= len(line)
        try:
            out = open(fn, 'wb')
        except IOError:
            return (False, "Can't create file to write, do you have permission to write?")
                
        preline = self.rfile.readline()
        remainbytes -= len(preline)
        while remainbytes > 0:
            line = self.rfile.readline()
            remainbytes -= len(line)
            if boundary in line:
                preline = preline[0:-1]
                if preline.endswith(b'
'):
                    preline = preline[0:-1]
                out.write(preline)
                out.close()
                return (True, "File '%s' upload success!" % fn)
            else:
                out.write(preline)
                preline = line
        return (False, "Unexpect Ends of data.")
 
    def send_head(self):
        """Common code for GET and HEAD commands.

        This sends the response code and MIME headers.

        Return value is either a file object (which has to be copied
        to the outputfile by the caller unless the command was HEAD,
        and must be closed by the caller under all circumstances), or
        None, in which case the caller has nothing further to do.

        """
        path = self.translate_path(self.path)
        f = None
        if os.path.isdir(path):
            if not self.path.endswith('/'):
                # redirect browser - doing basically what apache does
                self.send_response(301)
                self.send_header("Location", self.path + "/")
                self.end_headers()
                return None
            for index in "index.html", "index.htm":
                index = os.path.join(path, index)
                if os.path.exists(index):
                    path = index
                    break
            else:
                return self.list_directory(path)
        ctype = self.guess_type(path)
        try:
            # Always read in binary mode. Opening files in text mode may cause
            # newline translations, making the actual size of the content
            # transmitted *less* than the content-length!
            f = open(path, 'rb')
        except IOError:
            self.send_error(404, "File not found")
            return None
        self.send_response(200)
        self.send_header("Content-type", ctype)
        fs = os.fstat(f.fileno())
        self.send_header("Content-Length", str(fs[6]))
        self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "*")
        self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type")
        self.end_headers()
        return f
 
    def list_directory(self, path):
        """Helper to produce a directory listing (absent index.html).

        Return value is either a file object, or None (indicating an
        error).  In either case, the headers are sent, making the
        interface the same as for send_head().

        """
        try:
            list = os.listdir(path)
        except os.error:
            self.send_error(404, "No permission to list directory")
            return None
        list.sort(key=lambda a: a.lower())
        f = BytesIO()
        displaypath = cgi.escape(urllib.parse.unquote(self.path))
        f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
        f.write(("<html>
<title>Directory listing for %s</title>
" % displaypath).encode())
        f.write(("<body>
<h2>Directory listing for %s</h2>
" % displaypath).encode())
        f.write(b"<hr>
")
        f.write(b"<form ENCTYPE="multipart/form-data" method="post">")
        f.write(b"<input name="file" type="file"/>")
        f.write(b"<input type="submit" value="upload"/></form>
")
        f.write(b"<hr>
<ul>
")
        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 /
            f.write(('<li><a href="%s">%s</a>
'
                    % (urllib.parse.quote(linkname), cgi.escape(displayname))).encode())
        f.write(b"</ul>
<hr>
</body>
</html>
")
        length = f.tell()
        f.seek(0)
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.send_header("Content-Length", str(length))
        self.end_headers()
        return f
 
    def translate_path(self, path):
        """Translate a /-separated PATH to the local filename syntax.

        Components that mean special things to the local file system
        (e.g. drive or directory names) are ignored.  (XXX They should
        probably be diagnosed.)

        """
        # abandon query parameters
        path = path.split('?',1)[0]
        path = path.split('#',1)[0]
        path = posixpath.normpath(urllib.parse.unquote(path))
        words = path.split('/')
        words = [_f for _f in words if _f]
        path = os.getcwd()
        for word in words:
            drive, word = os.path.splitdrive(word)
            head, word = os.path.split(word)
            if word in (os.curdir, os.pardir): continue
            path = os.path.join(path, word)
        return path
 
    def copyfile(self, source, outputfile):
        """Copy all data between two file objects.

        The SOURCE argument is a file object open for reading
        (or anything with a read() method) and the DESTINATION
        argument is a file object open for writing (or
        anything with a write() method).

        The only reason for overriding this would be to change
        the block size or perhaps to replace newlines by CRLF
        -- note however that this the default server uses this
        to copy binary data as well.

        """
        shutil.copyfileobj(source, outputfile)
 
    def guess_type(self, path):
        """Guess the type of a file.

        Argument is a PATH (a filename).

        Return value is a string of the form type/subtype,
        usable for a MIME Content-type header.

        The default implementation looks the file's extension
        up in the table self.extensions_map, using application/octet-stream
        as a default; however it would be permissible (if
        slow) to look inside the data to make a better guess.

        """
 
        base, ext = posixpath.splitext(path)
        if ext in self.extensions_map:
            return self.extensions_map[ext]
        ext = ext.lower()
        if ext in self.extensions_map:
            return self.extensions_map[ext]
        else:
            return self.extensions_map['']
 
    if not mimetypes.inited:
        mimetypes.init() # try to read system mime.types
    extensions_map = mimetypes.types_map.copy()
    extensions_map.update({
        '': 'application/octet-stream', # Default
        '.py': 'text/plain',
        '.c': 'text/plain',
        '.h': 'text/plain',
        })
 
 
def test(HandlerClass = SimpleHTTPRequestHandler,
         ServerClass = http.server.HTTPServer):
    http.server.test(HandlerClass, ServerClass, "HTTP/1.0", 8085)
 
if __name__ == '__main__':
    test()   

执行方法:

python3 simple.py

网页代码

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title>WebUploader文件上传示例</title>
  
  <script type="text/javascript" src="script/jquery-1.8.2.min.js"></script>   
  <script type="text/javascript" src="script/webuploader.min.js"></script>
  <link href="css/webuploader.css" rel="stylesheet" />
  <link href="css/bootstrap.min.css" rel="stylesheet"/>   
  <link href="css/style.css" rel="stylesheet" />
  <link href="css/demo.css" rel="stylesheet" />
  <link href="css/font-awesome.css" rel="stylesheet" /> 
  
  <script type="text/javascript">
    var applicationPath = window.applicationPath === "" ? "" : window.applicationPath || "../../";
    // 文件上传
    jQuery(function () {
        var $ = jQuery,
            $list = $('#fileList'),
            $btn = $('#ctlBtn'),
            state = 'pending',
            uploader;
        uploader = WebUploader.create({
            // 不压缩image
            resize: false, 

            // swf文件路径
            swf: applicationPath + 'Script/webuploader/Uploader.swf',

            // 文件接收服务端。
            server: 'http://192.168.1.123:8085/',

            // 选择文件的按钮。可选。
            // 内部根据当前运行是创建,可能是input元素,也可能是flash.
            pick: {
               id:'#picker',
               multiple:true
            },
            multiple: true

        }); 

        // 当有文件添加进来的时候
        uploader.on('fileQueued', function (file) {

            $list.append('<div id="' + file.id + '" class="item">' +
                '<h4 class="info">' + file.name + '</h4>' +
                '<p class="state">等待上传...</p>' +
            '</div>');

        });

        // 文件上传过程中创建进度条实时显示。
        uploader.on('uploadProgress', function (file, percentage) {

            var $li = $('#' + file.id),
                $percent = $li.find('.progress .progress-bar'); 
            // 避免重复创建
            if (!$percent.length) {
                $percent = $('<div class="progress progress-striped active">' +
                  '<div class="progress-bar" role="progressbar" style=" 0%">' +
                  '</div>' +
                '</div>').appendTo($li).find('.progress-bar');
            }
            $li.find('p.state').text('上传中');
            $percent.css('width', percentage * 100 + '%');

        });

        uploader.on('uploadSuccess', function (file) {
            $('#' + file.id).find('p.state').text('已上传');
        });

        uploader.on('uploadError', function (file) {
            $('#' + file.id).find('p.state').text('上传出错');
        });

        uploader.on('uploadComplete', function (file) {
            $('#' + file.id).find('.progress').fadeOut();
        });
        uploader.on('all', function (type) {
            if (type === 'startUpload') {
                state = 'uploading';
            } else if (type === 'stopUpload') {
                state = 'paused';
            } else if (type === 'uploadFinished') {
                state = 'done';
            }
            if (state === 'uploading') {
                $btn.text('暂停上传');
            } else {
                $btn.text('开始上传');
            }

             });

             $btn.on('click', function () {
                 if (state === 'uploading') {
                     uploader.stop();
                 } else {
                     uploader.upload();
                 }
             });
    });
  </script>
</head>

<body>
  <div  class="container-fluid">
     <div class="col-md-10">
       <div class="row">文件上传示例:</div>
       <div class="row">
         <div id="uploader" class="wu-example">
         <!--用来存放文件信息-->
           <div id="fileList" class="uploader-list"></div>
           <div class="btns">
             <div id="picker" class="btn btn-primary">选择文件</div>
           </div>
         </div>
       </div>
       <div class="row"></div>
       <div class="row"><button id="ctlBtn" class="btn btn-default">开始上传</button></div>
    </div>
    <div>
    </div>
  </div>
</body>
</html>

使用方法
直接访问"http://192.168.1.123:8085/"即可,其中192.168.1.123是PC机的IP。

手机端

很遗憾,大多数手机浏览器不支持多选,只有QQ和Chrome两款手机浏览器支持。
QQ浏览器84MB,Chrome94MB,其中Chrome在应用商店还找不到。
强忍着QQ浏览器恶心的各种垃圾推送完成了测试。

三、问题和解决方法

1.如何修改监听端口?

http.server.test(HandlerClass, ServerClass, "HTTP/1.0", 8085) #修改simple.py这句中的8085

2.如何修改上传目录

#simple.py中把path = self.translate_path(self.path)改成自己的目录即可
path = /home/upload/

3.上传没有反应或报错"Provisional headers are shown"
跨域问题导致的,在index.html中,将如下部分改成自己PC的地址

// 文件接收服务端。
server: 'http://192.168.1.123:8085/',

4.手机上不能多选图片
答:目前支持多选的浏览器只有QQ浏览器和chrome浏览器。

四、参考网址

https://blog.csdn.net/weixin_33595571/article/details/86558658
https://blog.csdn.net/qq_37254866/article/details/84826219
https://www.cnblogs.com/sheqiuluo/p/7061278.html
https://www.zhihu.com/question/24212111
https://blog.csdn.net/yyt593891927/article/details/112025503
https://blog.csdn.net/syc000666/article/details/107846080

转载请注明来源:https://www.cnblogs.com/bugutian/
原文地址:https://www.cnblogs.com/bugutian/p/14884847.html