AwsLinker/scripts/serve-static.js
2025-09-16 17:19:58 +08:00

325 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* 自定义静态文件服务器支持自定义404页面和Next.js静态导出
* 用于部署Next.js静态导出的文件
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
class StaticServer {
constructor(directory = 'out', port = 8080) {
this.directory = path.resolve(directory);
this.port = port;
// MIME类型映射
this.mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject'
};
}
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
return this.mimeTypes[ext] || 'application/octet-stream';
}
ensureErrorPages() {
// 确保404.html存在于out目录
const out404Path = path.join(this.directory, '404.html');
const public404Path = path.join(__dirname, '..', 'public', '404.html');
if (!fs.existsSync(out404Path) && fs.existsSync(public404Path)) {
try {
fs.copyFileSync(public404Path, out404Path);
console.log('✅ 已复制404.html到out目录');
} catch (error) {
console.log('⚠️ 无法复制404.html:', error.message);
}
}
}
sendFile(res, filePath, statusCode = 200) {
try {
if (!fs.existsSync(filePath)) {
this.send404(res);
return;
}
const stat = fs.statSync(filePath);
const content = fs.readFileSync(filePath);
const mimeType = this.getMimeType(filePath);
res.writeHead(statusCode, {
'Content-Type': mimeType,
'Content-Length': stat.size,
'Cache-Control': filePath.endsWith('.html')
? 'no-cache, no-store, must-revalidate'
: 'public, max-age=31536000',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block'
});
res.end(content);
console.log(`${statusCode} ${filePath}`);
} catch (error) {
console.error(`✗ 错误读取文件 ${filePath}:`, error.message);
this.send500(res, error);
}
}
send404(res) {
// 尝试找到404页面
const custom404Paths = [
path.join(this.directory, '404.html'),
path.join(this.directory, '404', 'index.html'),
path.join(__dirname, '..', 'public', '404.html')
];
for (const custom404Path of custom404Paths) {
if (fs.existsSync(custom404Path)) {
console.log(`✓ 返回自定义404页面: ${custom404Path}`);
this.sendFile(res, custom404Path, 404);
return;
}
}
// 如果没有找到自定义404页面返回简单的404
const content = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 - 页面未找到 - AwsLinker</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #333;
}
.container {
max-width: 500px;
width: 90%;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 3rem 2rem;
text-align: center;
}
.error-code {
font-size: 4rem;
font-weight: 800;
color: #667eea;
margin-bottom: 1rem;
}
.error-title {
font-size: 1.5rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 1rem;
}
.error-description {
color: #718096;
margin-bottom: 2rem;
line-height: 1.6;
}
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
transition: transform 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container">
<div class="error-code">404</div>
<h1 class="error-title">页面未找到</h1>
<p class="error-description">抱歉,您访问的页面不存在或已被移除。</p>
<a href="/" class="btn">返回首页</a>
</div>
</body>
</html>`;
res.writeHead(404, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(content, 'utf8')
});
res.end(content);
console.log(`✗ 404 返回默认404页面`);
}
send500(res, error) {
const content = `
<!DOCTYPE html>
<html>
<head>
<title>500 - 服务器错误</title>
<meta charset="utf-8">
</head>
<body>
<h1>500 - 服务器内部错误</h1>
<p>服务器处理请求时发生错误。</p>
<p><a href="/">返回首页</a></p>
</body>
</html>`;
res.writeHead(500, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(content, 'utf8')
});
res.end(content);
console.log(`✗ 500 服务器错误:`, error.message);
}
handleRequest(req, res) {
const parsedUrl = url.parse(req.url, true);
let pathname = decodeURIComponent(parsedUrl.pathname);
// 移除查询参数和fragment
pathname = pathname.split('?')[0].split('#')[0];
console.log(`${req.method} ${pathname}`);
// 安全检查:防止目录遍历攻击
if (pathname.includes('..') || pathname.includes('\0')) {
this.send404(res);
return;
}
// 处理根路径
if (pathname === '/') {
const indexPath = path.join(this.directory, 'index.html');
this.sendFile(res, indexPath);
return;
}
// 构建文件路径
let filePath = path.join(this.directory, pathname.slice(1));
// 检查文件是否存在
if (fs.existsSync(filePath)) {
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 如果是目录尝试找index.html
const indexPath = path.join(filePath, 'index.html');
if (fs.existsSync(indexPath)) {
this.sendFile(res, indexPath);
} else {
this.send404(res);
}
} else {
// 文件存在,直接返回
this.sendFile(res, filePath);
}
} else {
// 文件不存在,尝试添加.html扩展名
const htmlPath = filePath + '.html';
if (fs.existsSync(htmlPath)) {
this.sendFile(res, htmlPath);
} else {
// 尝试在index.html中查找适用于Next.js路由
const indexPath = path.join(filePath, 'index.html');
if (fs.existsSync(indexPath)) {
this.sendFile(res, indexPath);
} else {
this.send404(res);
}
}
}
}
start() {
// 检查输出目录是否存在
if (!fs.existsSync(this.directory)) {
console.error(`❌ 错误: 目录 '${this.directory}' 不存在`);
console.error('请先运行构建命令生成静态文件npm run build:static');
process.exit(1);
}
// 确保404.html存在于out目录
this.ensureErrorPages();
const server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
server.listen(this.port, () => {
console.log('🚀 静态文件服务器已启动');
console.log(`📁 服务目录: ${this.directory}`);
console.log(`🌐 访问地址: http://localhost:${this.port}`);
console.log('⌨️ 按 Ctrl+C 停止服务器');
console.log('━'.repeat(50));
});
// 优雅关闭
process.on('SIGINT', () => {
console.log('\n🛑 正在关闭服务器...');
server.close(() => {
console.log('✅ 服务器已停止');
process.exit(0);
});
});
process.on('SIGTERM', () => {
console.log('\n🛑 收到终止信号,正在关闭服务器...');
server.close(() => {
console.log('✅ 服务器已停止');
process.exit(0);
});
});
}
}
// 主函数
function main() {
const args = process.argv.slice(2);
const port = args[0] ? parseInt(args[0]) : 8080;
const directory = args[1] || 'out';
if (isNaN(port) || port < 1 || port > 65535) {
console.error('❌ 错误: 无效的端口号');
console.log('使用方法: node serve-static.js [端口号] [目录]');
process.exit(1);
}
const server = new StaticServer(directory, port);
server.start();
}
if (require.main === module) {
main();
}
module.exports = StaticServer;