172 lines
6.1 KiB
Python
172 lines
6.1 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
自定义静态文件服务器,支持自定义404页面和SPA路由
|
|||
|
|
用于部署Next.js静态导出的文件
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import http.server
|
|||
|
|
import socketserver
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import urllib.parse
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
|
|
def __init__(self, *args, **kwargs):
|
|||
|
|
# 设置静态文件目录
|
|||
|
|
super().__init__(*args, directory="out", **kwargs)
|
|||
|
|
|
|||
|
|
def end_headers(self):
|
|||
|
|
# 添加安全头和缓存控制
|
|||
|
|
self.send_header('X-Content-Type-Options', 'nosniff')
|
|||
|
|
self.send_header('X-Frame-Options', 'DENY')
|
|||
|
|
self.send_header('X-XSS-Protection', '1; mode=block')
|
|||
|
|
|
|||
|
|
# 为HTML文件禁用缓存,为静态资源启用缓存
|
|||
|
|
if self.path.endswith('.html'):
|
|||
|
|
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
|
|||
|
|
self.send_header('Pragma', 'no-cache')
|
|||
|
|
self.send_header('Expires', '0')
|
|||
|
|
elif any(self.path.endswith(ext) for ext in ['.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.woff', '.woff2']):
|
|||
|
|
self.send_header('Cache-Control', 'public, max-age=31536000')
|
|||
|
|
|
|||
|
|
super().end_headers()
|
|||
|
|
|
|||
|
|
def do_GET(self):
|
|||
|
|
# URL解码
|
|||
|
|
parsed_path = urllib.parse.unquote(self.path)
|
|||
|
|
|
|||
|
|
# 移除查询参数
|
|||
|
|
if '?' in parsed_path:
|
|||
|
|
parsed_path = parsed_path.split('?')[0]
|
|||
|
|
|
|||
|
|
# 移除fragment
|
|||
|
|
if '#' in parsed_path:
|
|||
|
|
parsed_path = parsed_path.split('#')[0]
|
|||
|
|
|
|||
|
|
# 构建文件路径
|
|||
|
|
if parsed_path == '/':
|
|||
|
|
# 根路径重定向到index.html
|
|||
|
|
file_path = Path("out/index.html")
|
|||
|
|
else:
|
|||
|
|
# 移除开头的斜杠
|
|||
|
|
clean_path = parsed_path.lstrip('/')
|
|||
|
|
file_path = Path("out") / clean_path
|
|||
|
|
|
|||
|
|
# 如果是目录,尝试查找index.html
|
|||
|
|
if file_path.is_dir():
|
|||
|
|
file_path = file_path / "index.html"
|
|||
|
|
|
|||
|
|
# 如果没有扩展名,尝试添加.html
|
|||
|
|
elif not file_path.suffix and not file_path.exists():
|
|||
|
|
file_path = file_path.with_suffix('.html')
|
|||
|
|
|
|||
|
|
print(f"请求路径: {self.path}")
|
|||
|
|
print(f"解析路径: {parsed_path}")
|
|||
|
|
print(f"文件路径: {file_path}")
|
|||
|
|
print(f"文件存在: {file_path.exists()}")
|
|||
|
|
|
|||
|
|
# 检查文件是否存在
|
|||
|
|
if file_path.exists() and file_path.is_file():
|
|||
|
|
# 文件存在,正常处理
|
|||
|
|
self.path = '/' + str(file_path.relative_to("out"))
|
|||
|
|
super().do_GET()
|
|||
|
|
else:
|
|||
|
|
# 文件不存在,返回404页面
|
|||
|
|
self.send_custom_404()
|
|||
|
|
|
|||
|
|
def send_custom_404(self):
|
|||
|
|
"""发送自定义404页面"""
|
|||
|
|
try:
|
|||
|
|
# 检查自定义404页面是否存在
|
|||
|
|
error_page_path = Path("out/404.html")
|
|||
|
|
if not error_page_path.exists():
|
|||
|
|
# 如果404.html不存在,使用public/404.html
|
|||
|
|
error_page_path = Path("public/404.html")
|
|||
|
|
|
|||
|
|
if error_page_path.exists():
|
|||
|
|
with open(error_page_path, 'rb') as f:
|
|||
|
|
content = f.read()
|
|||
|
|
|
|||
|
|
self.send_response(404)
|
|||
|
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|||
|
|
self.send_header('Content-Length', str(len(content)))
|
|||
|
|
self.end_headers()
|
|||
|
|
self.wfile.write(content)
|
|||
|
|
print(f"返回自定义404页面: {error_page_path}")
|
|||
|
|
else:
|
|||
|
|
# 如果自定义404页面也不存在,返回简单的404
|
|||
|
|
self.send_simple_404()
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"发送自定义404页面时出错: {e}")
|
|||
|
|
self.send_simple_404()
|
|||
|
|
|
|||
|
|
def send_simple_404(self):
|
|||
|
|
"""发送简单的404响应"""
|
|||
|
|
message = """
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html>
|
|||
|
|
<head>
|
|||
|
|
<title>404 - 页面未找到</title>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<style>
|
|||
|
|
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
|||
|
|
h1 { color: #333; }
|
|||
|
|
p { color: #666; }
|
|||
|
|
a { color: #0066cc; text-decoration: none; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<h1>404 - 页面未找到</h1>
|
|||
|
|
<p>抱歉,您访问的页面不存在。</p>
|
|||
|
|
<p><a href="/">返回首页</a></p>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
content = message.encode('utf-8')
|
|||
|
|
self.send_response(404)
|
|||
|
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|||
|
|
self.send_header('Content-Length', str(len(content)))
|
|||
|
|
self.end_headers()
|
|||
|
|
self.wfile.write(content)
|
|||
|
|
|
|||
|
|
def log_message(self, format, *args):
|
|||
|
|
"""自定义日志格式"""
|
|||
|
|
print(f"[{self.log_date_time_string()}] {format % args}")
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
# 检查out目录是否存在
|
|||
|
|
if not os.path.exists("out"):
|
|||
|
|
print("错误: 'out' 目录不存在。请先运行构建命令生成静态文件。")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
# 设置端口
|
|||
|
|
PORT = 8080
|
|||
|
|
if len(sys.argv) > 1:
|
|||
|
|
try:
|
|||
|
|
PORT = int(sys.argv[1])
|
|||
|
|
except ValueError:
|
|||
|
|
print("警告: 无效的端口号,使用默认端口 8080")
|
|||
|
|
|
|||
|
|
# 启动服务器
|
|||
|
|
try:
|
|||
|
|
with socketserver.TCPServer(("", PORT), CustomHTTPRequestHandler) as httpd:
|
|||
|
|
print(f"静态文件服务器已启动")
|
|||
|
|
print(f"访问地址: http://localhost:{PORT}")
|
|||
|
|
print(f"静态文件目录: {os.path.abspath('out')}")
|
|||
|
|
print("按 Ctrl+C 停止服务器")
|
|||
|
|
print("-" * 50)
|
|||
|
|
httpd.serve_forever()
|
|||
|
|
except KeyboardInterrupt:
|
|||
|
|
print("\n服务器已停止")
|
|||
|
|
except OSError as e:
|
|||
|
|
if e.errno == 48: # Address already in use
|
|||
|
|
print(f"端口 {PORT} 已被占用,请尝试其他端口")
|
|||
|
|
print(f"使用方法: python {sys.argv[0]} [端口号]")
|
|||
|
|
else:
|
|||
|
|
print(f"启动服务器时出错: {e}")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|