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

856 lines
28 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 path from "path";
import fs from "fs";
import config from "../appConfig/index.js";
import { restartDevServer } from "../utils/build/restartDevUtils.js";
import { log } from "../utils/log/logUtils.js";
import {
ValidationError,
SystemError,
ResourceError,
FileError,
} from "../utils/error/errorHandler.js";
import { sanitizeSensitivePaths } from "../utils/common/sensitiveUtils.js";
import {
backupProjectToZip,
restoreProjectFromZip,
pruneMissingFiles,
removeEmptyDirectories,
} from "../utils/project/backupUtils.js";
import { extractSingleFileFromZip } from "../utils/common/zipUtils.js";
import {
shouldRestartForSingleFile,
shouldRestartDevServer,
} from "../utils/buildJudge/restartJudgeUtils.js";
import { resolveProjectPath } from "../utils/common/projectPathUtils.js";
/**
* 按行对比旧内容与新内容,返回合并后的内容和变更行数
* - 行内容不同视为修改
* - 旧文件多出的行视为删除
* - 新文件多出的行视为新增
* - 保持原文件换行符风格(\n 或 \r\n
*/
function diffContentByLines(existingContent, newContentStr) {
const oldLines = existingContent.split(/\r?\n/);
const newLines = newContentStr.split(/\r?\n/);
const oldLen = oldLines.length;
const newLen = newLines.length;
const minLen = Math.min(oldLen, newLen);
let changesCount = 0;
// 1) 对齐范围内的行:不同则视为修改
for (let idx = 0; idx < minLen; idx++) {
if (oldLines[idx] !== newLines[idx]) {
oldLines[idx] = newLines[idx];
changesCount++;
}
}
// 2) old 比 new 长的部分:多出来的行视为删除,从后往前删
if (oldLen > newLen) {
for (let idx = oldLen - 1; idx >= newLen; idx--) {
oldLines.splice(idx, 1);
changesCount++;
}
}
// 3) new 比 old 长的部分:多出来的行视为新增,按顺序追加
if (newLen > oldLen) {
for (let idx = oldLen; idx < newLen; idx++) {
oldLines.push(newLines[idx]);
changesCount++;
}
}
const newline = existingContent.includes("\r\n") ? "\r\n" : "\n";
const finalContent = oldLines.join(newline);
return { finalContent, changesCount };
}
/**
* 直接使用新内容替换旧内容
* - 若内容完全一致,则 changesCount 为 0
* - 预留备用:当前未在业务中使用
*/
function replaceContentDirectly(existingContent, newContentStr) {
if (existingContent === newContentStr) {
return { finalContent: existingContent, changesCount: 0 };
}
return { finalContent: newContentStr, changesCount: -1 };
}
/**
* 部分文件更新:支持新增、删除、重命名、修改操作
* @param {string} projectId 项目ID
* @param {string} codeVersion 代码版本号
* @param {Array} files 文件操作列表
* @param {Object} req 请求对象
* @returns {Object} 更新结果
*/
async function specifiedFilesUpdate(
projectId,
codeVersion,
files,
req,
isolationContext = {}
) {
const startTime = Date.now();
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",
});
}
const versionNum = Number(codeVersion);
if (!Number.isFinite(versionNum)) {
throw new ValidationError("codeVersion must be a number", {
field: "codeVersion",
});
}
if (!Array.isArray(files)) {
throw new ValidationError("files must be an array", { field: "files" });
}
// 验证文件操作结构
for (let i = 0; i < files.length; i++) {
const fileOp = files[i];
if (!fileOp || typeof fileOp.operation !== "string") {
throw new ValidationError(`files[${i}].operation cannot be empty`, {
field: `files[${i}].operation`,
});
}
// 使用 name 作为文件路径字段
if (!fileOp.name || typeof fileOp.name !== "string") {
throw new ValidationError(`files[${i}].name cannot be empty`, {
field: `files[${i}].name`,
});
}
const operation = fileOp.operation.toLowerCase();
if (!["create", "delete", "rename", "modify"].includes(operation)) {
throw new ValidationError(
`files[${i}].operation must be one of create, delete, rename or modify`,
{ field: `files[${i}].operation` }
);
}
// 验证特定操作所需的字段
if (operation === "rename" && !fileOp.renameFrom) {
throw new ValidationError(
`files[${i}].renameFrom cannot be empty (rename operation requires)`,
{ field: `files[${i}].renameFrom` }
);
}
if (operation === "modify") {
if (typeof fileOp.contents !== "string") {
throw new ValidationError(
`files[${i}].contents must be a string (modify operation requires)`,
{ field: `files[${i}].contents` }
);
}
}
}
const projectPath = resolveProjectPath(projectId, isolationContext);
if (!fs.existsSync(projectPath)) {
log(projectId, "ERROR", "Project does not exist", { projectId, projectPath });
throw new ResourceError("Project does not exist", { projectId });
}
let backupZipPath = "";
try {
// 1) 备份
const backupDir = path.join(config.UPLOAD_PROJECT_DIR, projectId);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const zipName = `${projectId}-v${versionNum}.zip`;
backupZipPath = path.join(backupDir, zipName);
log(projectId, "DEBUG", "Start backing up project", { projectId, backupZipPath });
await backupProjectToZip(projectId, projectPath, backupZipPath);
log(projectId, "INFO", "Project backed up successfully", {
projectId,
zipPath: backupZipPath,
});
// 2) 处理文件操作
try {
log(projectId, "DEBUG", "Start processing file operations", { projectId, filesCount: files.length });
for (const fileOp of files) {
const operation = fileOp.operation.toLowerCase();
// 使用 name 作为文件路径
const fileName = fileOp.name;
const normalizedPath = path.normalize(fileName).replace(/^[\/\\]+/, "");
const targetPath = path.join(projectPath, normalizedPath);
// 安全检查:确保目标路径在项目目录内
const resolvedTargetPath = path.resolve(targetPath);
const resolvedProjectPath = path.resolve(projectPath);
if (!resolvedTargetPath.startsWith(resolvedProjectPath + path.sep) &&
resolvedTargetPath !== resolvedProjectPath) {
log(projectId, "WARN", "Unsafe file path, skipping", {
filePath: normalizedPath,
resolvedPath: resolvedTargetPath,
});
continue;
}
switch (operation) {
case "create": {
// 创建新文件
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
const contents = fileOp.contents || "";
await fs.promises.writeFile(targetPath, contents, "utf8");
log(projectId, "INFO", "File created successfully", {
filePath: normalizedPath,
});
break;
}
case "delete": {
// 删除文件
if (fs.existsSync(targetPath)) {
await fs.promises.unlink(targetPath);
log(projectId, "INFO", "File deleted successfully", {
filePath: normalizedPath,
});
} else {
log(projectId, "WARN", "File to delete does not exist", {
filePath: normalizedPath,
});
}
break;
}
case "rename": {
// 重命名文件
const renameFrom = fileOp.renameFrom;
if (!renameFrom || typeof renameFrom !== "string") {
log(projectId, "WARN", "Rename operation missing renameFrom", {
filePath: normalizedPath,
});
break;
}
const normalizedFrom = path.normalize(renameFrom).replace(/^[\/\\]+/, "");
const oldPath = path.join(projectPath, normalizedFrom);
const resolvedOldPath = path.resolve(oldPath);
// 安全检查
if (!resolvedOldPath.startsWith(resolvedProjectPath + path.sep) &&
resolvedOldPath !== resolvedProjectPath) {
log(projectId, "WARN", "Unsafe rename source path, skipping", {
renameFrom: normalizedFrom,
resolvedPath: resolvedOldPath,
});
break;
}
if (fs.existsSync(oldPath)) {
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
await fs.promises.rename(oldPath, targetPath);
log(projectId, "INFO", "File renamed successfully", {
oldPath: normalizedFrom,
newPath: normalizedPath,
});
} else {
log(projectId, "WARN", "File to rename does not exist", {
renameFrom: normalizedFrom,
});
}
break;
}
case "modify": {
// 修改文件:前端传入完整 contents这里根据新旧内容按行生成增删改
if (!fs.existsSync(targetPath)) {
log(projectId, "WARN", "File to modify does not exist", {
filePath: normalizedPath,
});
break;
}
// 读取现有文件内容
const existingContent = await fs.promises.readFile(
targetPath,
"utf8"
);
const newContentStr =
typeof fileOp.contents === "string" ? fileOp.contents : "";
// 使用按行 diff 的方式生成最终内容
const { finalContent, changesCount } = diffContentByLines(
existingContent,
newContentStr
);
// 1) 若内容完全一致,不覆写文件,避免触发 HMR
if (changesCount === 0) {
log(projectId, "INFO", "File content unchanged, skipping write", {
filePath: normalizedPath,
});
break;
}
// 2) 写入修改后的内容
await fs.promises.writeFile(targetPath, finalContent, "utf8");
log(projectId, "INFO", "File modified successfully", {
filePath: normalizedPath,
changesCount,
});
break;
}
default: {
log(projectId, "WARN", "Unsupported operation type", {
operation,
filePath: normalizedPath,
});
break;
}
}
}
// 3) 清理空目录
log(projectId, "DEBUG", "Start cleaning empty directories", { projectId });
try {
await removeEmptyDirectories(
projectPath,
config.TRAVERSE_EXCLUDE_DIRS || []
);
} catch (e) {
log(projectId, "WARN", "Failed to clean empty directories", {
projectId,
error: e && e.message,
});
}
log(projectId, "INFO", "Specified files updated successfully", {
projectId,
filesCount: files.length,
elapsedMs: Date.now() - startTime,
});
return {
success: true,
message: "Specified files updated successfully",
projectId,
filesCount: files.length,
};
} catch (e) {
log(projectId, "ERROR", "Failed to process file operations", {
projectId,
error: e && e.message,
elapsedMs: Date.now() - startTime,
});
throw e;
}
} catch (backupErr) {
// 备份阶段失败:不执行回滚(无备份可回滚),直接抛出
if (!backupErr.isOperational) {
throw new SystemError("Failed to backup project", {
projectId,
originalError: backupErr && backupErr.message,
});
}
throw backupErr;
}
}
async function allFilesUpdate(
projectId,
codeVersion,
files,
req,
isolationContext = {}
) {
const startTime = Date.now();
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
const versionNum = Number(codeVersion);
if (!Number.isFinite(versionNum)) {
throw new ValidationError("codeVersion must be a number", {
field: "codeVersion",
});
}
if (!Array.isArray(files)) {
throw new ValidationError("files must be an array", { field: "files" });
}
const projectPath = resolveProjectPath(projectId, isolationContext);
if (!fs.existsSync(projectPath)) {
log(projectId, "ERROR", "Project does not exist", { projectId, projectPath });
throw new ResourceError("Project does not exist", { projectId });
}
let backupZipPath = "";
try {
// 1) 备份
const backupDir = path.join(config.UPLOAD_PROJECT_DIR, projectId);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const zipName = `${projectId}-v${versionNum}.zip`;
backupZipPath = path.join(backupDir, zipName);
log(projectId, "DEBUG", "Start backing up project", { projectId, backupZipPath });
await backupProjectToZip(projectId, projectPath, backupZipPath);
// 2) 写入文件
try {
log(projectId, "DEBUG", "Start writing files", { projectId, filesCount: files.length });
for (const file of files) {
if (!file || typeof file.name !== "string") continue;
const targetPath = path.join(projectPath, file.name);
// 处理文件重命名:如果存在 renameFrom需要先重命名原文件
if (file.renameFrom && typeof file.renameFrom === "string") {
const oldPath = path.join(projectPath, file.renameFrom);
if (fs.existsSync(oldPath)) {
// 确保目标目录存在(跨目录重命名时需要)
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
await fs.promises.rename(oldPath, targetPath);
log(projectId, "INFO", "File renamed successfully", {
projectId,
oldPath: file.renameFrom,
newPath: file.name,
});
continue; // 重命名后跳过后续的写入操作
}
}
const isBinary = file.binary === true;
const isText = file.binary === false;
const sizeExceeded = !!file.sizeExceeded;
const hasContents =
typeof file.contents === "string" && file.contents.length > 0;
// 处理二进制文件如果已经存在就不写入不存在才需要根据hasContents写入
if (isBinary) {
// 如果文件已存在,跳过写入
if (fs.existsSync(targetPath)) {
log(projectId, "INFO", "Binary file already exists, skipping write", {
filePath: file.name,
});
continue;
}
// 文件不存在,且 hasContents 为 true才写入
if (hasContents) {
// 二进制文件有 base64 编码的内容,需要解码为 Buffer 并写入
try {
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
const buffer = Buffer.from(file.contents, "base64");
await fs.promises.writeFile(targetPath, buffer);
log(projectId, "INFO", "Binary file written successfully", {
filePath: file.name,
});
} catch (e) {
log(projectId, "ERROR", "Failed to write binary file", {
filePath: file.name,
error: e && e.message,
});
}
} else {
// 二进制文件没有内容(可能是大文件),且文件不存在
log(projectId, "WARN", "Binary file does not exist and has no content, skipping", {
filePath: file.name,
});
}
continue;
}
// 处理文本文件
const shouldReplace =
isText && (!sizeExceeded || (sizeExceeded && hasContents));
if (!shouldReplace) {
continue;
}
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
await fs.promises.writeFile(targetPath, file.contents || "", "utf8");
}
} catch (e) {
log(projectId, "ERROR", "Failed to write files", {
projectId,
error: e && e.message,
elapsedMs: Date.now() - startTime,
});
throw e;
}
// 3) 清理缺失文件与空目录
try {
log(projectId, "DEBUG", "Start cleaning missing files and empty directories", { projectId });
const keepSet = new Set(
files
.filter((f) => f && typeof f.name === "string")
.map((f) => path.normalize(f.name))
);
await pruneMissingFiles(
projectPath,
keepSet,
config.TRAVERSE_EXCLUDE_DIRS || []
);
await removeEmptyDirectories(
projectPath,
config.TRAVERSE_EXCLUDE_DIRS || []
);
} catch (e) {
log(projectId, "ERROR", "Failed to clean missing files, starting rollback", {
projectId,
error: e && e.message,
elapsedMs: Date.now() - startTime,
});
throw e;
}
log(projectId, "INFO", "Files submitted successfully", {
projectId,
filesCount: files.length,
elapsedMs: Date.now() - startTime,
});
return {
success: true,
message: "Files submitted successfully",
projectId,
restarted: false,
};
} catch (backupErr) {
// 备份阶段失败:不执行回滚(无备份可回滚),直接抛出
if (!backupErr.isOperational) {
throw new SystemError("Failed to backup old version", {
projectId,
originalError: backupErr && backupErr.message,
});
}
throw backupErr;
}
}
/**
* 上传单个文件到指定路径
* @param {string} projectId 项目ID
* @param {string} codeVersion 代码版本号
* @param {Object} file 文件对象 (包含文件内容和元数据)
* @param {string} filePath 文件在项目中的相对路径
* @param {Object} req 请求对象
* @returns {Object} 上传结果
*/
async function uploadSingleFile(
projectId,
codeVersion,
file,
filePath,
req,
isolationContext = {}
) {
const startTime = Date.now();
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
const versionNum = Number(codeVersion);
if (!Number.isFinite(versionNum)) {
throw new ValidationError("codeVersion must be a number", {
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" });
}
const projectPath = resolveProjectPath(projectId, isolationContext);
if (!fs.existsSync(projectPath)) {
log(projectId, "ERROR", "Project does not exist", { projectId, projectPath });
throw new ResourceError("Project does not exist", { projectId });
}
// 规范化文件路径,确保是相对路径
const normalizedPath = path.normalize(filePath).replace(/^[\/\\]+/, "");
const targetPath = path.join(projectPath, normalizedPath);
// 安全检查:确保目标路径在项目目录内
const resolvedTargetPath = path.resolve(targetPath);
const resolvedProjectPath = path.resolve(projectPath);
if (!resolvedTargetPath.startsWith(resolvedProjectPath)) {
throw new ValidationError("File path is not safe, cannot exceed project directory", {
field: "filePath",
providedPath: filePath,
resolvedPath: resolvedTargetPath,
});
}
let backupZipPath = "";
try {
// 1) 备份
const backupDir = path.join(config.UPLOAD_PROJECT_DIR, projectId);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const zipName = `${projectId}-v${versionNum}.zip`;
backupZipPath = path.join(backupDir, zipName);
log(projectId, "DEBUG", "Start backing up project", { projectId, backupZipPath });
await backupProjectToZip(projectId, projectPath, backupZipPath);
log(projectId, "INFO", `Project backed up: ${backupZipPath}`, {
projectId,
zipPath: backupZipPath,
});
// 2) 写入文件
try {
// 确保目标目录存在
log(projectId, "DEBUG", "Start writing uploaded file", { projectId, filePath: normalizedPath });
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
// 写入文件内容统一使用buffermulter memoryStorage对所有文件类型都提供buffer
if (!file.buffer) {
throw new ValidationError("File content format is incorrect, missing buffer", {
field: "file",
});
}
// 记录写入前的文件信息,用于调试
log(projectId, "INFO", "Prepare to write file", {
targetPath,
bufferLength: file.buffer.length,
expectedSize: file.size,
bufferIsBuffer: Buffer.isBuffer(file.buffer),
sizeMatch: file.buffer.length === file.size,
});
// 直接写入BufferNode.js会自动以二进制模式写入
await fs.promises.writeFile(targetPath, file.buffer);
log(projectId, "INFO", "File uploaded successfully", {
projectId,
filePath: normalizedPath,
targetPath: resolvedTargetPath,
fileSize: file.buffer ? file.buffer.length : 0,
elapsedMs: Date.now() - startTime,
});
// 判断是否需要重启开发服务器
// const needRestart = shouldRestartForSingleFile(path.basename(normalizedPath));
const needRestart = false;
if (needRestart) {
try {
const restartResult = await restartDevServer(req, projectId);
log(projectId, "INFO", "Restart development server successfully", {
projectId,
pid: restartResult.pid,
port: restartResult.port,
});
return {
success: true,
message: "File uploaded and restarted development server successfully",
projectId,
filePath: normalizedPath,
targetPath: resolvedTargetPath,
fileSize: file.buffer ? file.buffer.length : 0,
pid: restartResult.pid,
port: restartResult.port,
restarted: true,
};
} catch (e) {
log(projectId, "ERROR", "Failed to restart development server", {
projectId,
filePath: normalizedPath,
error: e && e.message,
});
}
} else {
log(projectId, "INFO", "File modification does not require restarting development server", {
projectId,
filePath: normalizedPath,
});
return {
success: true,
message: "File uploaded successfully, no need to restart development server",
projectId,
restarted: false,
};
}
} catch (e) {
log(projectId, "ERROR", "Failed to write file", {
projectId,
filePath: normalizedPath,
error: e && e.message,
elapsedMs: Date.now() - startTime,
});
throw e;
}
} catch (backupErr) {
// 备份阶段失败:不执行回滚(无备份可回滚),直接抛出
if (!backupErr.isOperational) {
throw new SystemError("Failed to backup project", {
projectId,
filePath: normalizedPath,
originalError: backupErr && backupErr.message,
});
}
throw backupErr;
}
}
/**
* 回滚项目到指定版本
* @param {string} projectId 项目ID
* @param {string} codeVersion 当前代码版本号
* @param {string} rollbackTo 要回滚到的版本号
* @param {Object} req 请求对象
* @returns {Object} 回滚结果
*/
async function rollbackVersion(
projectId,
codeVersion,
rollbackTo,
req,
isolationContext = {}
) {
const startTime = Date.now();
if (!projectId) {
throw new ValidationError("Project ID cannot be empty", { field: "projectId" });
}
const versionNum = Number(codeVersion);
if (!Number.isFinite(versionNum)) {
throw new ValidationError("codeVersion must be a number", {
field: "codeVersion",
});
}
const rollbackToNum = Number(rollbackTo);
if (!Number.isFinite(rollbackToNum)) {
throw new ValidationError("rollbackTo must be a number", {
field: "rollbackTo",
});
}
if (rollbackToNum < 0) {
throw new ValidationError("rollbackTo cannot be less than 0", {
field: "rollbackTo",
});
}
if (rollbackToNum >= versionNum) {
throw new ValidationError("rollbackTo must be less than current codeVersion", {
field: "rollbackTo",
});
}
const projectPath = resolveProjectPath(projectId, isolationContext);
if (!fs.existsSync(projectPath)) {
log(projectId, "ERROR", "Project does not exist", { projectId, projectPath });
throw new ResourceError("Project does not exist", { projectId });
}
// 检查要回滚到的版本的备份文件是否存在
const backupDir = path.join(config.UPLOAD_PROJECT_DIR, projectId);
const rollbackZipName = `${projectId}-v${rollbackToNum}.zip`;
const rollbackZipPath = path.join(backupDir, rollbackZipName);
if (!fs.existsSync(rollbackZipPath)) {
log(projectId, "ERROR", "Rollback version backup file does not exist", {
projectId,
rollbackTo: rollbackToNum,
zipPath: rollbackZipPath,
});
throw new ResourceError("Rollback version backup file does not exist", {
projectId,
rollbackTo: rollbackToNum,
});
}
let currentBackupZipPath = "";
try {
// 1) 备份当前版本
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
const currentZipName = `${projectId}-v${versionNum}.zip`;
currentBackupZipPath = path.join(backupDir, currentZipName);
// 如果当前版本的备份已存在,跳过备份(避免覆盖)
if (!fs.existsSync(currentBackupZipPath)) {
await backupProjectToZip(projectId, projectPath, currentBackupZipPath);
log(projectId, "INFO", "Current version backed up", {
projectId,
zipPath: currentBackupZipPath,
});
} else {
log(projectId, "INFO", "Current version backup already exists, skipping backup", {
projectId,
zipPath: currentBackupZipPath,
});
}
// 2) 从指定版本恢复项目
log(projectId, "DEBUG", "Start restoring project from backup", { projectId, rollbackToNum, rollbackZipPath });
await restoreProjectFromZip(projectId, projectPath, rollbackZipPath);
log(projectId, "INFO", "Project rolled back successfully", {
projectId,
newVersion: versionNum,
toVersion: rollbackToNum,
rollbackZipPath,
elapsedMs: Date.now() - startTime,
});
return {
success: true,
message: "Project rolled back successfully",
newVersion: versionNum,
rollbackTo: rollbackToNum,
};
} catch (restoreErr) {
log(projectId, "ERROR", "Failed to rollback project", {
projectId,
rollbackTo: rollbackToNum,
error: restoreErr && restoreErr.message,
elapsedMs: Date.now() - startTime,
});
// 如果恢复失败,尝试从当前版本备份恢复(如果存在)
if (currentBackupZipPath && fs.existsSync(currentBackupZipPath)) {
try {
log(projectId, "INFO", "Failed to rollback, trying to restore current version", {
projectId,
backupPath: currentBackupZipPath,
});
await restoreProjectFromZip(projectId, projectPath, currentBackupZipPath);
log(projectId, "INFO", "Current version restored", {
projectId,
});
} catch (recoveryErr) {
log(projectId, "ERROR", "Failed to restore current version", {
projectId,
error: recoveryErr && recoveryErr.message,
});
}
}
if (!restoreErr.isOperational) {
throw new SystemError("Failed to rollback project", {
projectId,
rollbackTo: rollbackToNum,
originalError: restoreErr && restoreErr.message,
});
}
throw restoreErr;
}
}
export { specifiedFilesUpdate, allFilesUpdate, uploadSingleFile, rollbackVersion };
export default {
specifiedFilesUpdate,
allFilesUpdate,
uploadSingleFile,
rollbackVersion,
};