UNPKG

ql-publish

Version:

234 lines (202 loc) 8.08 kB
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();