2026-04-07 15:43:13 +08:00

155 lines
4.4 KiB
JavaScript

/**
* Bun Web服务器脚本
* 提供静态文件服务和API接口
*/
const fs = require('fs');
const path = require('path');
const BASE_DIR = path.resolve(process.env.BASE_DIR || process.cwd());
const WEB_DIR = path.join(BASE_DIR, 'web');
const DATA_DIR = path.join(BASE_DIR, 'data');
const PIC_DIR = path.join(BASE_DIR, 'pic');
const PUBLIC_DIR = path.join(BASE_DIR, 'public');
const PORT = Number(process.env.PORT || 8080);
const HOST = process.env.HOST || '127.0.0.1';
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
};
function json(data, status = 200) {
return Response.json(data, {
status,
headers: CORS_HEADERS
});
}
function text(body, status = 200) {
return new Response(body, {
status,
headers: CORS_HEADERS
});
}
function resolveSafePath(baseDir, requestPath) {
const relativePath = requestPath.replace(/^\/+/, '');
const resolvedPath = path.resolve(baseDir, relativePath);
if (resolvedPath === baseDir || resolvedPath.startsWith(`${baseDir}${path.sep}`)) {
return resolvedPath;
}
return null;
}
function listAvailableDates() {
try {
const files = fs.readdirSync(DATA_DIR);
return files
.filter((file) => file.endsWith('.json') && !file.includes('_raw') && !file.includes('test'))
.map((file) => file.replace('.json', ''))
.sort()
.reverse();
} catch {
return [];
}
}
async function serveFile(filePath) {
const file = Bun.file(filePath);
if (!(await file.exists())) {
return text('Not Found', 404);
}
return new Response(file, {
status: 200,
headers: CORS_HEADERS
});
}
const server = Bun.serve({
hostname: HOST,
port: PORT,
async fetch(req) {
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: CORS_HEADERS
});
}
try {
const { pathname } = new URL(req.url);
if (pathname === '/api/dates') {
return json({ dates: listAvailableDates() });
}
if (pathname.startsWith('/api/data/')) {
const date = pathname.replace('/api/data/', '');
const filePath = resolveSafePath(DATA_DIR, `${date}.json`);
if (!filePath) {
return json({ error: '非法路径' }, 403);
}
const file = Bun.file(filePath);
if (!(await file.exists())) {
return json({ error: '数据不存在' }, 404);
}
return new Response(file, {
status: 200,
headers: {
...CORS_HEADERS,
'Content-Type': 'application/json; charset=utf-8'
}
});
}
let filePath;
if (pathname === '/') {
filePath = path.join(WEB_DIR, 'index.html');
} else if (pathname.startsWith('/data/')) {
filePath = resolveSafePath(DATA_DIR, pathname.slice('/data/'.length));
} else if (pathname.startsWith('/pic/')) {
filePath = resolveSafePath(PIC_DIR, pathname.slice('/pic/'.length));
} else if (pathname.startsWith('/public/')) {
filePath = resolveSafePath(PUBLIC_DIR, pathname.slice('/public/'.length));
} else {
filePath = resolveSafePath(WEB_DIR, pathname);
}
if (!filePath) {
return text('Forbidden', 403);
}
return serveFile(filePath);
} catch (error) {
console.error('服务器错误:', error);
return text('Internal Server Error', 500);
}
}
});
console.log('==========================================');
console.log('北京市房地产数据监控服务器已启动');
console.log(`访问地址: http://${HOST}:${PORT}`);
console.log('按 Ctrl+C 停止服务器');
console.log('==========================================');
function shutdown(signal) {
console.log(`\n${signal}\n正在关闭服务器...`);
server.stop(true);
console.log('服务器已关闭');
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));