155 lines
4.4 KiB
JavaScript
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'));
|