ql-publish
Version:
353 lines (318 loc) • 11.8 kB
JavaScript
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();