UNPKG

ql-publish

Version:

353 lines (318 loc) 11.8 kB
const config = require('../config/config.js')[process.env.NODE_ENV]; const inquirer = require('inquirer'); const logger = require('../utils/logger'); const readline = require('readline'); const refreshCdn = require('../cdn/refresh') const { Client } = require("ssh2"); const path = require("path"); const { PassThrough } = require("stream"); const dayjs = require("dayjs"); /** * 确保目录存在,如果不存在则创建 * @param {Object} sftp - SFTP客户端实例 * @param {string} dirPath - 目录路径 */ function ensureDirectoryExists(sftp, dirPath) { return new Promise((resolve, reject) => { sftp.stat(dirPath, (err) => { if (!err) { // 目录已存在 resolve(); return; } // 递归创建父目录 const parentDir = path.posix.dirname(dirPath); if (parentDir === dirPath) { // 已经是根目录 reject(new Error(`无法创建目录: ${dirPath}`)); return; } // 先确保父目录存在,再创建当前目录 ensureDirectoryExists(sftp, parentDir) .then(() => { sftp.mkdir(dirPath, (err) => { if (err) { reject(new Error(`创建目录失败 ${dirPath}: ${err.message}`)); } else { // console.log(`已创建目录: ${dirPath}`); resolve(); } }); }) .catch(reject); }); }); } const reduceLog = () => { return Math.random() * 2 > 1.98 } /** * 复制单个文件 * @param {Object} sftp - SFTP客户端实例 * @param {string} sourcePath - 源文件路径 * @param {string} targetPath - 目标文件路径 */ function copyFile(sftp, sourcePath, targetPath) { return new Promise((resolve) => { const readStream = sftp.createReadStream(sourcePath); const writeStream = sftp.createWriteStream(targetPath); const transferStream = new PassThrough(); // 错误处理 readStream.on('error', (err) => { console.error(`读取文件失败 ${sourcePath}: ${err.message}`); resolve(false); }); writeStream.on('error', (err) => { console.error(`写入文件失败 ${targetPath}: ${err.message}`); resolve(false); }); writeStream.on('close', () => { // console.log(`写入流已关闭: ${targetPath}`); reduceLog(1.9) && logger.log(`✅ 已复制到: ${targetPath}`); resolve(true); }); // 完成处理 writeStream.on('finish', () => { // console.log(`已复制: ${sourcePath}`); resolve(true); }); // 管道传输 readStream.pipe(transferStream).pipe(writeStream); }); } /** * 递归复制目录内容 * @param {Object} sftp - SFTP客户端实例 * @param {string} currentSourceDir - 当前源目录 * @param {string} currentTargetDir - 当前目标目录 */ async function recursiveCopy(sftp, currentSourceDir, currentTargetDir) { // 确保目标目录存在 await ensureDirectoryExists(sftp, currentTargetDir); // 获取源目录中的所有项目 const items = await new Promise((resolve, reject) => { sftp.readdir(currentSourceDir, (err, list) => { if (err) { reject(new Error(`读取目录失败 ${currentSourceDir}: ${err.message}`)); } else { resolve(list); } }); }); // 处理每个项目 for (const item of items) { const sourcePath = path.posix.join(currentSourceDir, item.filename); const targetPath = path.posix.join(currentTargetDir, item.filename); if (item.attrs.isDirectory()) { // 如果是目录,递归处理 await recursiveCopy(sftp, sourcePath, targetPath); } else { // 如果是文件,直接复制 await copyFile(sftp, sourcePath, targetPath); } } } const resetDir = async (sftp, resetName) => { logger.warn(`开始回滚代码`, `从${config.path}_old/${resetName}/,到${config.path}/`); const oldDir = `${config.path}_old/${resetName}/`; // 备份上一次的代码 await recursiveCopy(sftp, oldDir, `${config.path}/`); } const authProjectName = (name) => { return new Promise((resolve) => { // 创建readline接口 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // 向用户提问 rl.question(`${process.env.NODE_ENV}环境:是否确认回滚 ${name} 项目? (输入 y 继续) `, (answer) => { // 检查用户输入是否为'y'(不区分大小写) if (answer.trim().toLowerCase() === 'y') { resolve(true); } else { logger.error('❌ 发布程序终止'); resolve(false); } // 关闭接口 rl.close(); }); }) } /** * 获取文件列表 */ const getFileList = () => { return new Promise(async resolve => { const { sftp, close } = await getSftp(); const authDirectoryByName = (name) => { return new Promise(r => { // 检查目录是否存在 sftp.stat(name, (err, stats) => { if (!err && stats.isDirectory()) { r(true) return; } // 目录不存在,创建目录 sftp.mkdir(name, (err) => { if (err) { console.error(`❌ 创建目录 ${name} 失败:`, err); process.exit(1); r(false) } else { logger.log(`✅ 目录 ${name} 创建成功`); r(true) } close(); }); }); }) } if (!await authDirectoryByName(`${config.path}_old`)) { process.exit(1); return } // 读取目录内容 sftp.readdir(`${config.path}_old`, (err, list) => { if (err) { console.error('读取目录失败:', err); process.exit(1); return; } // 遍历并显示目录内容 resolve(list.map(item => { return item.filename }).sort((a, b) => { let _a = a.split('_'); _a = `${_a[0]}-${_a[1]}-${_a[2]} ${_a[3]}:${_a[4]}:${_a[5]}`; let _b = b.split('_'); _b = `${_b[0]}-${_b[1]}-${_b[2]} ${_b[3]}:${_b[4]}:${_b[5]}`; return dayjs(_b).diff(dayjs(_a)); })); }); }) } const sftpReaddir = async (sftp, pathStr) => { return new Promise((resolve) => { // 读取目录内容 sftp.readdir(pathStr, (err, entries) => { if (err) { logger.error(`❌ 读取目录失败 ${pathStr}: ${err.message}`); resolve([]); } else { resolve(entries); } }); }) } // 递归删除文件夹及其内容 async function deleteRemoteDirectory(sftp, pathStr) { try { // 获取目录下的所有条目 const entries = await sftpReaddir(sftp, pathStr); // 先删除目录中的所有内容 for (const entry of entries) { const fullPath = `${pathStr}/${entry.filename}`; // 判断是文件还是目录 if (entry.attrs.isDirectory()) { // 递归删除子目录 await deleteRemoteDirectory(sftp, fullPath); } else { // 删除文件 await sftp.unlink(fullPath); } } // 最后删除空目录 await sftp.rmdir(pathStr); logger.log(`✅ 已删除目录: ${pathStr}`); } catch (err) { logger.error(`❌ 删除过程中出错: ${err.message}`); process.exit(1); } } const getSftp = () => { return new Promise(async resolve => { let conn, sftp; try { logger.warn('连接到服务器...'); // 连接到服务器 conn = new Client(); await new Promise((r, reject) => { conn.on('ready', r); conn.on('error', (err) => reject(new Error(`❌ 服务器连接失败: ${err.message}`))); conn.connect(config); }); logger.info('✅ 已成功连接到服务器'); // 初始化SFTP sftp = await new Promise((r, reject) => { conn.sftp((err, sftpClient) => { if (err) { reject(new Error(`SFTP初始化失败: ${err.message}`)); } else { r(sftpClient); } }); }); resolve({ sftp, conn, close: () => { if (sftp) sftp.end(); if (conn) conn.end(); } }); } catch (err) { logger.error('❌ 过程出错:', err.message); process.exit(1); } }) } async function run() { if (!await authProjectName(config.path)) { process.exit(1); } try { let folderNames = await getFileList(); 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); } const { sftp, close } = await getSftp(); // 删除目标文件夹代码 await deleteRemoteDirectory(sftp, config.path); // 恢复目标日期代码 await resetDir(sftp, answers.selectedOption) close(); logger.success(`🎉 ${process.env.NODE_ENV}环境:项目回滚完成!`); config.cdn && config.cdn.list && await refreshCdn(config.cdn.list); process.exit(0); }).catch(error => { if (error.isTtyError) { logger.error('❌ 终端不支持交互模式'); } else { logger.error(`❌ ${process.env.NODE_ENV}环境:回滚过程中发生错误:`); logger.error(error); process.exit(1); } }); } catch (err) { logger.error('❌ 过程出错:', err.message); } } run();