/** * 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'));