"""
独立图片预览 HTTP 服务
启动方式: python image_server.py [端口]
默认端口: 8080
"""
import os
import sys
import json
import mimetypes
import http.server
from pathlib import Path
from urllib.parse import unquote, urlparse, parse_qs
# 图片根目录(与 settings/base.py 中 FILESPACE_ROOT_PATH 默认值一致)
IMAGE_ROOT = Path(r"D:\00_Workspace\python\model_train\project_space")
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".tif"}
BASE_DIR = Path(__file__).resolve().parent
TEMPLATE = r"""
图片预览 - {title}
{body}
×
‹
›
"""
def is_image(filename: str) -> bool:
return Path(filename).suffix.lower() in IMAGE_EXTENSIONS
def get_breadcrumb_html(relative_path: str) -> str:
"""生成面包屑导航"""
if not relative_path or relative_path == ".":
return '根目录'
parts = relative_path.replace("\\", "/").strip("/").split("/")
links = ['根目录']
accumulated = ""
for p in parts:
accumulated += "/" + p
links.append(f'{p}')
return " / ".join(links)
def get_parent_dir(relative_path: str) -> str:
if not relative_path or relative_path == ".":
return ""
parent = str(Path(relative_path).parent)
return "" if parent == "." else parent
def build_page_html(current_dir: str, view_mode: str, port: int) -> str:
root = IMAGE_ROOT
if current_dir:
scan_path = root / current_dir
else:
scan_path = root
dir_param = f"dir={current_dir}&" if current_dir else ""
if not scan_path.exists():
body = f'目录不存在: {scan_path}
'
return TEMPLATE.format(
title="目录不存在",
breadcrumb=get_breadcrumb_html(current_dir),
dir_param=dir_param,
body=body,
current_view=view_mode,
gallery_active="active" if view_mode != "list" else "",
list_active="active" if view_mode == "list" else "",
image_list="[]",
port=port,
)
folders = []
images = []
try:
for entry in sorted(scan_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
if entry.name.startswith("."):
continue
if entry.is_dir():
folders.append(entry.name)
elif is_image(entry.name):
images.append(entry.name)
except PermissionError:
pass
body_parts = []
# 返回上级目录
if current_dir and current_dir != ".":
parent = get_parent_dir(current_dir)
body_parts.append(
f''
)
# 子目录
if folders:
parts = []
for f in folders:
sub_dir = f"{current_dir}/{f}".lstrip("/") if current_dir else f
parts.append(
f''
f'📁{f}'
)
body_parts.append(f'{"".join(parts)}
')
# 图片
if images:
image_list_data = []
for img in images:
if current_dir:
rel = f"{current_dir}/{img}".replace("\\", "/")
else:
rel = img
image_list_data.append({"name": img, "path": rel})
if view_mode == "list":
rows = []
for i, img_info in enumerate(image_list_data):
rows.append(
f'| {i+1} | '
f'{img_info["name"]} | '
f'预览 |
'
)
body_parts.append(
''
'| # | '
'文件名 | '
'操作 | '
'
' + "".join(rows) + "
"
)
else:
cards = []
for i, img_info in enumerate(image_list_data):
cards.append(
f''
f'

'
f'
'
f'
'
)
body_parts.append(f'{"".join(cards)}
')
body = "".join(body_parts)
return TEMPLATE.format(
title=current_dir or "根目录",
breadcrumb=get_breadcrumb_html(current_dir),
dir_param=dir_param,
body=body,
current_view=view_mode,
gallery_active="active" if view_mode != "list" else "",
list_active="active" if view_mode == "list" else "",
image_list=json.dumps(image_list_data),
port=port,
)
else:
if folders:
body = "".join(body_parts)
else:
body_parts.append('此目录下没有图片
')
body = "".join(body_parts)
return TEMPLATE.format(
title=current_dir or "根目录",
breadcrumb=get_breadcrumb_html(current_dir),
dir_param=dir_param,
body=body,
current_view=view_mode,
gallery_active="active" if view_mode != "list" else "",
list_active="active" if view_mode == "list" else "",
image_list="[]",
port=port,
)
class ImagePreviewHandler(http.server.SimpleHTTPRequestHandler):
"""自定义请求处理器"""
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(BASE_DIR), **kwargs)
def do_GET(self):
parsed = urlparse(self.path)
path = unquote(parsed.path)
params = parse_qs(parsed.query)
# 根路径 → HTML 页面
if path == "/":
current_dir = params.get("dir", [""])[0]
view_mode = params.get("view", ["gallery"])[0]
html = build_page_html(current_dir, view_mode, PORT)
self._respond_html(html)
return
# /file/ 路径 → 返回实际图片文件
if path.startswith("/file/"):
file_rel = path[len("/file/"):]
file_path = IMAGE_ROOT / file_rel
if file_path.exists() and file_path.is_file():
self._serve_file(file_path)
else:
self.send_error(404, f"File not found: {file_rel}")
return
# 其他路径 → 默认静态文件处理
super().do_GET()
def _respond_html(self, html: str):
data = html.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.write(data)
def _serve_file(self, file_path: Path):
mime_type, _ = mimetypes.guess_type(str(file_path))
if mime_type is None:
mime_type = "application/octet-stream"
file_size = file_path.stat().st_size
self.send_response(200)
self.send_header("Content-Type", mime_type)
self.send_header("Content-Length", str(file_size))
self.send_header("Cache-Control", "public, max-age=3600")
self.end_headers()
with open(file_path, "rb") as f:
self.wfile.write(f.read())
def log_message(self, format, *args):
print(f"[{self.log_date_time_string()}] {args[0]}")
def main():
global PORT
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
# 确保图片根目录存在
if not IMAGE_ROOT.exists():
os.makedirs(IMAGE_ROOT, exist_ok=True)
print(f"已创建目录: {IMAGE_ROOT}")
server = http.server.HTTPServer(("0.0.0.0", PORT), ImagePreviewHandler)
print(f"=" * 55)
print(f" 图片预览服务已启动")
print(f" 图片根目录: {IMAGE_ROOT}")
print(f" 访问地址: http://localhost:{PORT}")
print(f" 按 Ctrl+C 停止服务")
print(f"=" * 55)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n服务已停止")
server.server_close()
if __name__ == "__main__":
main()