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() |