ql-publish
Version:
234 lines (202 loc) • 8.08 kB
JavaScript
const config = require('../config/config.js')[process.env.NODE_ENV];
const OSS = require('ali-oss');
const inquirer = require('inquirer');
const logger = require('../utils/logger');
const readline = require('readline');
const refreshCdn = require('../cdn/refresh')
// 初始化OSS客户端
const client = new OSS(config);
const reduceLog = () => {
return Math.random() * 2 > 1.98
}
/**
* 复制OSS中的文件夹到指定目录
* @param {string} sourcePrefix - 源文件夹前缀(如: 'source-folder/')
* @param {string} targetBucket - 目标Bucket名称(可以与源Bucket相同)
* @param {string} targetPrefix - 目标文件夹前缀(如: 'target-folder/')
* @param {string} [delimiter=''] - 分隔符,用于列出文件夹下的文件
*/
async function copyFolder(sourcePrefix, targetBucket, targetPrefix, delimiter = '') {
try {
let marker = '';
const maxKeys = 100; // 每次最多获取100个文件,可根据需要调整
do {
// 列出源文件夹下的文件
const result = await client.list({
prefix: sourcePrefix,
marker,
maxKeys,
delimiter
});
// 复制文件
if (result.objects && result.objects.length > 0) {
for (let file of result.objects) {
// 构建目标文件路径
const targetPath = targetPrefix + file.name.replace(sourcePrefix, '');
try {
// 复制文件
await client.copy(targetPath, file.name);
reduceLog() && logger.log(`✅ 已复制: ${file.name} -> ${targetPath}`);
} catch (err) {
logger.error(`❌ 复制失败 ${file.name}:`, err.message);
throw new Error(err.message)
}
}
}
// 处理子目录(如果有)
if (result.prefixes && result.prefixes.length > 0) {
for (const prefix of result.prefixes) {
// 递归复制子目录
await copyFolder(prefix, targetBucket, targetPrefix, delimiter);
}
}
marker = result.nextMarker;
} while (marker);
logger.warn('🎉 文件夹复制完成');
return true;
} catch (err) {
logger.error('❌ 复制文件夹时发生错误:', err);
return false;
}
}
/**
* 删除OSS中的文件夹及其所有内容
* @param {string} folderPath - 要删除的文件夹路径,例如: 'test-folder/'
*/
async function deleteFolder(folderPath) {
try {
let nextMarker = null;
let deletedCount = 0;
do {
// 列出文件夹下的所有文件
const result = await client.list({
prefix: folderPath, // 只列出指定前缀的文件
marker: nextMarker, // 分页标记
maxKeys: 100 // 每次最多列出100个文件
});
// 添加检查:确保 result.objects 存在且为数组
if (!result.objects || result.objects.length === 0) {
break;
}
// 准备要删除的文件列表
const objects = result.objects.filter(obj => obj && obj.name).map(obj => obj.name);
try {
// 批量删除文件
await client.deleteMulti(objects, { quiet: false });
} catch (e) {
throw new Error(e.message)
}
deletedCount += objects.length;
nextMarker = result.nextMarker;
} while (nextMarker);
logger.warn(`✅ 成功删除文件夹 "${folderPath}" 及其 ${deletedCount} 个文件`);
return true;
} catch (err) {
logger.error('❌ 删除文件夹失败:', err);
throw err;
}
}
const resetDir = async (resetName) => {
const sourceFolder = `${config.targetDir}_old/${resetName}/`;
// 目标Bucket(可以与源Bucket相同)
const targetBucket = config.bucket;
// 目标文件夹(注意末尾的斜杠)
const targetFolder = `${config.targetDir}/`;
logger.info(`开始回滚版本 ${sourceFolder} 到 ${targetFolder} 目录`);
return await copyFolder(sourceFolder, targetBucket, targetFolder);
}
const authProjectName = (name) => {
return new Promise((resolve) => {
// 创建readline接口
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 向用户提问
rl.question(`是否确认回滚 ${name || config.targetDir} 项目? (输入 y 继续) `, (answer) => {
// 检查用户输入是否为'y'(不区分大小写)
if (answer.trim().toLowerCase() === 'y') {
resolve(true);
} else {
logger.error('❌ 发布程序终止');
resolve(false);
}
// 关闭接口
rl.close();
});
})
}
const getDirList = async () => {
try {
const folderPath = `${config.targetDir}_old/`;
let nextMarker = null;
let folderNames = [];
logger.info(`查询可回滚版本中...`);
do {
// 列出文件夹下的所有文件
const result = await client.list({
prefix: folderPath, // 只列出指定前缀的文件
marker: nextMarker, // 分页标记
maxKeys: 1000 // 每次最多列出100个文件
});
// 添加检查:确保 result.objects 存在且为数组
if (!result.objects || result.objects.length === 0) {
break;
}
folderNames = [...folderNames, ...result.objects.filter(obj => obj && obj.name)
.map(obj => obj.name.split('/')[1])];
nextMarker = result.nextMarker;
} while (nextMarker);
logger.warn(`✅ 项目可回滚目录查询成功`);
return [...new Set(folderNames)];
} catch (err) {
logger.error('❌ 项目可回滚目录查询失败:', err);
throw err;
}
}
async function run() {
if (!await authProjectName()) {
process.exit(1);
}
const folderNames = await getDirList();
if (!folderNames || folderNames.length === 0) {
process.exit(1);
}
// 创建交互式问题
const questions = [
{
type: 'list',
name: 'selectedOption',
message: '请使用上下键选择一个版本进行回滚,按回车确认:',
choices: folderNames,
// 可选:设置默认选中的索引
default: 0
}
];
// 执行交互
inquirer.prompt(questions).then(async answers => {
logger.warn(`回滚版本号: ${answers.selectedOption}`);
if (!await authProjectName(answers.selectedOption)) {
process.exit(1);
}
// 删除目标文件夹代码
if (!await deleteFolder(`${config.targetDir}/`)) {
process.exit(1);
}
// 备份上一次发版的代码
if (!await resetDir(answers.selectedOption)) {
process.exit(1);
}
logger.success('🎉 生产环境:项目回滚完成!');
config.cdn && config.cdn.list && refreshCdn(config.cdn.list);
}).catch(error => {
if (error.isTtyError) {
logger.error('❌ 终端不支持交互模式');
} else {
logger.error('❌ 生产环境:回滚过程中发生错误:');
logger.error(error);
process.exit(1);
}
});
}
run();