Files
qiming/qiming-file-server/src/server.js

254 lines
8.2 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.
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/<projectId>/<path/to/file> -> 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/<userId>/<cId>/<path/to/file> -> 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'));