成品展示
输入 http://127.0.0.1:8080/,展示截图如下

代码如下
python
# -*- coding: utf-8 -*-
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import quote, unquote
import zipfile
from io import BytesIO
# 配置
HOST = "0.0.0.0"
PORT = 8080
IMG_DIR = "img"
# 仅这些格式支持页面预览+大图弹窗
PREVIEW_SUFFIX = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")
# 统一正斜杠根路径
BASE_IMG_URL = IMG_DIR.replace(os.sep, "/")
os.makedirs(IMG_DIR, exist_ok=True)
class ImageHandler(BaseHTTPRequestHandler):
# 过滤NiceGUI websocket刷屏日志,其余访问日志正常输出
def log_message(self, format, *args):
msg = format % args
if "_nicegui_ws" not in msg:
print(msg)
def read_file(self, path):
with open(path, "rb") as f:
return f.read()
# 安全路径校验,防目录穿越,统一正斜杠
def safe_abs_path(self, rel_path):
base_abs = os.path.abspath(IMG_DIR)
# 统一转为正斜杠拼接
target_rel = os.path.join(BASE_IMG_URL, rel_path).replace(os.sep, "/")
target_abs = os.path.abspath(target_rel.replace("/", os.sep))
if not target_abs.startswith(base_abs):
return None
return target_abs
# 递归生成左侧树形目录HTML
def build_tree_html(self, current_rel, parent_rel=""):
abs_dir = self.safe_abs_path(parent_rel)
if abs_dir is None or not os.path.isdir(abs_dir):
return ""
items = []
for name in sorted(os.listdir(abs_dir)):
full_path = os.path.join(abs_dir, name)
if os.path.isdir(full_path):
sub_rel = f"{parent_rel}/{name}".strip("/")
child_html = self.build_tree_html(current_rel, sub_rel)
active_cls = "tree-active" if sub_rel == current_rel else ""
items.append(f"""
<div class="tree-item">
<div class="tree-line">
<a class="tree-link {active_cls}" href="/list/{quote(sub_rel)}">📂 {name}</a>
<a class="tree-zip-btn" href="javascript:startZipDownload('{quote(sub_rel)}')" title="批量下载该目录全部文件">↓打包</a>
</div>
<div class="tree-child">{child_html}</div>
</div>
""")
return "".join(items)
# 解析URL获取当前浏览目录(无分隔符替换)
def get_current_dir(self):
if self.path == "/":
return ""
if self.path.startswith("/list/"):
raw = unquote(self.path[6:])
return raw.strip("/")
return None
# 递归写入zip内存流
def add_dir_to_zip(self, zf, root_rel, curr_rel):
curr_abs = self.safe_abs_path(curr_rel)
if not curr_abs:
return 0
count = 0
for name in os.listdir(curr_abs):
file_abs = os.path.join(curr_abs, name)
file_rel = f"{curr_rel}/{name}".strip("/")
zip_inner_path = file_rel[len(root_rel):].lstrip("/")
if os.path.isdir(file_abs):
cnt = self.add_dir_to_zip(zf, root_rel, file_rel)
count += cnt
elif os.path.isfile(file_abs):
zf.write(file_abs, arcname=zip_inner_path)
count += 1
return count
# 整目录打包下载接口
def handle_zip_download(self, rel_dir):
target_abs = self.safe_abs_path(rel_dir)
if target_abs is None or not os.path.isdir(target_abs):
self.send_response(404)
self.send_header("Content-Type", "text/plain;charset=utf-8")
self.end_headers()
self.wfile.write("目录不存在,无法打包".encode("utf-8"))
return
dir_name = os.path.basename(target_abs) if rel_dir else "根目录文件"
mem_buf = BytesIO()
with zipfile.ZipFile(mem_buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
file_count = self.add_dir_to_zip(zf, rel_dir, rel_dir)
if file_count == 0:
self.send_response(400)
self.send_header("Content-Type", "text/plain;charset=utf-8")
self.end_headers()
self.wfile.write("该目录下无任何文件,无法打包".encode("utf-8"))
return
mem_buf.seek(0)
zip_data = mem_buf.read()
self.send_response(200)
self.send_header("Content-Type", "application/zip")
self.send_header("Content-Disposition", f'attachment; filename="{dir_name}.zip"')
self.send_header("Cache-Control", "no-cache")
self.send_header("Content-Length", str(len(zip_data)))
self.end_headers()
self.wfile.write(zip_data)
# 渲染页面(卡片缩小一半、按钮宽高减半CSS完整保留)
def render_split_page(self, rel_dir):
abs_dir = self.safe_abs_path(rel_dir)
tree_html = self.build_tree_html(rel_dir)
root_active = "tree-active" if rel_dir == "" else ""
folders = []
all_files = []
if abs_dir and os.path.isdir(abs_dir):
for name in os.listdir(abs_dir):
full_path = os.path.join(abs_dir, name)
if os.path.isdir(full_path):
folders.append(name)
elif os.path.isfile(full_path):
all_files.append(name)
else:
return "<h1>403/404 目录不存在</h1>".encode("utf-8")
html = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件浏览下载服务</title>
<style>
* {{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft Yahei",Arial;}}
body {{display:flex;height:100vh;overflow:hidden;background:#f5f7fa;}}
/* 打包加载遮罩 */
.loading-mask {{
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.6);
z-index: 9999;
align-items: center;
justify-content: center;
}}
.loading-box {{
background: #fff;
padding: 30px 40px;
border-radius: 10px;
font-size: 16px;
color: #222;
}}
/* 左侧目录侧边栏 */
.sidebar {{
width: 280px;
min-width:280px;
background:#fff;
border-right:1px solid #ddd;
padding:20px 10px;
overflow-y:auto;
}}
.sidebar h3 {{text-align:center;margin-bottom:20px;color:#222;}}
.tree-item {{margin:6px 0;padding-left:12px;}}
.tree-child {{padding-left:16px;}}
.tree-line {{display:flex;align-items:center;gap:6px;}}
.tree-link {{
flex:1;
display:block;
padding:6px 8px;
text-decoration:none;
color:#333;
border-radius:4px;
}}
.tree-link:hover {{background:#e8f0fe;}}
.tree-active {{background:#2478ff;color:#fff !important;}}
.tree-zip-btn {{
font-size:10px;
padding:3px 5px;
background:#00aa66;
color:#fff;
text-decoration:none;
border-radius:3px;
white-space:nowrap;
}}
.tree-zip-btn:hover {{background:#008850;}}
/* 右侧内容区域 */
.main-content {{
flex:1;
overflow-y:auto;
padding:30px;
}}
h2 {{text-align:center;color:#222;margin-bottom:10px;}}
.tip {{text-align:center;color:#d32f2f;margin-bottom:20px;}}
.section-title {{
font-size:18px;
margin:30px 0 12px;
color:#333;
border-left:4px solid #2478ff;
padding-left:10px;
}}
/* 卡片整体缩小一半 minmax(120px,1fr) */
.grid {{
display:grid;
grid-template-columns:repeat(auto-fill,minmax(120px,1fr));
gap:14px;
margin-bottom:40px;
}}
.folder-card {{
background:#fff;
padding:14px;
border-radius:8px;
box-shadow:0 2px 8px #ddd;
text-align:center;
}}
.folder-card a {{
display:block;
text-decoration:none;
color:#2478ff;
font-size:14px;
padding:8px 0;
}}
.file-card {{
background:#fff;
padding:6px;
border-radius:8px;
box-shadow:0 1px 6px #ddd;
}}
/* 缩略图同步缩小一半 height:52px */
.preview-box {{
width:100%;
height:52px;
display:flex;
align-items:center;
justify-content:center;
background:#eee;
margin-bottom:4px;
}}
.preview-box img {{max-width:100%;max-height:100%;object-fit:contain;}}
.file-txt {{color:#666;font-size:10px;text-align:center;padding:0 2px;}}
/* 按钮整体宽度缩小一半,水平居中 */
.btn-row {{
display:grid;
grid-template-columns: 1fr 1fr;
gap:0;
margin-top:4px;
width:50%;
margin-left:auto;
margin-right:auto;
}}
.zoom-btn {{
border-radius:4px 0 0 4px;
}}
.download-btn {{
border-radius:0 4px 4px 0;
}}
/* 按钮高度缩小一半,字号缩小 */
.download-btn, .zoom-btn {{
display:block;
text-align:center;
padding:2px 0;
color:#fff;
text-decoration:none;
font-size:9px;
border:none;
cursor:pointer;
}}
.download-btn {{background:#2478ff;}}
.download-btn:hover {{background:#0f60e0;}}
.zoom-btn {{background:#22aa55;}}
.zoom-btn:hover {{background:#188844;}}
/* 大图弹窗模态框 */
.modal {{
display:none;
position:fixed;
top:0;
left:0;
width:100vw;
height:100vh;
background:rgba(0,0,0,0.85);
z-index:998;
align-items:center;
justify-content:center;
padding:30px;
}}
.modal-content {{
max-width:95%;
max-height:95%;
position:relative;
}}
.modal-img {{
max-width:100%;
max-height:90vh;
border:4px solid #fff;
}}
.modal-close {{
position:absolute;
top:-30px;
right:-30px;
color:#fff;
font-size:32px;
cursor:pointer;
}}
</style>
</head>
<body>
<div class="loading-mask" id="loadingMask">
<div class="loading-box">⏳ 正在打包压缩,请稍候...</div>
</div>
<div class="modal" id="imgModal" onclick="closeModal(event)">
<div class="modal-content">
<span class="modal-close" onclick="closeModal()">×</span>
<img id="modalImage" class="modal-img" src="" alt="大图预览">
</div>
</div>
<div class="sidebar">
<h3>📁 全部目录 <span style="font-size:10px;color:#666;">(↓打包=批量下载)</span></h3>
<div class="tree-item">
<div class="tree-line">
<a class="tree-link {root_active}" href="/list/">🏠 根目录(img/)</a>
<a class="tree-zip-btn" href="javascript:startZipDownload('')" title="批量下载根目录全部文件">↓打包</a>
</div>
<div class="tree-child">
{tree_html}
</div>
</div>
</div>
<div class="main-content">
<h2>当前目录:{rel_dir if rel_dir else "根目录 img/"}</h2>
<div class="tip">提示:仅图片文件可在线预览/放大,所有格式文件均可下载;tif/tiff浏览器无法预览大图;左侧目录点击↓打包可批量下载整个目录,打包期间会显示加载提示</div>
<div class="section-title">📂 子文件夹</div>
<div class="grid">
"""
for fname in folders:
sub_rel = f"{rel_dir}/{fname}".strip("/")
url = "/list/" + quote(sub_rel)
html += f'<div class="folder-card"><a href="{url}">{fname}</a></div>'
html += "</div><div class='section-title'>📄 当前目录全部文件</div><div class='grid'>"
for file_name in all_files:
file_rel = f"{rel_dir}/{file_name}".strip("/")
safe_file = quote(file_rel)
lower_name = file_name.lower()
is_previewable = lower_name.endswith(PREVIEW_SUFFIX)
if is_previewable:
if lower_name.endswith((".tif", ".tiff")):
preview_html = '<div class="preview-box"><div class="file-txt">TIFF</div></div>'
btn_html = f'<a class="download-btn" href="/download/{safe_file}">下载</a>'
else:
preview_html = f'<div class="preview-box"><img src="/img/{safe_file}" alt="{file_name}"></div>'
btn_html = f"""
<div class="btn-row">
<button class="zoom-btn" onclick="openModal('/img/{safe_file}')">放大</button>
<a class="download-btn" href="/download/{safe_file}">下载</a>
</div>
"""
else:
preview_html = '<div class="preview-box"><div class="file-txt">文件</div></div>'
btn_html = f'<a class="download-btn" href="/download/{safe_file}">下载</a>'
html += f"""
<div class="file-card">
{preview_html}
<div style="font-size:9px;word-break:break-all;margin:2px 0;">{file_name}</div>
{btn_html}
</div>
"""
html += """
</div>
</div>
<script>
const loadingMask = document.getElementById('loadingMask');
function startZipDownload(dirPath) {
loadingMask.style.display = 'flex';
let zipUrl;
if(dirPath === ''){
zipUrl = `/zip`;
}else{
zipUrl = `/zip/${dirPath}`;
}
const newWin = window.open(zipUrl, '_blank');
if (!newWin || newWin.closed || typeof newWin.closed=='undefined') {
loadingMask.style.display = 'none';
alert("浏览器拦截了弹出窗口,请允许本站弹窗后重试");
return;
}
setTimeout(()=>{
loadingMask.style.display = 'none';
}, 30000);
}
const modal = document.getElementById("imgModal");
const modalImg = document.getElementById("modalImage");
function openModal(src) {
modal.style.display = "flex";
modalImg.src = src;
}
function closeModal(e) {
if (!e || e.target === modal) {
modal.style.display = "none";
modalImg.src = "";
}
}
document.addEventListener('keydown', function(e){
if(e.key === 'Escape') closeModal();
})
</script>
</body>
</html>
"""
return html.encode("utf-8")
def do_GET(self):
# 首页自动跳转根目录列表
if self.path == "/":
self.send_response(302)
self.send_header("Location", "/list/")
self.end_headers()
return
# 目录打包接口 /zip /zip/xxx
if self.path.startswith("/zip"):
raw = self.path[4:].lstrip("/")
dir_rel = unquote(raw).strip("/")
self.handle_zip_download(dir_rel)
return
# 页面浏览 /list/xxx
current_dir = self.get_current_dir()
if current_dir is not None:
page_data = self.render_split_page(current_dir)
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(page_data)
return
# 图片预览 /img/xxx
if self.path.startswith("/img/"):
rel_path = unquote(self.path[5:]).strip("/")
abs_file = self.safe_abs_path(rel_path)
if abs_file is None or not os.path.isfile(abs_file):
self.send_response(404)
self.end_headers()
self.wfile.write("404 文件不存在".encode("utf-8"))
return
if not rel_path.lower().endswith(PREVIEW_SUFFIX):
self.send_response(403)
self.end_headers()
self.wfile.write("403 仅图片可在线预览,请使用下载按钮获取文件".encode("utf-8"))
return
suffix = rel_path.lower()
self.send_response(200)
if suffix.endswith(".png"):
self.send_header("Content-Type", "image/png")
elif suffix.endswith(".gif"):
self.send_header("Content-Type", "image/gif")
elif suffix.endswith((".tif", ".tiff")):
self.send_header("Content-Type", "image/tiff")
else:
self.send_header("Content-Type", "image/jpeg")
self.end_headers()
self.wfile.write(self.read_file(abs_file))
return
# 文件下载 /download/xxx
if self.path.startswith("/download/"):
rel_path = unquote(self.path[10:]).strip("/")
abs_file = self.safe_abs_path(rel_path)
if abs_file is None or not os.path.isfile(abs_file):
self.send_response(404)
self.end_headers()
self.wfile.write("404 文件不存在".encode("utf-8"))
return
file_name = os.path.basename(abs_file)
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Disposition", f'attachment; filename="{file_name}"')
self.end_headers()
self.wfile.write(self.read_file(abs_file))
return
# 兜底404
self.send_response(404)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write("404 Not Found".encode("utf-8"))
if __name__ == "__main__":
server = HTTPServer((HOST, PORT), ImageHandler)
print("===== 过滤NiceGUI websocket刷屏日志、保留正常访问日志 =====")
print(f"访问地址:http://127.0.0.1:{PORT}")
print("说明:")
print("1. 自动过滤/_nicegui_ws/垃圾404日志,不再刷屏")
print("2. 正常页面、图片、下载、打包请求日志照常打印,方便调试")
print("3. 路径统一正斜杠,彻底修复Windows下文件404问题")
print("Ctrl + C 停止服务")
try:
server.serve_forever()
except KeyboardInterrupt:
server.server_close()
print("\n服务已正常关闭")