http文件服务器

原代码地址:GitHub仓库:JevenM

本代码删除了原仓库中的一些个人用不到的功能(生成目录树?),另外稍微给页面加了一点格式,美化了一点点。

运行步骤:

  • 查看帮助信息:
$ python httpServer.py -h
usage: httpServer.py [-h] [-p PORT] [--path PATH] [--bind ADDRESS]

options:
  -h, --help            show this help message and exit
  -p PORT, --port PORT  Specify alternate port [default: 8000]
  --path PATH           Specify the folder path to share
  --bind ADDRESS, -b ADDRESS
                        Specify alternate bind address [default: "0.0.0.0"]
  • 打开Terminal/CMD窗口,进入要共享的文件目录,注意使用 python3 运行代码:
python httpServer.py [port]
  • 然后在局域网内访问:ip:port 就可查看和上传下载文件了。(想要让别的设备在局域网内访问此网站,需要在防火墙设置入站规则,将 TCP 协议的 port 加入入站规则中)

代码:

httpServer.py

#!/usr/bin/python
# -*- coding: UTF-8 -*-
# 本地测试启动 python simple_httpServer.py 8000
# 忽略挂断信号 , 默认端口 8000
# nohup python3 httpServer.py >> ../HTTP_SERVER.log 2>&1 &

__version__ = "0.3.0"
__all__ = ["MyHTTPRequestHandler"]

import argparse
import os
import posixpath
import time

try:
	from html import escape
except ImportError:
	from cgi import escape
import shutil
import mimetypes
import re
import signal
from io import BytesIO
import codecs

from urllib.parse import quote
from urllib.parse import unquote
from http.server import ThreadingHTTPServer
from http.server import BaseHTTPRequestHandler


class MyHTTPRequestHandler(BaseHTTPRequestHandler):
	global args

	def do_GET(self):
		"""处理 GET 请求的方法"""
		fd = self.send_head()
		if fd:
			shutil.copyfileobj(fd, self.wfile)
			fd.close()

	def do_HEAD(self):
		"""处理请求头"""
		fd = self.send_head()
		if fd:
			fd.close()

	def do_POST(self):
		"""处理 POST 请求的方法"""
		r, info = self.deal_post_data()

		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><br>")
		else:
			f.write(b"<strong>Failed:</strong><br>")
		for i in info:
			f.write(i.encode('utf-8') + b"<br>")
		f.write(b"<br><a href=\"%s\">back</a>" % self.headers['referer'].encode('utf-8'))
		f.write(b"</body>\n</html>\n")
		length = f.tell()
		f.seek(0)
		self.send_response(200)
		self.send_header("Content-type", "text/html;charset=utf-8")
		self.send_header("Content-Length", str(length))
		self.end_headers()
		if f:
			shutil.copyfileobj(f, self.wfile)
			f.close()

	def str_to_chinese(self, var):
		"""
		将url中的字符转换成中文
		"""
		not_end = True
		while not_end:
			start1 = var.find("\\x")
			if start1 > -1:
				str1 = var[start1 + 2:start1 + 4]
				start2 = var[start1 + 4:].find("\\x") + start1 + 4
				if start2 > -1:
					str2 = var[start2 + 2:start2 + 4]
					start3 = var[start2 + 4:].find("\\x") + start2 + 4
					if start3 > -1:
						str3 = var[start3 + 2:start3 + 4]
			else:
				not_end = False
			if start1 > -1 and start2 > -1 and start3 > -1:
				str_all = str1 + str2 + str3
				str_all = codecs.decode(str_all, "hex").decode('utf-8')

				str_re = var[start1:start3 + 4]
				var = var.replace(str_re, str_all)
		return var

	def deal_post_data(self):
		"""
		处理 POST 请求发来的数据,也就是客户端提交上传的文件
		"""
		boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8')
		remain_bytes = int(self.headers['content-length'])

		res = []
		line = self.rfile.readline()
		while boundary in line and str(line, encoding="utf-8")[-4:] != "--\r\n":
			remain_bytes -= len(line)
			if boundary not in line:
				return False, "Content NOT begin with boundary"
			line = self.rfile.readline()
			remain_bytes -= len(line)
			# 查找上传的文件
			fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', str(line))
			if not fn:
				return False, "Can't find out file name..."
			path = translate_path(self.path)
			fname = fn[0]
			fname = self.str_to_chinese(fname)
			fn = os.path.join(path, fname)
			# 如果本地有同名文件如 test.txt,会将用户上传的文件重命名为:test_1.txt
			while os.path.exists(fn):
				try:
					name, suffix = fn.rsplit(".", 1)
					fn = name + "_1" + "." + suffix
				except ValueError:
					fn += "_1"
			dirname = os.path.dirname(fn)
			if not os.path.exists(dirname):
				os.makedirs(dirname)
			line = self.rfile.readline()
			remain_bytes -= len(line)
			line = self.rfile.readline()
			remain_bytes -= len(line)
			try:
				out = open(fn, 'wb')
			except IOError:
				return False, "Can't create file to write, do you have permission to write?"

			pre_line = self.rfile.readline()
			remain_bytes -= len(pre_line)
			Flag = True
			while remain_bytes > 0:
				line = self.rfile.readline()

				if boundary in line:
					remain_bytes -= len(line)
					pre_line = pre_line[0:-1]
					if pre_line.endswith(b'\r'):
						pre_line = pre_line[0:-1]
					out.write(pre_line)
					out.close()
					res.append(fn)
					Flag = False
					break
				else:
					out.write(pre_line)
					pre_line = line
			if pre_line is not None and Flag == True:
				out.write(pre_line)
				out.close()
				res.append(fn)
		return True, res

	def send_head(self):
		"""Common code for GET and HEAD commands.
		This sends the response code and MIME headers.

		这个方法返回一个文件对象(代表要发送给客户端的数据)或者 None(代表不做任何处理)。如果返回的是文件对象,调用者必须手动关闭这个文件。
		"""
		path = translate_path(self.path)
		if os.path.isdir(path):
			if not self.path.endswith('/'):
				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)
		content_type = 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", content_type)
		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.end_headers()
		return f

	def list_directory(self, path):
		"""用来列出路径下所有的内容,将内容写入文件中,返回这个文件对象
		"""
		try:
			list_dir = os.listdir(path)
		except os.error:
			self.send_error(404, "No permission to list directory")
			return None
		list_dir.sort(key=lambda name: name.lower())
		list_dir.sort(key=lambda name: os.path.isfile(os.path.join(path, name)))

		f = BytesIO()
		display_path = escape(unquote(self.path))
		f.write(b'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<html>\n')
		f.write(b'<head>')
		# 网页自适应手机
		f.write(b'<meta name="viewport" content="width=device-width,initial-scale=0.8, minimum-scale=0.5, maximum-scale=2, user-scalable=yes"/>')
		f.write(b'<title>File Server</title>\n')
		f.write(b'</head>')
		f.write(b"<body>\n<h2>Current path: %s</h2>\n" % display_path.encode('utf-8'))
		f.write(b"<hr>\n")
		# 上传目录表单
		f.write(b"<h3>Directory Upload</h3>\n")
		f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">")
		f.write(b"<input ref=\"input\" webkitdirectory multiple name=\"file\" type=\"file\"/>")
		f.write(b"<input type=\"submit\" value=\"Start Upload\"/></form>\n")
		f.write(b"<hr>\n")
		# 上传文件表单
		f.write(b"<h3>Files Upload</h3>\n")
		f.write(b"<form ENCTYPE=\"multipart/form-data\" method=\"post\">")
		f.write(b"<input ref=\"input\" multiple name=\"file\" type=\"file\"/>")
		f.write(b"<input type=\"submit\" value=\"Start Upload\"/></form>\n")
		f.write(b"<hr>\n")
		# 展示文件列表
		f.write(b"<h3>Files Listing</h3>")
		f.write(b'<table style="width: 100%">')
		f.write(b'<tr><th style="text-align: left">Path</th>')
		f.write(b'<th style="width: 20%; text-align: left">Size</th>')
		f.write(b'<th style="width: 20%; text-align: left">Modify Time</th>')
		f.write(b"</tr>")

		# 如果当前路径不是根目录,则写入一个 ../ 链接,让用户可以返回上一层目录
		if self.path != "/":
			current_url = self.path
			if current_url.endswith('/'):
				current_url = current_url[:-1]
			outer_foler_url = current_url.rsplit('/', 1)[0]
			outer_foler_url = '/' if not outer_foler_url else outer_foler_url
			f.write(
					b'<tr style="height: 30px"><td style="background-color: rgb(0,150,0,0.2)"><a href="%s">%s</a></td></tr>' % (
						quote(outer_foler_url).encode('utf-8'), "../".encode('utf-8')))

		# 列出所有的内容
		for name in list_dir:
			fullname = os.path.join(path, name)
			display_name = linkname = name

			# 如果是文件夹的话
			if os.path.isdir(fullname):
				display_name = linkname = name + '/'
				f.write(
						b'<tr style="height: 30px"><td style="background-color: rgb(0,150,0,0.2)"><a href="%s">%s</a></td></tr>' % (
							quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8')))

			# 如果是链接文件
			elif os.path.islink(fullname):
				linkname = linkname + "/"
				display_name = name + "@"
				# Note: a link to a directory displays with @ and links with /
				f.write(
						b'<tr style="height: 30px; background-color: rgb(150,0,0,0.2)"><td><a href="%s">%s</a></td></tr>' % (
							quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8')))
			else:
				# 其他类型的文件,计算文件大小和修改时间,直接显示出来
				st = os.stat(fullname)
				fsize = st.st_size
				fsize = round(fsize / 1024, 2)
				if fsize < 1024:
					size_info = f"{fsize:<6}KB"
				elif fsize < (1024 * 1024):
					size_info = f"{round(fsize / 1024, 2):<6}MB"
				else:
					size_info = f"{round(fsize / 1024 / 1024, 2):<6}GB"

				fmtime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(st.st_mtime))
				f.write(b'<tr style="height: 30px">')
				f.write(b'<td style="background-color: rgb(150,150,0,0.2)"><a href="%s">%s</a></td>' % (
					quote(linkname).encode('utf-8'), escape(display_name).encode('utf-8')))
				f.write(b'<td><pre>%s</pre></td>' % bytes(size_info, encoding='utf-8'))
				f.write(b'<td><pre>%s</pre></td>' % escape(fmtime).encode('utf-8'))
				f.write(b"</tr>")

		f.write(b"</table>")
		f.write(b"\n<hr>\n</body>\n</html>\n")
		length = f.tell()
		f.seek(0)
		self.send_response(200)
		self.send_header("Content-type", "text/html;charset=utf-8")
		self.send_header("Content-Length", str(length))
		self.end_headers()
		return f

	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 translate_path(path):
	"""将带有 / 的网络 URL 转换成本地文件路径
	"""
	global args
	# abandon query parameters
	path = path.split('?', 1)[0]
	path = path.split('#', 1)[0]
	path = posixpath.normpath(unquote(path))
	words = path.split('/')
	words = filter(None, words)
	path = args.path.replace('"', "")
	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 signal_handler(signal, frame):
	exit()


def get_host_ip():
	"""
	查询本机ip地址
	:return: ip
	"""
	import socket
	s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	s.connect(('8.8.8.8', 80))
	ip = s.getsockname()[0]
	s.close()
	return ip


def main():
	global args
	server_address = (args.bind, args.port)

	signal.signal(signal.SIGINT, signal_handler)
	signal.signal(signal.SIGTERM, signal_handler)

	httpd = ThreadingHTTPServer(server_address, MyHTTPRequestHandler)
	server = httpd.socket.getsockname()
	print(f"Serveing HTTP on: {server[0]}:{server[1]}")
	print(f"Local IP Address: {get_host_ip()}:{args.port}")
	httpd.serve_forever()


if __name__ == '__main__':
	parser = argparse.ArgumentParser()
	parser.add_argument('-p', "--port", action='store', default=8000, type=int,
			help='Specify alternate port [default: 8000]')
	parser.add_argument('--path', action='store', default=os.getcwd(), help='Specify the folder path to share')
	parser.add_argument('--bind', '-b', metavar='ADDRESS', default='0.0.0.0',
			help='Specify alternate bind address [default: "0.0.0.0"]')

	args = parser.parse_args()
	if os.path.isfile(args.path):
		raise NotADirectoryError("--path argument should specify a folder path, not a file.")
	if not os.path.exists(args.path):
		raise FileExistsError("--path argument path not exists.")

	main()

posted @ 2022-09-13 10:50  wztshine  阅读(203)  评论(0编辑  收藏  举报