chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,855 @@
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,
};