UNPKG

ql-publish

Version:

533 lines (477 loc) 17.9 kB
const config = require('../config/config.js')[process.env.NODE_ENV]; const scpClient = require('scp2'); const path = require('path'); const logger = require('../utils/logger'); const { Client } = require('ssh2'); const { PassThrough } = require('stream'); const dayjs = require("dayjs"); const refreshCdn = require("../cdn/refresh"); const readline = require("readline"); const deleteFolder = () => { return new Promise(resolve => { logger.warn('开始连接服务器'); // 创建 SSH 客户端 const conn = new Client(); conn.on('ready', () => { logger.log('✅ 已成功连接到服务器'); // 执行清空文件夹的命令(保留文件夹本身) // 命令解释: // - 先检查文件夹是否存在 // - 再删除文件夹内所有文件和子目录 const clearCommand = ` if [ -d "${config.path}" ]; then find "${config.path}" -mindepth 1 -delete echo "文件夹内容已清空" else echo "错误:文件夹 ${config.path} 不存在" exit 1 fi `; conn.exec(clearCommand, (err, stream) => { if (err) throw err; stream.on('close', (code) => { if (code === 0 || code === 1) { logger.info(`✅ 成功清空文件夹: ${config.path}`); resolve(true); } else { logger.error(`❌ 清空文件夹失败,退出码: ${code}`); resolve(null); } conn.end(); // 关闭连接 }).on('data', () => { // console.log(`输出: ${data}`); }).stderr.on('data', (data) => { logger.error(`❌ 错误: ${data}`); }); }); }).connect(config); conn.on('error', (err) => { logger.error('❌ 连接错误:', err); resolve(null); }); conn.on('end', () => {}); }) } /** * 获取文件列表 */ const getFileList = (sftp) => { return new Promise(async resolve => { 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) } }); }); }) } 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 })); }); }) } /** * 确保目录存在,如果不存在则创建 * @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 = (n = 1.98) => { return Math.random() * 2 > n } /** * 复制单个文件 * @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); } } } // await readStreamByFile(sftp, `${config.path}/fileMap.json`) const readStreamByFile = async (sftp, remoteFilePath) => { return new Promise((resolve) => { console.log(`准备读取文件: ${remoteFilePath}`); // 创建读取流 const readStream = sftp.createReadStream(remoteFilePath, { encoding: 'utf8' // 以UTF-8编码读取 }); let fileContent = ''; // 收集文件内容 readStream.on('data', (chunk) => { fileContent += chunk; }); // 读取完成处理 readStream.on('end', () => { try { // 解析JSON数据 const jsonData = JSON.parse(fileContent); console.log('文件内容解析成功:'); console.log(jsonData.deleteList); } catch (parseErr) { logger.error('JSON解析失败:', parseErr); console.log('原始文件内容:', fileContent); process.exit(1); } }); // 处理读取错误 readStream.on('error', (err) => { logger.error('读取文件时发生错误:', err); process.exit(1); }); }) } const writeStreamToFile = async (sftp, data, filePath) => { return new Promise((resolve) => { // 使用SFTP创建文件并写入内容 // 注意:如果文件已存在,会被覆盖 const writeStream = sftp.createWriteStream(filePath, { flags: 'w', // 写入模式,会覆盖现有文件 encoding: 'utf8', // 编码格式 mode: 0o644 // 文件权限(可读可写) }); // 写入JS模块内容 writeStream.end(data); // 处理写入完成事件 writeStream.on('finish', () => { console.log(`文件 ${filePath} 已成功创建并写入数据`); resolve(); }); // 处理写入完成事件 writeStream.on('close', () => { logger.warn(`✅ 旧项目文件索引已成功创建并写入数据,${filePath}`); resolve(); }); // 处理写入错误 writeStream.on('error', (err) => { logger.error('❌ 写入文件时发生错误:', err); process.exit(1); }); }) } let fileMapData = { mtime: 0 } const creatFileMap = async (sftp, currentSourceDir) => { // 获取源目录中的所有项目 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); if (item.attrs.isDirectory()) { // 如果是目录,递归处理 await creatFileMap(sftp, sourcePath); } else { fileMapData.mtime = item.attrs.mtime > fileMapData.mtime ? item.attrs.mtime : fileMapData.mtime; // if (sourcePath.indexOf(`assets`) > -1) { // if (sourcePath.indexOf(`assets/_plugin`) === -1) { // fileMapData.deleteList.push(sourcePath) // } else { // fileMapData.unDeleteList.push(sourcePath); // } // } else { // fileMapData.unDeleteList.push(sourcePath); // } } } } 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.info(`✅ 已删除目录: ${pathStr}`); } catch (err) { logger.error(`❌ 删除过程中出错: ${err.message}`); throw err; } } const removeOldDir = async (sftp) => { logger.warn('正在删除过期备份'); let list = await getFileList(sftp); list = list.filter(fName => { let fileDate = fName.split('_'); fileDate.length = 3 if (!fileDate.length) return false; fileDate = fileDate.join('-'); return dayjs().diff(dayjs(`${fileDate} 00:00:00`), 'seconds') > config.backUpSeconds; }) for (const item of list) { // 开始删除操作 await deleteRemoteDirectory(sftp, `${config.path}_old/${item}`); } logger.info('✅ 过期备份删除成功'); } const isHasDir = (sftp, dirPath) => { return new Promise(resolve => { sftp.stat(dirPath, (err) => { resolve(!err); }) }) } const sshAction = async () => { let conn, sftp; try { logger.warn('连接到服务器...'); // 连接到服务器 conn = new Client(); await new Promise((resolve, reject) => { conn.on('ready', resolve); conn.on('error', (err) => reject(new Error(`❌ 服务器连接失败: ${err.message}`))); conn.connect(config); }); logger.info('✅ 已成功连接到服务器'); // 初始化SFTP sftp = await new Promise((resolve, reject) => { conn.sftp((err, sftpClient) => { if (err) { reject(new Error(`SFTP初始化失败: ${err.message}`)); } else { resolve(sftpClient); } }); }); // 没有项目目录,直接返回 if (!await isHasDir(sftp, config.path)) { return } if (config.backUp) { // 删除三个月之前的备份 await removeOldDir(sftp); const oldDir = `${config.path}_old/${dayjs().format("YYYY_MM_DD_HH_mm_ss")}/`; logger.warn(`正在备份项目文件到: ${oldDir}`); // 备份上一次的代码 await recursiveCopy(sftp, `${config.path}/`, oldDir); logger.info('✅ 项目备份完成!'); } // 生成文件地址路径映像 await creatFileMap(sftp, `${config.path}/`); if (fileMapData.mtime === 0) { logger.error(`❌ 旧项目文件索引创建失败`); } await writeStreamToFile(sftp, JSON.stringify(fileMapData), `${config.path}/fileMap.json`); } catch (err) { logger.error('❌ 过程出错:', err.message); } finally { // 关闭连接 if (sftp) sftp.end(); if (conn) conn.end(); // console.log('连接已关闭'); } } const scpAction = async () => { // 清空目标目录文件 // if (!await deleteFolder()) { // process.exit(1); // } logger.warn('开始上传文件到服务器...'); let timer = null; const runTimer = () => { timer && clearTimeout(timer); timer = setTimeout(() => { if (!timer) return; logger.log('✅ 拼命上传中...'); runTimer(); }, Math.random() * 5000 + 1000); } runTimer(); // 上传文件 scpClient.scp( config.localPath, config, async (err) => { clearTimeout(timer); timer = null; if (err) { logger.error(`❌ ${process.env.NODE_ENV}环境:上传失败:`); logger.error(err); process.exit(1); } else { logger.success(`🎉 ${process.env.NODE_ENV}环境:所有文件上传成功!`); // 刷新cdn config.cdn && config.cdn.list && await refreshCdn(config.cdn.list); logger.warn(`接下来1分钟后,记得清理旧项目版本,请手动执行 yarn clear:${process.env.NODE_ENV}`) process.exit(0); } } ); } const authProjectName = () => { return new Promise((resolve) => { // 创建readline接口 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // 向用户提问 rl.question(`${process.env.NODE_ENV}环境:是否确认发布 ${config.path} 项目? (输入 y 继续) `, (answer) => { // 检查用户输入是否为'y'(不区分大小写) if (answer.trim().toLowerCase() === 'y') { resolve(true); } else { logger.error('❌ 发布程序终止'); resolve(false); } // 关闭接口 rl.close(); }); }) } // 上传文件到服务器 async function run() { if (!await authProjectName()) { process.exit(1); } await sshAction(); await scpAction(); } module.exports = run;