chore: initialize qiming workspace repository
This commit is contained in:
231
qiming-file-server/src/routes/buildRoutes.js
Normal file
231
qiming-file-server/src/routes/buildRoutes.js
Normal file
@@ -0,0 +1,231 @@
|
||||
import express from "express";
|
||||
import { startDevServer } from "../utils/build/startDevUtils.js";
|
||||
import { restartDevServer } from "../utils/build/restartDevUtils.js";
|
||||
import { keepAliveDevServer } from "../utils/build/keepAliveDevUtils.js";
|
||||
import { stopDevServer } from "../utils/build/stopDevUtils.js";
|
||||
import { buildProject } from "../utils/build/buildProjectUtils.js";
|
||||
import { listRunningProcesses } from "../utils/build/processManager.js";
|
||||
import BuildErrorParser from "../utils/error/buildErrorParser.js";
|
||||
import { getDevLog } from "../utils/log/getDevLogUtils.js";
|
||||
import logCacheManager from "../utils/log/logCacheManager.js";
|
||||
import portPool from "../utils/buildArg/portPool.js";
|
||||
import {
|
||||
ValidationError,
|
||||
BusinessError,
|
||||
SystemError,
|
||||
ProcessError,
|
||||
asyncHandler,
|
||||
} from "../utils/error/errorHandler.js";
|
||||
|
||||
const buildRouter = express.Router();
|
||||
|
||||
// 路由配置
|
||||
const routes = [
|
||||
{
|
||||
path: "/start-dev",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const projectId = req.query.projectId;
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
// 设置长超时时间:依赖安装可能需要较长时间
|
||||
req.setTimeout(600000); // 10分钟
|
||||
res.setTimeout(600000); // 10分钟
|
||||
|
||||
const result = await startDevServer(req, String(projectId));
|
||||
res.json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/keep-alive",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const projectId = req.query.projectId;
|
||||
const pid = req.query.pid;
|
||||
const port = req.query.port;
|
||||
const basePath = req.query.basePath;
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (!pid) {
|
||||
throw new ValidationError("Process ID cannot be empty", { field: "pid" });
|
||||
}
|
||||
if (!port) {
|
||||
throw new ValidationError("Port cannot be empty", { field: "port" });
|
||||
}
|
||||
if (!basePath) {
|
||||
throw new ValidationError("basePath cannot be empty", { field: "basePath" });
|
||||
}
|
||||
|
||||
const result = await keepAliveDevServer(
|
||||
req,
|
||||
String(projectId),
|
||||
pid,
|
||||
port,
|
||||
basePath,
|
||||
);
|
||||
res.json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/build",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const projectId = req.query.projectId;
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
// 设置长超时时间:构建可能需要较长时间
|
||||
req.setTimeout(600000); // 10分钟
|
||||
res.setTimeout(600000); // 10分钟
|
||||
|
||||
const result = await buildProject(req, String(projectId));
|
||||
res.json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/stop-dev",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const projectId = req.query.projectId;
|
||||
const pid = req.query.pid;
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (!pid) {
|
||||
throw new ValidationError("Process ID cannot be empty", { field: "pid" });
|
||||
}
|
||||
|
||||
const result = await stopDevServer(req, String(projectId), pid, {
|
||||
strict: true,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/restart-dev",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const projectId = req.query.projectId;
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
// 设置长超时时间:重启包含依赖安装,可能需要较长时间
|
||||
req.setTimeout(600000); // 10分钟
|
||||
res.setTimeout(600000); // 10分钟
|
||||
|
||||
const result = await restartDevServer(req, String(projectId));
|
||||
res.json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/list-dev",
|
||||
method: "get",
|
||||
handler: (req, res) => {
|
||||
const list = listRunningProcesses();
|
||||
const result = { success: true, list };
|
||||
res.json(result);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/parse-build-error",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, errorMessage } = req.body;
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (!errorMessage) {
|
||||
throw new ValidationError("Error message cannot be empty", {
|
||||
field: "errorMessage",
|
||||
});
|
||||
}
|
||||
|
||||
const errorParser = new BuildErrorParser();
|
||||
const userFriendlyMessage = errorParser.parseBuildError(
|
||||
errorMessage,
|
||||
String(projectId)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: userFriendlyMessage,
|
||||
});
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/get-dev-log",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const projectId = req.query.projectId;
|
||||
const startIndex = req.query.startIndex;
|
||||
const logType = req.query.logType || "temp";
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
// 解析起始行号,默认为1
|
||||
const startLine = startIndex ? parseInt(startIndex, 10) : 1;
|
||||
if (isNaN(startLine) || startLine < 1) {
|
||||
throw new ValidationError("Start index must be a positive integer (starting from 1)", {
|
||||
field: "startIndex",
|
||||
value: startIndex,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await getDevLog(String(projectId), startLine, logType);
|
||||
res.json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/get-log-cache-stats",
|
||||
method: "get",
|
||||
handler: (req, res) => {
|
||||
const stats = logCacheManager.getStats();
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Get log cache statistics successfully",
|
||||
stats,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/clear-all-log-cache",
|
||||
method: "get",
|
||||
handler: (req, res) => {
|
||||
logCacheManager.clear();
|
||||
res.json({
|
||||
success: true,
|
||||
message: "All log caches have been cleared",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/port-pool-status",
|
||||
method: "get",
|
||||
handler: (req, res) => {
|
||||
const status = portPool.getStatus();
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Get port pool status successfully",
|
||||
...status,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 注册路由
|
||||
routes.forEach((route) => {
|
||||
buildRouter[route.method](route.path, route.handler);
|
||||
});
|
||||
|
||||
export default buildRouter;
|
||||
222
qiming-file-server/src/routes/codeRoutes.js
Normal file
222
qiming-file-server/src/routes/codeRoutes.js
Normal file
@@ -0,0 +1,222 @@
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
import { asyncHandler, ValidationError } from "../utils/error/errorHandler.js";
|
||||
import codeService from "../service/codeService.js";
|
||||
import { log } from "../utils/log/logUtils.js";
|
||||
import config from "../appConfig/index.js";
|
||||
import { extractIsolationContext } from "../utils/common/projectPathUtils.js";
|
||||
|
||||
const codeRouter = express.Router();
|
||||
|
||||
// 配置multer用于内存存储(适合小文件)
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: config.UPLOAD_SINGLE_FILE_SIZE_BYTES, // 从环境变量读取文件大小限制
|
||||
},
|
||||
});
|
||||
|
||||
// 路由配置
|
||||
const routes = [
|
||||
{
|
||||
path: "/specified-files-update",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion, files } = req.body || {};
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
log(projectId, "INFO", "Partial files update", {
|
||||
projectId,
|
||||
codeVersion,
|
||||
filesCount: files ? files.length : 0,
|
||||
});
|
||||
|
||||
// 解码文件内容(前端使用 encodeURIComponent 编码)
|
||||
if (files && Array.isArray(files)) {
|
||||
files.forEach((fileOp) => {
|
||||
if (!fileOp) return;
|
||||
|
||||
// 只要有 contents 就解码
|
||||
if (typeof fileOp.contents === "string" && fileOp.contents) {
|
||||
try {
|
||||
fileOp.contents = decodeURIComponent(fileOp.contents);
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Decode file content failed", {
|
||||
fileName: fileOp.path,
|
||||
error: err.message,
|
||||
});
|
||||
// 如果解码失败,保持原样(可能前端没有编码)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const result = await codeService.specifiedFilesUpdate(
|
||||
String(projectId),
|
||||
String(codeVersion),
|
||||
files,
|
||||
req,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/all-files-update",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion, files, basePath, pid } = req.body || {};
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
log(projectId, "INFO", "Submit files", {
|
||||
projectId,
|
||||
codeVersion,
|
||||
basePath,
|
||||
pid,
|
||||
});
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (codeVersion === undefined || codeVersion === null) {
|
||||
throw new ValidationError("codeVersion cannot be empty", {
|
||||
field: "codeVersion",
|
||||
});
|
||||
}
|
||||
if (!Array.isArray(files)) {
|
||||
throw new ValidationError("files must be an array", { field: "files" });
|
||||
}
|
||||
|
||||
// 解码文件内容(前端使用 encodeURIComponent 编码)
|
||||
if (files && Array.isArray(files)) {
|
||||
files.forEach((file) => {
|
||||
if (file && typeof file.contents === "string" && file.contents) {
|
||||
try {
|
||||
file.contents = decodeURIComponent(file.contents);
|
||||
} catch (err) {
|
||||
log(projectId, "WARN", "Decode file content failed", {
|
||||
fileName: file.name,
|
||||
error: err.message,
|
||||
});
|
||||
// 如果解码失败,保持原样(可能前端没有编码)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const result = await codeService.allFilesUpdate(
|
||||
String(projectId),
|
||||
String(codeVersion),
|
||||
files,
|
||||
req,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/upload-single-file",
|
||||
method: "post",
|
||||
middleware: upload.single("file"),
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion, filePath } = req.body || {};
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
const file = req.file; // 从 multer 中间件获取上传的文件
|
||||
|
||||
log(projectId, "INFO", "上传单个文件", {
|
||||
projectId,
|
||||
codeVersion,
|
||||
filePath,
|
||||
});
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (codeVersion === undefined || codeVersion === null) {
|
||||
throw new ValidationError("codeVersion cannot be empty", {
|
||||
field: "codeVersion",
|
||||
});
|
||||
}
|
||||
if (!file) {
|
||||
throw new ValidationError("File cannot be empty", { field: "file" });
|
||||
}
|
||||
if (!filePath || typeof filePath !== "string") {
|
||||
throw new ValidationError("File path cannot be empty", { field: "filePath" });
|
||||
}
|
||||
|
||||
// 记录接收到的文件信息,用于调试
|
||||
log(projectId, "INFO", "Received file information", {
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
bufferLength: file.buffer ? file.buffer.length : 0,
|
||||
bufferIsBuffer: Buffer.isBuffer(file.buffer),
|
||||
});
|
||||
|
||||
// 构建文件对象,统一使用buffer(multer memoryStorage对所有文件类型都提供buffer)
|
||||
const fileObj = {
|
||||
buffer: file.buffer,
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
};
|
||||
|
||||
const result = await codeService.uploadSingleFile(
|
||||
String(projectId),
|
||||
String(codeVersion),
|
||||
fileObj,
|
||||
filePath,
|
||||
req,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/rollback-version",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion, rollbackTo } = req.body || {};
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
log(projectId, "INFO", "Rollback version", {
|
||||
projectId,
|
||||
codeVersion,
|
||||
rollbackTo,
|
||||
});
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (codeVersion === undefined || codeVersion === null) {
|
||||
throw new ValidationError("codeVersion cannot be empty", {
|
||||
field: "codeVersion",
|
||||
});
|
||||
}
|
||||
if (rollbackTo === undefined || rollbackTo === null) {
|
||||
throw new ValidationError("rollbackTo cannot be empty", {
|
||||
field: "rollbackTo",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await codeService.rollbackVersion(
|
||||
String(projectId),
|
||||
String(codeVersion),
|
||||
String(rollbackTo),
|
||||
req,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
// 注册路由
|
||||
routes.forEach((route) => {
|
||||
if (route.middleware) {
|
||||
// 如果有中间件,先注册中间件再注册处理器
|
||||
codeRouter[route.method](route.path, route.middleware, route.handler);
|
||||
} else {
|
||||
// 没有中间件,直接注册处理器
|
||||
codeRouter[route.method](route.path, route.handler);
|
||||
}
|
||||
});
|
||||
|
||||
export default codeRouter;
|
||||
414
qiming-file-server/src/routes/computerRoutes.js
Normal file
414
qiming-file-server/src/routes/computerRoutes.js
Normal file
@@ -0,0 +1,414 @@
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ValidationError, asyncHandler } from "../utils/error/errorHandler.js";
|
||||
import { log } from "../utils/log/logUtils.js";
|
||||
import config from "../appConfig/index.js";
|
||||
import { createWorkspace, pushSkillsToWorkspace, } from "../utils/computer/computerUtils.js";
|
||||
import {
|
||||
getFileList,
|
||||
updateFiles,
|
||||
uploadFile,
|
||||
uploadFiles,
|
||||
downloadAllFiles,
|
||||
} from "../utils/computer/computerFileUtils.js";
|
||||
|
||||
const computerRouter = express.Router();
|
||||
|
||||
// 使用磁盘存储,便于后续解压 zip
|
||||
const upload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
try {
|
||||
// 将临时上传目录放到 COMPUTER_WORKSPACE_DIR/<userId>/<cId>/.tmp 下
|
||||
const baseDir = config.COMPUTER_WORKSPACE_DIR;
|
||||
if (!baseDir) {
|
||||
return cb(
|
||||
new Error("COMPUTER_WORKSPACE_DIR is not configured, cannot determine upload temporary directory")
|
||||
);
|
||||
}
|
||||
|
||||
const userId = req.body?.userId || "unknown";
|
||||
const cId = req.body?.cId || "unknown";
|
||||
const tmpUploadDir = path.join(
|
||||
baseDir,
|
||||
String(userId),
|
||||
String(cId),
|
||||
".tmp"
|
||||
);
|
||||
if (!fs.existsSync(tmpUploadDir)) {
|
||||
fs.mkdirSync(tmpUploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
cb(null, tmpUploadDir);
|
||||
} catch (err) {
|
||||
cb(err);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname) || ".zip";
|
||||
const baseName = path.basename(file.originalname, ext);
|
||||
const uniqueSuffix = `${Date.now()}_${Math.round(Math.random() * 1e6)}`;
|
||||
cb(null, `${baseName}_${uniqueSuffix}${ext}`);
|
||||
},
|
||||
}),
|
||||
limits: {
|
||||
fileSize: config.UPLOAD_MAX_FILE_SIZE_BYTES,
|
||||
},
|
||||
});
|
||||
|
||||
// Multer 错误处理中间件(复用通用模式)
|
||||
function multerErrorHandler(err, req, res, next) {
|
||||
if (err && err.name === "MulterError") {
|
||||
return next(err);
|
||||
}
|
||||
if (err && (err.name === "ValidationError" || err instanceof ValidationError)) {
|
||||
return next(err);
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: "/create-workspace",
|
||||
method: "post",
|
||||
middleware: upload.single("file"),
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId } = req.body || {};
|
||||
const file = req.file || null;
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
|
||||
log(logId, "INFO", "Create workspace request", {
|
||||
userId,
|
||||
cId,
|
||||
hasFile: !!file,
|
||||
fileName: file?.originalname,
|
||||
fileSize: file?.size,
|
||||
});
|
||||
|
||||
// 兼容老逻辑:仅处理上传 file,不处理 skillUrls
|
||||
const result = await createWorkspace(userId, cId, file);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/create-workspace-v2",
|
||||
method: "post",
|
||||
middleware: upload.single("file"),
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId, skillUrls } = req.body || {};
|
||||
const file = req.file || null;
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
|
||||
// multipart/form-data 中数组可能被序列化成 JSON 字符串
|
||||
let normalizedSkillUrls = skillUrls;
|
||||
if (typeof skillUrls === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(skillUrls);
|
||||
normalizedSkillUrls = Array.isArray(parsed) ? parsed : [skillUrls];
|
||||
} catch {
|
||||
normalizedSkillUrls = [skillUrls];
|
||||
}
|
||||
}
|
||||
|
||||
log(logId, "INFO", "Create workspace v2 request", {
|
||||
userId,
|
||||
cId,
|
||||
hasFile: !!file,
|
||||
fileName: file?.originalname,
|
||||
fileSize: file?.size,
|
||||
skillUrlsCount: Array.isArray(normalizedSkillUrls) ? normalizedSkillUrls.length : 0,
|
||||
});
|
||||
|
||||
const result = await createWorkspace(userId, cId, file, normalizedSkillUrls);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/push-skills-to-workspace",
|
||||
method: "post",
|
||||
middleware: upload.single("file"),
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId } = req.body || {};
|
||||
const file = req.file || null;
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
|
||||
log(logId, "INFO", "推送技能到工作空间请求", {
|
||||
userId,
|
||||
cId,
|
||||
hasFile: !!file,
|
||||
fileName: file?.originalname,
|
||||
fileSize: file?.size,
|
||||
});
|
||||
|
||||
// 兼容老逻辑:仅处理上传 file,不处理 skillUrls
|
||||
const result = await pushSkillsToWorkspace(userId, cId, file);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/push-skills-to-workspace-v2",
|
||||
method: "post",
|
||||
middleware: upload.single("file"),
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId, skillUrls } = req.body || {};
|
||||
const file = req.file || null;
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
|
||||
// multipart/form-data 中数组可能被序列化成 JSON 字符串
|
||||
let normalizedSkillUrls = skillUrls;
|
||||
if (typeof skillUrls === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(skillUrls);
|
||||
normalizedSkillUrls = Array.isArray(parsed) ? parsed : [skillUrls];
|
||||
} catch {
|
||||
normalizedSkillUrls = [skillUrls];
|
||||
}
|
||||
}
|
||||
|
||||
log(logId, "INFO", "推送技能到工作空间请求(v2)", {
|
||||
userId,
|
||||
cId,
|
||||
hasFile: !!file,
|
||||
fileName: file?.originalname,
|
||||
fileSize: file?.size,
|
||||
skillUrlsCount: Array.isArray(normalizedSkillUrls) ? normalizedSkillUrls.length : 0,
|
||||
});
|
||||
|
||||
const result = await pushSkillsToWorkspace(userId, cId, file, normalizedSkillUrls);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/get-file-list",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId, proxyPath } = req.query;
|
||||
const result = await getFileList(userId, cId, proxyPath);
|
||||
res.status(200).json({ success: true, ...result });
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/files-update",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId, files } = req.body || {};
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
log(logId, "INFO", "Files update", {
|
||||
userId,
|
||||
cId,
|
||||
filesCount: files ? files.length : 0,
|
||||
});
|
||||
|
||||
// 解码文件内容(前端使用 encodeURIComponent 编码)
|
||||
if (files && Array.isArray(files)) {
|
||||
files.forEach((fileOp) => {
|
||||
if (!fileOp) return;
|
||||
|
||||
// 只要有 contents 就解码
|
||||
if (typeof fileOp.contents === "string" && fileOp.contents) {
|
||||
try {
|
||||
fileOp.contents = decodeURIComponent(fileOp.contents);
|
||||
} catch (err) {
|
||||
log(logId, "WARN", "Decode file content failed", {
|
||||
fileName: fileOp.name,
|
||||
error: err.message,
|
||||
});
|
||||
// 如果解码失败,保持原样(可能前端没有编码)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const result = await updateFiles(userId, cId, files);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/upload-file",
|
||||
method: "post",
|
||||
middleware: upload.single("file"),
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId, filePath } = req.body || {};
|
||||
const file = req.file; // 从 multer 中间件获取上传的文件
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
|
||||
log(logId, "INFO", "Upload single file", {
|
||||
userId,
|
||||
cId,
|
||||
filePath,
|
||||
});
|
||||
|
||||
// 从磁盘读取文件内容(diskStorage 模式下 file.buffer 不存在)
|
||||
const fileBuffer = await fs.promises.readFile(file.path);
|
||||
|
||||
// 构建文件对象,支持文本和二进制文件
|
||||
const fileObj = {
|
||||
buffer: fileBuffer,
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await uploadFile(userId, cId, fileObj, filePath);
|
||||
res.status(200).json(result);
|
||||
} finally {
|
||||
// 清理临时文件
|
||||
if (fs.existsSync(file.path)) {
|
||||
await fs.promises.unlink(file.path);
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/upload-files",
|
||||
method: "post",
|
||||
middleware: upload.array("files"),
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId, filePaths } = req.body || {};
|
||||
const files = req.files || []; // 从 multer 中间件获取上传的文件数组
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
|
||||
// 标准化 filePaths 为数组格式,兼容单文件上传时可能传入字符串的情况
|
||||
const normalizedFilePaths = Array.isArray(filePaths)
|
||||
? filePaths
|
||||
: typeof filePaths === "string"
|
||||
? [filePaths]
|
||||
: filePaths;
|
||||
|
||||
log(logId, "INFO", "Batch upload files request", {
|
||||
userId,
|
||||
cId,
|
||||
filesCount: files.length,
|
||||
filePathsCount: Array.isArray(normalizedFilePaths) ? normalizedFilePaths.length : 0,
|
||||
});
|
||||
|
||||
// 从磁盘读取所有文件内容(diskStorage 模式下 file.buffer 不存在)
|
||||
const fileObjects = [];
|
||||
const tempFilePaths = [];
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
tempFilePaths.push(file.path);
|
||||
const fileBuffer = await fs.promises.readFile(file.path);
|
||||
fileObjects.push({
|
||||
buffer: fileBuffer,
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
});
|
||||
}
|
||||
|
||||
// 调用批量上传工具函数
|
||||
const result = await uploadFiles(userId, cId, fileObjects, normalizedFilePaths);
|
||||
res.status(200).json(result);
|
||||
} finally {
|
||||
// 清理所有临时文件
|
||||
for (const tempPath of tempFilePaths) {
|
||||
if (fs.existsSync(tempPath)) {
|
||||
try {
|
||||
await fs.promises.unlink(tempPath);
|
||||
} catch (error) {
|
||||
log(logId, "WARN", "Clean temporary file failed", {
|
||||
tempPath,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/download-all-files",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { userId, cId } = req.query || {};
|
||||
const logId = `computer:${userId}:${cId}`;
|
||||
|
||||
log(logId, "INFO", "Download all files request", {
|
||||
userId,
|
||||
cId,
|
||||
});
|
||||
|
||||
const { archive, zipFileName } = await downloadAllFiles(
|
||||
userId,
|
||||
cId
|
||||
);
|
||||
|
||||
res.setHeader("Content-Type", "application/zip");
|
||||
// 兼容中文文件名
|
||||
const encodedName = encodeURIComponent(zipFileName);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${encodedName}"; filename*=UTF-8''${encodedName}`
|
||||
);
|
||||
|
||||
archive.on("error", (err) => {
|
||||
// 让全局错误处理中间件接管
|
||||
res.destroy(err);
|
||||
});
|
||||
|
||||
archive.pipe(res);
|
||||
archive.finalize();
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (route.middleware) {
|
||||
// 如果有中间件,先注册中间件再注册处理器
|
||||
computerRouter[route.method](route.path, route.middleware, route.handler);
|
||||
} else if (route.customHandler) {
|
||||
// 对于有自定义处理器的路由
|
||||
const middlewares = [];
|
||||
|
||||
// 先添加multer中间件
|
||||
if (route.handler) {
|
||||
middlewares.push(route.handler);
|
||||
}
|
||||
|
||||
// 再添加文件名解码中间件(如果有)
|
||||
if (route.decodeMiddleware) {
|
||||
middlewares.push(route.decodeMiddleware);
|
||||
}
|
||||
|
||||
// 添加错误处理中间件
|
||||
middlewares.push(multerErrorHandler);
|
||||
|
||||
// 最后添加自定义处理器
|
||||
middlewares.push(route.customHandler);
|
||||
|
||||
// 注册所有中间件
|
||||
computerRouter[route.method](route.path, ...middlewares);
|
||||
} else {
|
||||
// 普通路由直接注册处理器
|
||||
computerRouter[route.method](route.path, route.handler);
|
||||
}
|
||||
});
|
||||
|
||||
export default computerRouter;
|
||||
|
||||
|
||||
552
qiming-file-server/src/routes/projectRoutes.js
Normal file
552
qiming-file-server/src/routes/projectRoutes.js
Normal file
@@ -0,0 +1,552 @@
|
||||
import express from "express";
|
||||
import multer from "multer";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import iconv from "iconv-lite";
|
||||
import projectService from "../service/projectService.js";
|
||||
import {
|
||||
getProjectContent,
|
||||
getProjectContentByVersion,
|
||||
} from "../utils/project/getContentUtils.js";
|
||||
import { uploadAttachmentFile } from "../utils/project/uploadAttachmentFileUtils.js";
|
||||
import { copyProject } from "../utils/project/copyProjectUtils.js";
|
||||
import config from "../appConfig/index.js";
|
||||
import { log } from "../utils/log/logUtils.js";
|
||||
import {
|
||||
extractIsolationContext,
|
||||
resolveProjectPath,
|
||||
} from "../utils/common/projectPathUtils.js";
|
||||
import {
|
||||
ValidationError,
|
||||
SystemError,
|
||||
asyncHandler,
|
||||
} from "../utils/error/errorHandler.js";
|
||||
|
||||
const projectRouter = express.Router();
|
||||
|
||||
// 配置multer用于文件上传
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
// 先保存到临时目录,后续在路由处理器中移动到项目目录
|
||||
const uploadDir = path.join(config.UPLOAD_PROJECT_DIR, "temp");
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e6);
|
||||
cb(
|
||||
null,
|
||||
file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: function (req, file, cb) {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
if (config.UPLOAD_ALLOWED_EXTENSIONS.includes(ext)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
// 将文件类型错误转换为 ValidationError
|
||||
cb(
|
||||
new ValidationError("File type not allowed", {
|
||||
fileExtension: ext,
|
||||
allowedExtensions: config.UPLOAD_ALLOWED_EXTENSIONS,
|
||||
}),
|
||||
false
|
||||
);
|
||||
}
|
||||
},
|
||||
limits: {
|
||||
fileSize: config.UPLOAD_MAX_FILE_SIZE_BYTES,
|
||||
},
|
||||
});
|
||||
|
||||
// 为上传附件文件创建独立的multer配置,支持更多文件类型
|
||||
const attachmentStorage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
const uploadDir = path.join(config.UPLOAD_PROJECT_DIR, "temp");
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e6);
|
||||
cb(
|
||||
null,
|
||||
file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 允许的附件文件扩展名(常见文档、图片、PDF等)
|
||||
const ATTACHMENT_ALLOWED_EXTENSIONS = [
|
||||
".pdf",
|
||||
".doc",
|
||||
".docx",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".txt",
|
||||
".md",
|
||||
".csv",
|
||||
".json",
|
||||
".xml",
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".bmp",
|
||||
".svg",
|
||||
".ico",
|
||||
".webp",
|
||||
".avif",
|
||||
".zip",
|
||||
".rar",
|
||||
".7z",
|
||||
".tar",
|
||||
".gz",
|
||||
".mp4",
|
||||
".avi",
|
||||
".mov",
|
||||
".wmv",
|
||||
".flv",
|
||||
".mp3",
|
||||
".wav",
|
||||
".ogg",
|
||||
".m4a",
|
||||
];
|
||||
|
||||
const uploadAttachment = multer({
|
||||
storage: attachmentStorage,
|
||||
fileFilter: function (req, file, cb) {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
if (ATTACHMENT_ALLOWED_EXTENSIONS.includes(ext)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
// 将文件类型错误转换为 ValidationError
|
||||
cb(
|
||||
new ValidationError("附件文件类型不被允许", {
|
||||
fileExtension: ext,
|
||||
allowedExtensions: ATTACHMENT_ALLOWED_EXTENSIONS,
|
||||
}),
|
||||
false
|
||||
);
|
||||
}
|
||||
},
|
||||
limits: {
|
||||
fileSize: config.UPLOAD_MAX_FILE_SIZE_BYTES,
|
||||
},
|
||||
});
|
||||
|
||||
// 文件名解码中间件,用于正确处理中文文件名
|
||||
function decodeFileNameMiddleware(req, res, next) {
|
||||
if (req.file && req.file.originalname) {
|
||||
try {
|
||||
// 保存原始文件名用于日志
|
||||
const beforeDecode = req.file.originalname;
|
||||
// 尝试将文件名从 latin1 解码为 UTF-8
|
||||
// 这是一个常见的编码问题,当文件名包含中文字符时
|
||||
const decodedName = iconv.decode(
|
||||
Buffer.from(req.file.originalname, "latin1"),
|
||||
"utf8"
|
||||
);
|
||||
req.file.originalname = decodedName;
|
||||
log("system", "INFO", "File name decoded successfully", {
|
||||
before: beforeDecode,
|
||||
after: decodedName,
|
||||
});
|
||||
} catch (err) {
|
||||
// 如果解码失败,记录日志但继续处理
|
||||
log("system", "WARN", "File name decoded failed", {
|
||||
originalName: req.file.originalname,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// 路由配置
|
||||
const routes = [
|
||||
{
|
||||
path: "/push-skills-to-workspace",
|
||||
method: "post",
|
||||
handler: upload.single("file"),
|
||||
customHandler: asyncHandler(async (req, res) => {
|
||||
const { projectId, skillUrls } = req.body || {};
|
||||
const file = req.file || null;
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
const result = await projectService.pushSkillsToWorkspace(
|
||||
String(projectId),
|
||||
file,
|
||||
skillUrls,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json({ success: true, ...result });
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/create-project",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, templateType } = req.body;
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
const result = await projectService.createProject(
|
||||
String(projectId),
|
||||
templateType,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/upload-project",
|
||||
method: "post",
|
||||
handler: upload.single("file"),
|
||||
customHandler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion, pid, basePath } = req.body;
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (!codeVersion) {
|
||||
throw new ValidationError("Code version cannot be empty", { field: "codeVersion" });
|
||||
}
|
||||
if (!req.file) {
|
||||
throw new ValidationError("Please upload a zip file", { field: "zipFile" });
|
||||
}
|
||||
|
||||
// 处理文件上传(移动到项目目录)
|
||||
const uploadResult = await projectService.handleFileUpload(
|
||||
String(projectId),
|
||||
codeVersion,
|
||||
req.file
|
||||
);
|
||||
|
||||
// 更新req.file.path为新的路径
|
||||
req.file.path = uploadResult.filePath;
|
||||
|
||||
try {
|
||||
const result = await projectService.uploadProject(
|
||||
String(projectId),
|
||||
req.file.path,
|
||||
req,
|
||||
codeVersion,
|
||||
pid,
|
||||
basePath,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
// 失败时清理项目目录(uploadProject内部已经处理,这里作为额外保障)
|
||||
try {
|
||||
await projectService.cleanupProjectDirectory(
|
||||
String(projectId),
|
||||
isolationContext
|
||||
);
|
||||
} catch (cleanupErr) {
|
||||
log(projectId, "ERROR", "Route layer cleanup project directory failed", {
|
||||
projectId,
|
||||
error: cleanupErr.message,
|
||||
});
|
||||
}
|
||||
|
||||
// 清理上传的文件
|
||||
if (req.file && fs.existsSync(req.file.path)) {
|
||||
try {
|
||||
fs.unlinkSync(req.file.path);
|
||||
} catch (cleanupErr) {
|
||||
log(projectId, "ERROR", "Clean upload file failed", {
|
||||
projectId,
|
||||
error: cleanupErr.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
throw err; // 重新抛出错误,让全局错误处理器处理
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/get-project-content",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, command, proxyPath } = req.query;
|
||||
const isolationContext = extractIsolationContext(req.query || {});
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
// 构建项目路径
|
||||
const projectPath = resolveProjectPath(projectId, isolationContext);
|
||||
|
||||
// 检查项目目录是否存在
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
throw new ValidationError("Project does not exist", { field: "projectId" });
|
||||
}
|
||||
|
||||
// 获取项目内容
|
||||
try {
|
||||
const result = await getProjectContent(
|
||||
projectPath,
|
||||
command,
|
||||
proxyPath
|
||||
);
|
||||
res.status(200).json({ success: true, ...result });
|
||||
} catch (err) {
|
||||
const message = err?.message || "Query failed";
|
||||
res.status(500).json({ success: false, message });
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/get-project-content-by-version",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion, command, proxyPath } = req.query;
|
||||
const isolationContext = extractIsolationContext(req.query || {});
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (!codeVersion) {
|
||||
throw new ValidationError("Code version cannot be empty", { field: "codeVersion" });
|
||||
}
|
||||
try {
|
||||
const result = await getProjectContentByVersion(
|
||||
String(projectId),
|
||||
codeVersion,
|
||||
command,
|
||||
proxyPath,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json({ success: true, ...result });
|
||||
} catch (err) {
|
||||
const message = err?.message || "Query failed";
|
||||
res.status(500).json({ success: false, message });
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/backup-current-version",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion } = req.body;
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
|
||||
const result = await projectService.backupCurrentVersion(
|
||||
String(projectId),
|
||||
codeVersion,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json({ success: true, ...result });
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/export-project",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, codeVersion, exportType, config } = req.body;
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
|
||||
const result = await projectService.exportProject(
|
||||
String(projectId),
|
||||
codeVersion,
|
||||
exportType,
|
||||
config,
|
||||
isolationContext
|
||||
);
|
||||
|
||||
// 检查zip文件是否存在
|
||||
if (!fs.existsSync(result.zipPath)) {
|
||||
throw new SystemError("Exported zip file does not exist", {
|
||||
zipPath: result.zipPath,
|
||||
});
|
||||
}
|
||||
|
||||
// 设置响应头,指定文件下载
|
||||
const fileName = path.basename(result.zipPath);
|
||||
res.setHeader("Content-Type", "application/zip");
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${fileName}"`
|
||||
);
|
||||
|
||||
res.sendFile(result.zipPath, (err) => {
|
||||
if (err) {
|
||||
log(projectId, "ERROR", "Send zip file failed", {
|
||||
projectId,
|
||||
error: err.message,
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, message: "File send failed" });
|
||||
}
|
||||
} else {
|
||||
log(projectId, "INFO", "Zip file send successfully", {
|
||||
projectId,
|
||||
zipPath: result.zipPath,
|
||||
});
|
||||
}
|
||||
});
|
||||
}),
|
||||
},
|
||||
|
||||
{
|
||||
path: "/delete-project",
|
||||
method: "get",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { projectId, pid } = req.query;
|
||||
const isolationContext = extractIsolationContext(req.query || {});
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
|
||||
const result = await projectService.deleteProject(
|
||||
String(projectId),
|
||||
pid,
|
||||
req,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/upload-attachment-file",
|
||||
method: "post",
|
||||
handler: uploadAttachment.single("file"),
|
||||
decodeMiddleware: decodeFileNameMiddleware,
|
||||
customHandler: asyncHandler(async (req, res) => {
|
||||
const { projectId, fileName } = req.body;
|
||||
const isolationContext = extractIsolationContext(req.body || {});
|
||||
|
||||
if (!projectId) {
|
||||
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
|
||||
}
|
||||
if (!req.file) {
|
||||
throw new ValidationError("Please upload a file", { field: "file" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadAttachmentFile(
|
||||
String(projectId),
|
||||
req.file,
|
||||
fileName,
|
||||
isolationContext
|
||||
);
|
||||
res.status(200).json({ success: true, ...result });
|
||||
} catch (err) {
|
||||
// 失败时清理上传的文件
|
||||
if (req.file && fs.existsSync(req.file.path)) {
|
||||
try {
|
||||
fs.unlinkSync(req.file.path);
|
||||
} catch (cleanupErr) {
|
||||
log(projectId, "ERROR", "Clean upload file failed", {
|
||||
projectId,
|
||||
error: cleanupErr.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
throw err; // 重新抛出错误,让全局错误处理器处理
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "/copy-project",
|
||||
method: "post",
|
||||
handler: asyncHandler(async (req, res) => {
|
||||
const { sourceProjectId, targetProjectId } = req.body;
|
||||
const sourceIsolationContext = extractIsolationContext({
|
||||
tenantId: req.body?.sourceTenantId || req.body?.tenantId,
|
||||
spaceId: req.body?.sourceSpaceId || req.body?.spaceId,
|
||||
isolationType: req.body?.sourceIsolationType || req.body?.isolationType,
|
||||
});
|
||||
const targetIsolationContext = extractIsolationContext({
|
||||
tenantId: req.body?.targetTenantId || req.body?.tenantId,
|
||||
spaceId: req.body?.targetSpaceId || req.body?.spaceId,
|
||||
isolationType: req.body?.targetIsolationType || req.body?.isolationType,
|
||||
});
|
||||
|
||||
if (!sourceProjectId) {
|
||||
throw new ValidationError("Source project ID cannot be empty", {
|
||||
field: "sourceProjectId",
|
||||
});
|
||||
}
|
||||
if (!targetProjectId) {
|
||||
throw new ValidationError("Target project ID cannot be empty", {
|
||||
field: "targetProjectId",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await copyProject(
|
||||
String(sourceProjectId),
|
||||
String(targetProjectId),
|
||||
{
|
||||
sourceIsolationContext,
|
||||
targetIsolationContext,
|
||||
}
|
||||
);
|
||||
res.status(200).json(result);
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
// Multer错误处理中间件
|
||||
function multerErrorHandler(err, req, res, next) {
|
||||
if (err instanceof multer.MulterError) {
|
||||
// Multer错误会被全局错误处理器捕获并处理
|
||||
return next(err);
|
||||
}
|
||||
// 如果是 ValidationError,直接传递给错误处理器
|
||||
if (err.name === "ValidationError" || err instanceof ValidationError) {
|
||||
return next(err);
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
|
||||
// 注册路由
|
||||
routes.forEach((route) => {
|
||||
if (route.customHandler) {
|
||||
// 对于有自定义处理器的路由
|
||||
const middlewares = [];
|
||||
|
||||
// 先添加multer中间件
|
||||
if (route.handler) {
|
||||
middlewares.push(route.handler);
|
||||
}
|
||||
|
||||
// 再添加文件名解码中间件(如果有)
|
||||
if (route.decodeMiddleware) {
|
||||
middlewares.push(route.decodeMiddleware);
|
||||
}
|
||||
|
||||
// 添加错误处理中间件
|
||||
middlewares.push(multerErrorHandler);
|
||||
|
||||
// 最后添加自定义处理器
|
||||
middlewares.push(route.customHandler);
|
||||
|
||||
// 注册所有中间件
|
||||
projectRouter[route.method](route.path, ...middlewares);
|
||||
} else {
|
||||
// 普通路由直接注册处理器
|
||||
projectRouter[route.method](route.path, route.handler);
|
||||
}
|
||||
});
|
||||
|
||||
export default projectRouter;
|
||||
56
qiming-file-server/src/routes/router.js
Normal file
56
qiming-file-server/src/routes/router.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import express from "express";
|
||||
import buildRouter from "./buildRoutes.js";
|
||||
import projectRouter from "./projectRoutes.js";
|
||||
import codeRouter from "./codeRoutes.js";
|
||||
import computerRouter from "./computerRoutes.js";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// 读取 package.json 获取版本号(使用绝对路径确保跨场景兼容)
|
||||
let version = "1.0.0";
|
||||
try {
|
||||
const packageJsonPath = path.resolve(__dirname, "..", "..", "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
version = packageJson.version || "1.0.0";
|
||||
} catch (err) {
|
||||
// 使用默认值
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
res.send("Hello");
|
||||
});
|
||||
|
||||
router.get("/health", (req, res) => {
|
||||
const uptimeSeconds = Math.floor(process.uptime());
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const memory = {
|
||||
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024 * 100) / 100,
|
||||
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024 * 100) / 100,
|
||||
rss: Math.round(memoryUsage.rss / 1024 / 1024 * 100) / 100,
|
||||
external: Math.round(memoryUsage.external / 1024 / 1024 * 100) / 100,
|
||||
};
|
||||
const healthData = {
|
||||
status: "ok",
|
||||
timestamp: Date.now(),
|
||||
uptime: uptimeSeconds,
|
||||
version: version,
|
||||
platform: process.platform,
|
||||
nodeVersion: process.version,
|
||||
pid: process.pid,
|
||||
memory: memory,
|
||||
env: process.env.NODE_ENV || "unknown",
|
||||
};
|
||||
res.json(healthData);
|
||||
});
|
||||
|
||||
router.use("/api/build", buildRouter);
|
||||
router.use("/api/project", projectRouter);
|
||||
router.use("/api/project", codeRouter);
|
||||
router.use("/api/computer", computerRouter);
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user