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>\n<title>Upload Result Page</title>\n")
        f.write(b"<body>\n<h2>Upload Result Page</h2>\n")
        f.write(b"<hr>\n")
        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>\n</html>\n")
        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'\r'):
                    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>\n<title>Directory listing for %s</title>\n" % displaypath).encode())
        f.write(("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath).encode())
        f.write(b"<hr>\n")
        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>\n")
        f.write(b"<hr>\n<ul>\n")
        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>\n'
                    % (urllib.parse.quote(linkname), cgi.escape(displayname))).encode())
        f.write(b"</ul>\n<hr>\n</body>\n</html>\n")
        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="width: 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM