基于python3+wxpython的简单http 文件服务器
前言
ai时代就是好,只需要提供思路,程序的问题完全可以交给ai,下面全部内容由traecode和deepseek生成。
很多时候,需要跨系统传输文件,如果有多个跳板机的情况下,使用http 文件服务器的形式进行文件传输会方便很多
最简单的方式就是在文件所在目录使用命令行 python3 -m http.server --bind 0.0.0.0 端口号 的方式,快速开启一个http server 后台。
我的需求是,最好有一个界面化的程序,固定在我的windows任务栏,直接点击程序,然后选择需要代理的目录,即可快速实现http 文件服务。在windows环境下,相较上面唤起命令行,输入指令,这样可以节省一些步骤。
下面是具体实现
正文
图标制作程序
generate_icon.py
# -*- coding: utf-8 -*-
"""
生成 HTTP 文件服务器的图标 (server.ico)
包含 16x16 ~ 256x256 多尺寸
需要 Pillow 库
"""
import os
from PIL import Image, ImageDraw, ImageFont
def create_icon():
# 定义所有尺寸
sizes = [16, 24, 32, 48, 64, 128, 256]
images = []
for size in sizes:
# 创建 RGBA 图像(透明背景)
img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# ---------- 绘制圆角矩形背景(深蓝渐变效果)----------
# 因为绘制渐变较复杂,这里用纯色 + 圆角模拟
bg_color = (30, 80, 180, 255) # 深蓝
margin = size // 16
radius = size // 8
# 画圆角矩形(用矩形+四个小圆弧组合,简化用普通矩形)
# 为了美观,我们画一个圆角矩形(利用PIL的rounded_rectangle,需要新版)
try:
draw.rounded_rectangle(
[margin, margin, size - margin, size - margin],
radius=radius,
fill=bg_color
)
except AttributeError:
# 旧版本PIL,用普通矩形代替
draw.rectangle(
[margin, margin, size - margin, size - margin],
fill=bg_color
)
# ---------- 绘制文件夹图标(白色)---------
# 文件夹:一个梯形+矩形组合,简化:矩形加一条横线
# 根据尺寸缩放
f_w = size * 0.7
f_h = size * 0.55
f_x = (size - f_w) // 2
f_y = (size - f_h) // 2 + size * 0.05
# 文件夹主体(白色半透明)
draw.rectangle(
[f_x, f_y, f_x + f_w, f_y + f_h],
fill=(255, 255, 255, 220),
outline=(255, 255, 255, 255)
)
# 文件夹“舌头”
tab_w = f_w * 0.3
tab_h = f_h * 0.15
draw.rectangle(
[f_x, f_y - tab_h, f_x + tab_w, f_y],
fill=(255, 255, 255, 220),
outline=(255, 255, 255, 255)
)
# ---------- 在地球/网络符号(表示互联网)---------
# 绘制一个简易地球:两个弧线 + 竖线
earth_cx = size // 2
earth_cy = size // 2 - size * 0.05
earth_r = size * 0.2
draw.ellipse(
[earth_cx - earth_r, earth_cy - earth_r,
earth_cx + earth_r, earth_cy + earth_r],
outline=(255, 255, 255, 200),
width=max(1, size // 30)
)
# 横向弧线(赤道)
draw.arc(
[earth_cx - earth_r, earth_cy - earth_r,
earth_cx + earth_r, earth_cy + earth_r],
start=0, end=180,
fill=(255, 255, 255, 200),
width=max(1, size // 30)
)
# 纵向弧线(经线)
draw.arc(
[earth_cx - earth_r, earth_cy - earth_r,
earth_cx + earth_r, earth_cy + earth_r],
start=90, end=270,
fill=(255, 255, 255, 200),
width=max(1, size // 30)
)
# 小圆点(代表卫星或连接点)
dot_r = max(1, size // 40)
draw.ellipse(
[earth_cx - dot_r, earth_cy - dot_r,
earth_cx + dot_r, earth_cy + dot_r],
fill=(255, 255, 255, 255)
)
# ---------- 底部添加小文字 "HTTP"(仅大尺寸)----------
if size >= 64:
try:
font = ImageFont.truetype("arial.ttf", size // 10)
except:
font = ImageFont.load_default()
text = "HTTP"
# 获取文本尺寸
bbox = draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
tx = (size - tw) // 2
ty = f_y + f_h + size * 0.05
if ty + th < size:
draw.text((tx, ty), text, font=font, fill=(255, 255, 255, 200))
# 保存该尺寸图像
images.append(img)
# 保存为 ICO 文件(多尺寸)
if images:
# 以第一个图像作为主图像,保存所有尺寸
images[0].save(
'server.ico',
format='ICO',
sizes=[(img.width, img.height) for img in images],
append_images=images[1:]
)
print("✅ 图标已生成:server.ico")
else:
print("❌ 生成失败")
if __name__ == "__main__":
create_icon()
使用方式,运行:python3 generate_icon.py 即可生成一个名字为 server.ico 的图标
主程序以及打包脚本
主程序
wx_http_server_gui.py
# -*- coding: utf-8 -*-
"""
本地 HTTP 文件服务器 GUI 工具
- wxPython 界面
- 支持自定义 IP 绑定(默认 127.0.0.1)
- 自动显示本机可访问 IP
- 配置自动保存
- 可打包为 exe
"""
import os
import sys
import threading
import socket
import webbrowser
import time
import json
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import wx
import wx.richtext as rt
# ------------------ 常量 ------------------
APP_TITLE = "本地 HTTP 文件服务器"
APP_VERSION = "1.3.0"
DEFAULT_PORT = 12123
DEFAULT_HOST = "127.0.0.1"
CONFIG_FILE = "server_gui_config.json"
# ------------------ 日志重定向 ------------------
class RedirectStream:
def __init__(self, callback):
self._callback = callback
self._buffer = ""
def write(self, text):
if not text:
return
self._buffer += text
while "\n" in self._buffer:
line, self._buffer = self._buffer.split("\n", 1)
if line:
self._callback(line)
def flush(self):
if self._buffer:
self._callback(self._buffer)
self._buffer = ""
# ------------------ HTTP 请求处理器 ------------------
class FileServerHandler(SimpleHTTPRequestHandler):
log_callback = None
def __init__(self, *args, directory=None, **kwargs):
self._directory = directory
super().__init__(*args, directory=directory, **kwargs)
def log_message(self, format, *args):
msg = format % args
client = self.address_string()
if self.log_callback:
self.log_callback(f"{client} - {msg}")
def end_headers(self):
self.send_header("Access-Control-Allow-Origin", "*")
super().end_headers()
# ------------------ 主窗口 ------------------
class MainFrame(wx.Frame):
def __init__(self):
super().__init__(None, title=f"{APP_TITLE} v{APP_VERSION}", size=(820, 560))
self.httpd = None
self.server_thread = None
self.server_lock = threading.Lock()
self.config = self._load_config()
self._init_ui()
self._bind_events()
self._apply_config()
self._update_button_state(running=False)
# 设置窗口图标(支持打包后加载)
self.SetIcon(self._get_app_icon())
# 重定向 stdout/stderr
self._redirect = RedirectStream(self._on_redirect_output)
sys.stdout = self._redirect
sys.stderr = self._redirect
FileServerHandler.log_callback = self._on_handler_log
self.log_line(f"{APP_TITLE} v{APP_VERSION} 已就绪。", color="gray")
# ------------------ UI 构建 ------------------
def _init_ui(self):
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
# ----- 第1行:共享目录 -----
row_dir = wx.BoxSizer(wx.HORIZONTAL)
row_dir.Add(wx.StaticText(panel, label="共享目录:"),
flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=8)
self.dir_picker = wx.DirPickerCtrl(
panel,
message="选择要共享的文件夹",
style=wx.DIRP_USE_TEXTCTRL | wx.DIRP_DIR_MUST_EXIST
)
self.dir_picker.SetMinSize((400, -1))
row_dir.Add(self.dir_picker, proportion=1,
flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=4)
vbox.Add(row_dir, flag=wx.EXPAND | wx.TOP, border=8)
# ----- 第2行:主机IP + 端口 + 状态/清空按钮 -----
row_host_port = wx.BoxSizer(wx.HORIZONTAL)
row_host_port.Add(wx.StaticText(panel, label="主机 IP:"),
flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=8)
self.host_text = wx.TextCtrl(panel, value=DEFAULT_HOST, size=(130, -1))
row_host_port.Add(self.host_text,
flag=wx.LEFT | wx.ALIGN_CENTER_VERTICAL, border=4)
row_host_port.Add(wx.StaticText(panel, label="端口:"),
flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=12)
self.port_text = wx.TextCtrl(panel, value=str(DEFAULT_PORT), size=(80, -1))
row_host_port.Add(self.port_text,
flag=wx.LEFT | wx.ALIGN_CENTER_VERTICAL, border=4)
self.status_indicator = wx.StaticText(panel, label="● 已停止",
style=wx.ALIGN_CENTER_VERTICAL)
self.status_indicator.SetForegroundColour(wx.Colour(120, 120, 120))
row_host_port.Add(self.status_indicator,
flag=wx.LEFT | wx.ALIGN_CENTER_VERTICAL, border=20)
row_host_port.AddStretchSpacer()
self.clear_log_btn = wx.Button(panel, label="清空日志")
row_host_port.Add(self.clear_log_btn,
flag=wx.LEFT | wx.ALIGN_CENTER_VERTICAL, border=4)
vbox.Add(row_host_port, flag=wx.EXPAND | wx.TOP, border=8)
# ----- 第3行:控制按钮 -----
row_btns = wx.BoxSizer(wx.HORIZONTAL)
self.start_btn = wx.Button(panel, label="开启服务")
self.stop_btn = wx.Button(panel, label="关闭服务")
self.open_browser_btn = wx.Button(panel, label="在浏览器打开")
row_btns.Add(self.start_btn, flag=wx.LEFT, border=8)
row_btns.Add(self.stop_btn, flag=wx.LEFT, border=4)
row_btns.Add(self.open_browser_btn, flag=wx.LEFT, border=4)
vbox.Add(row_btns, flag=wx.EXPAND | wx.TOP, border=8)
# ----- 第4行:日志区域 -----
vbox.Add(wx.StaticText(panel, label="日志:"),
flag=wx.LEFT | wx.TOP, border=8)
self.log_ctrl = rt.RichTextCtrl(
panel,
style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.BORDER_SIMPLE
)
vbox.Add(self.log_ctrl, proportion=1,
flag=wx.EXPAND | wx.ALL, border=8)
panel.SetSizer(vbox)
self.Centre()
def _bind_events(self):
self.dir_picker.Bind(wx.EVT_DIRPICKER_CHANGED, self.on_dir_changed)
self.port_text.Bind(wx.EVT_CHAR, self.on_port_char)
self.start_btn.Bind(wx.EVT_BUTTON, self.on_start)
self.stop_btn.Bind(wx.EVT_BUTTON, self.on_stop)
self.open_browser_btn.Bind(wx.EVT_BUTTON, self.on_open_browser)
self.clear_log_btn.Bind(wx.EVT_BUTTON, self.on_clear_log)
self.Bind(wx.EVT_CLOSE, self.on_close)
# ------------------ 图标加载 ------------------
def _get_app_icon(self):
"""获取应用程序图标,支持 PyInstaller 打包后的路径。"""
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.dirname(os.path.abspath(__file__))
icon_path = os.path.join(base_path, 'server.ico')
if os.path.exists(icon_path):
icon = wx.Icon()
if icon.LoadFile(icon_path, wx.BITMAP_TYPE_ICO):
return icon
return wx.NullIcon
# ------------------ 配置读写 ------------------
def _load_config(self):
default = {"directory": os.getcwd(), "host": DEFAULT_HOST, "port": DEFAULT_PORT}
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return {k: data.get(k, default[k]) for k in default}
except Exception:
pass
return default
def _save_config(self):
try:
data = {
"directory": self.dir_picker.GetPath(),
"host": self.host_text.GetValue().strip(),
"port": int(self.port_text.GetValue().strip())
}
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
except Exception:
pass
def _apply_config(self):
self.dir_picker.SetPath(self.config["directory"])
self.host_text.SetValue(self.config["host"])
self.port_text.SetValue(str(self.config["port"]))
# ------------------ 事件处理 ------------------
def on_dir_changed(self, event):
self._save_config()
def on_port_char(self, event):
key = event.GetKeyCode()
if (47 < key < 58) or key in (8, 127, 314, 315, 316, 317):
event.Skip()
else:
if not event.ShiftDown():
wx.Bell()
def on_start(self, event):
with self.server_lock:
if self.httpd is not None:
self.log_line("服务已在运行中。", color="orange")
return
# 校验目录
directory = self.dir_picker.GetPath().strip()
if not directory or not os.path.isdir(directory):
self.log_line("请选择有效的共享目录。", color="red")
wx.MessageBox("请选择有效的共享目录。", "提示", wx.OK | wx.ICON_WARNING)
return
# 校验IP
host = self.host_text.GetValue().strip()
if not host:
self.log_line("请输入有效的主机IP。", color="red")
return
# 校验端口
try:
port = int(self.port_text.GetValue().strip())
except ValueError:
self.log_line("端口号无效,请输入数字。", color="red")
return
if not (1 <= port <= 65535):
self.log_line("端口号必须在 1-65535 之间。", color="red")
return
# 检查端口是否被占用(针对指定IP)
if self._is_port_in_use(host, port):
self.log_line(f"{host}:{port} 已被占用,请更换。", color="red")
wx.MessageBox(f"{host}:{port} 已被占用,请更换。", "提示", wx.OK | wx.ICON_WARNING)
return
# 创建服务器
try:
handler_class = partial(FileServerHandler, directory=directory)
self.httpd = ThreadingHTTPServer((host, port), handler_class)
self.httpd.daemon_threads = True
except Exception as e:
self.log_line(f"启动失败:{e}", color="red")
wx.MessageBox(f"启动服务器失败:{e}", "错误", wx.OK | wx.ICON_ERROR)
self.httpd = None
return
# 启动服务线程
self.server_thread = threading.Thread(
target=self._serve_loop,
args=(self.httpd,),
daemon=True
)
self.server_thread.start()
# ---------- 日志显示访问地址 ----------
if host == '0.0.0.0':
local_ips = self._get_local_ips()
if local_ips:
self.log_line(f"服务已启动,监听所有接口 (0.0.0.0:{port})。可通过以下地址访问:", color="green")
for ip in local_ips:
self.log_line(f" http://{ip}:{port}/", color="blue")
else:
self.log_line(f"服务已启动,但未检测到有效局域网 IP。", color="orange")
elif host == '127.0.0.1':
self.log_line(f"服务已启动 → http://127.0.0.1:{port}/ 仅本机可访问", color="green")
else:
self.log_line(f"服务已启动 → http://{host}:{port}/ 目录:{directory}", color="green")
self.status_indicator.SetLabel("● 运行中")
self.status_indicator.SetForegroundColour(wx.Colour(0, 180, 0))
self._update_button_state(running=True)
self._save_config()
def on_stop(self, event):
with self.server_lock:
if self.httpd is None:
self.log_line("服务未运行。", color="orange")
return
try:
self.httpd.shutdown()
self.httpd.server_close()
if self.server_thread and self.server_thread.is_alive():
self.server_thread.join(1.0)
except Exception as e:
self.log_line(f"关闭异常:{e}", color="red")
finally:
self.httpd = None
self.server_thread = None
self.log_line("服务已停止。", color="gray")
self.status_indicator.SetLabel("● 已停止")
self.status_indicator.SetForegroundColour(wx.Colour(120, 120, 120))
self._update_button_state(running=False)
def on_open_browser(self, event):
if self.httpd is None:
self.log_line("服务未运行,无法打开浏览器。", color="orange")
return
host = self.host_text.GetValue().strip()
try:
port = int(self.port_text.GetValue().strip())
except ValueError:
port = DEFAULT_PORT
if host == "0.0.0.0":
display_host = "127.0.0.1"
else:
display_host = host
url = f"http://{display_host}:{port}/"
self.log_line(f"在浏览器中打开:{url}", color="blue")
webbrowser.open(url)
def on_clear_log(self, event):
self.log_ctrl.Clear()
def on_close(self, event):
if self.httpd is not None:
self.on_stop(None)
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
self.Destroy()
# ------------------ 服务线程 ------------------
def _serve_loop(self, httpd):
try:
self.log_line("服务线程进入监听循环。", color="gray")
httpd.serve_forever()
except Exception as e:
wx.CallAfter(self.log_line, f"服务异常:{e}", "red")
finally:
wx.CallAfter(self.log_line, "服务线程已退出。", color="gray")
# ------------------ 日志处理 ------------------
def _on_handler_log(self, line):
wx.CallAfter(self.log_line, line, "black")
def _on_redirect_output(self, line):
wx.CallAfter(self.log_line, line, "black")
def log_line(self, line, color="black"):
if not line:
return
ts = time.strftime("%H:%M:%S")
self.log_ctrl.SetInsertionPointEnd()
attr = wx.TextAttr()
attr.SetTextColour(wx.Colour(self._color_to_rgb(color)))
self.log_ctrl.SetDefaultStyle(attr)
self.log_ctrl.WriteText(f"[{ts}] {line}\n")
self.log_ctrl.ShowPosition(self.log_ctrl.GetLastPosition())
@staticmethod
def _color_to_rgb(name):
mapping = {
"black": (0, 0, 0),
"red": (220, 20, 20),
"green": (0, 128, 0),
"blue": (0, 0, 200),
"orange": (200, 120, 0),
"gray": (120, 120, 120),
}
return mapping.get(name, (0, 0, 0))
# ------------------ 辅助方法 ------------------
def _is_port_in_use(self, host, port):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
s.bind((host, port))
return False
except Exception:
return True
def _get_local_ips(self):
"""获取本机所有非环回 IPv4 地址(用于 0.0.0.0 绑定时的提示)。"""
ips = set()
try:
hostname = socket.gethostname()
for ip in socket.gethostbyname_ex(hostname)[2]:
if ip != '127.0.0.1' and not ip.startswith('169.254.'):
ips.add(ip)
except Exception:
pass
# 备用方法:通过连接外部获得默认出口 IP
if not ips:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
s.close()
if ip and ip != '127.0.0.1' and not ip.startswith('169.254.'):
ips.add(ip)
except Exception:
pass
return list(ips)
def _update_button_state(self, running=False):
self.start_btn.Enable(not running)
self.stop_btn.Enable(running)
self.open_browser_btn.Enable(running)
# ------------------ 应用程序类 ------------------
class ServerApp(wx.App):
def OnInit(self):
try:
self.frame = MainFrame()
self.SetTopWindow(self.frame)
self.frame.Show()
return True
except Exception as e:
import traceback
tb = traceback.format_exc()
try:
err_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "crash.log")
with open(err_path, "w", encoding="utf-8") as f:
f.write(tb)
except Exception:
pass
try:
dlg = wx.MessageDialog(None, f"初始化失败:{e}\n\n详情见 crash.log", "严重错误",
wx.OK | wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
except Exception:
pass
return False
def main():
app = ServerApp()
app.MainLoop()
if __name__ == "__main__":
main()
打包配置文件
WxHttpFileServer.spec
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all
# 收集 wx 所有依赖
tmp_ret = collect_all('wx')
datas = tmp_ret[0] + [('server.ico', '.')] # 添加自定义图标
binaries = tmp_ret[1]
hiddenimports = tmp_ret[2]
a = Analysis(
['wx_http_server_gui.py'],
pathex=[],
binaries=binaries,
datas=datas, # 包含图标文件
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='WxHttpFileServer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='server.ico', # 设置 exe 文件的图标(资源管理器显示)
)
打包脚本
build.bat
@echo off
chcp 65001 >nul
title 打包 HTTP 文件服务器
echo ============================================
echo 开始打包 WxHttpFileServer
echo ============================================
:: 检查 PyInstaller 是否安装
pip show pyinstaller >nul 2>&1
if errorlevel 1 (
echo [错误] 未找到 PyInstaller,请先安装:pip install pyinstaller
pause
exit /b 1
)
:: 清理旧构建文件(可选)
if exist "build" (
echo 删除旧的 build 目录...
rmdir /s /q build
)
if exist "dist" (
echo 删除旧的 dist 目录...
rmdir /s /q dist
)
:: 执行打包(使用 spec 文件)
echo 正在执行打包,请稍候...
pyinstaller WxHttpFileServer.spec
if errorlevel 1 (
echo [错误] 打包失败,请检查错误信息。
pause
exit /b 1
)
echo ============================================
echo 打包完成!可执行文件位于 dist\ 目录下
echo ============================================
pause
使用方式
- 把上述所有文件保存到同一个目录下;
- 制作图标
python3 generate_icon.py; - 打包:双击或在命令行执行:build.bat,然后会在dist目录生成exe文件
- 进入dist目录,双击或在命令行打开打包好的程序:WxHttpFileServer.exe
运行效果如下:



浙公网安备 33010602011771号