最近用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的解决办法