import express from "express"; import JSONbig from "json-bigint"; import swaggerUi from "swagger-ui-express"; import swaggerSpecs from "./config/swagger.js"; import config from "./appConfig/index.js"; import { log, logger } from "./utils/log/logUtils.js"; import logCacheManager from "./utils/log/logCacheManager.js"; import { errorHandler, notFoundHandler } from "./utils/error/errorHandler.js"; import router from "./routes/router.js"; import { cleanupInitProjectOnStartup } from "./utils/project/initProjectCleanupUtils.js"; import { startScheduler, stopScheduler } from "./scheduler/pnpmPruneScheduler.js"; import path from "path"; const app = express(); // 解析 JSON 请求体,使用 json-bigint 处理大整数 app.use( express.json({ limit: config.REQUEST_BODY_LIMIT, reviver: (key, value) => { // 对于大整数,保持为字符串 if (typeof value === "number" && !Number.isSafeInteger(value)) { return value.toString(); } return value; }, }) ); // 解析 URL 编码的请求体 app.use( express.urlencoded({ extended: true, limit: config.REQUEST_BODY_LIMIT }) ); // 使用日志中间件 app.use(logger); // 帮助方法:对 URL 路径做尽可能多次解码(处理客户端双重编码等情况) const safeDecodePath = (p) => { let prev = p; try { // 连续解码直到不再变化或解码失败 while (true) { const decoded = decodeURIComponent(prev); if (decoded === prev) break; prev = decoded; } } catch (e) { // 非法编码时直接返回当前结果,避免抛错 } return prev; }; // 静态文件服务:提供页面工程文件的直接访问 // 格式1: /api/page/static// -> config.PROJECT_SOURCE_DIR app.use("/api/page/static/:projectId", (req, res, next) => { const { projectId } = req.params; // req.path 是挂载点之后的路径,形如 "/path/to/file" let filePath = req.path || "/"; if (!projectId || filePath === "/") { return res.status(404).send("Not Found"); } // 设置 CORS 头,允许跨域访问静态资源(必须在 sendFile 之前设置) const origin = req.headers.origin; // 如果请求包含 Origin,使用具体的 origin;否则使用 * // 注意:当使用 credentials 时,不能使用 *,必须使用具体的 origin const allowOrigin = origin || "*"; res.header("Access-Control-Allow-Origin", allowOrigin); res.header("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,DELETE,OPTIONS"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Fragment"); res.header("Access-Control-Expose-Headers", "Content-Type"); if (origin) { res.header("Access-Control-Allow-Credentials", "true"); res.header("Vary", "Origin"); } // 处理 OPTIONS 预检请求 if (req.method === "OPTIONS") { return res.sendStatus(200); } // 去掉前导 "/",转成相对路径 filePath = filePath.replace(/^\/+/, ""); // 对可能被多次编码的路径做安全解码(支持中文等) const decodedPath = safeDecodePath(filePath); const fullPath = path.join(config.PROJECT_SOURCE_DIR, projectId, decodedPath); // 使用 headers 选项确保 CORS 头被保留 const corsHeaders = { "Access-Control-Allow-Origin": allowOrigin, "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Fragment", }; if (origin) { corsHeaders["Access-Control-Allow-Credentials"] = "true"; corsHeaders["Vary"] = "Origin"; } return res.sendFile( fullPath, { // dotfiles: "ignore", dotfiles: "allow", headers: corsHeaders, }, (err) => { if (err) return next(); } ); }); // 静态文件服务:提供桌面文件的直接访问 // 格式2: /api/computer/static/// -> config.COMPUTER_WORKSPACE_DIR app.use("/api/computer/static/:userId/:cId", (req, res, next) => { const { userId, cId } = req.params; let filePath = req.path || "/"; if (!userId || !cId || filePath === "/") { return res.status(404).send("Not Found"); } // 设置 CORS 头,允许跨域访问静态资源(必须在 sendFile 之前设置) const origin = req.headers.origin; // 如果请求包含 Origin,使用具体的 origin;否则使用 * // 注意:当使用 credentials 时,不能使用 *,必须使用具体的 origin const allowOrigin = origin || "*"; res.header("Access-Control-Allow-Origin", allowOrigin); res.header("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,DELETE,OPTIONS"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Range, If-Range"); res.header("Access-Control-Expose-Headers", "Content-Type, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified"); if (origin) { res.header("Access-Control-Allow-Credentials", "true"); res.header("Vary", "Origin"); } // 处理 OPTIONS 预检请求 if (req.method === "OPTIONS") { return res.sendStatus(200); } // 去掉前导 "/",确保是相对路径 filePath = filePath.replace(/^\/+/, ""); // 安全多次解码,解决 "%25E4%25BD%25A0" 这种双重编码的中文 const decodedPath = safeDecodePath(filePath); const fullPath = path.join( config.COMPUTER_WORKSPACE_DIR, userId, cId, decodedPath ); // 使用 headers 选项确保 CORS 头被保留 const corsHeaders = { "Access-Control-Allow-Origin": allowOrigin, "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control, Range, If-Range", "Access-Control-Expose-Headers": "Content-Type, Content-Length, Content-Range, Accept-Ranges, ETag, Last-Modified", }; if (origin) { corsHeaders["Access-Control-Allow-Credentials"] = "true"; corsHeaders["Vary"] = "Origin"; } return res.sendFile( fullPath, { // dotfiles: "ignore", dotfiles: "allow", headers: corsHeaders, }, (err) => { if (err) return next(); } ); }); // Swagger API 文档 app.use( "/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { customCss: ".swagger-ui .topbar { display: none }", customSiteTitle: "qiming-file-server API Documentation", }) ); // 使用路由配置 app.use(router); // 404处理中间件(必须在所有路由之后) app.use(notFoundHandler); // 全局错误处理中间件(必须在最后) app.use(errorHandler); // 启动服务器 const server = app.listen(config.PORT, async () => { log( "default", "INFO", `Server is running on port ${config.PORT} (${config.NODE_ENV} mode)` ); // 项目启动时清理初始化项目文件夹,目的是更新初始化包时,只需要更新zip包 await cleanupInitProjectOnStartup(config); // 启动 pnpm prune 定时任务 try { startScheduler(); } catch (error) { log("default", "ERROR", `pnpm prune scheduled task failed to start: ${error.message}`); } }); // 设置服务器超时时间 10 分钟(默认是 120 秒) // 对于长时间运行的操作(如依赖安装、构建)是必要的 server.timeout = 600000; // 10 分钟 server.keepAliveTimeout = 610000; // 略大于 timeout server.headersTimeout = 620000; // 略大于 keepAliveTimeout // 优雅退出处理 const gracefulShutdown = (signal) => { log("default", "INFO", `Received ${signal} signal, preparing graceful exit...`); // 停止定时任务 stopScheduler(); // 清理日志缓存管理器(清理定时器) try { logCacheManager.destroy(); log("default", "INFO", "Log cache manager cleared"); } catch (error) { log("default", "ERROR", `Failed to clear log cache manager: ${error.message}`); } // 关闭服务器 server.close(() => { log("default", "INFO", "Server closed"); process.exit(0); }); // 如果 30 秒后还未退出,强制退出 setTimeout(() => { log("default", "ERROR", "Force exit (timeout)"); process.exit(1); }, 30000); }; // 监听退出信号 process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT'));