前端自动化部署node-ssh2-archiver
环境准备
本文采用vue-cli搭建项目2
npm i ssh2 archiver
整体思路
项目打包=>将打包文件压缩=>建立ssh连接=>通过sftp协议上传=>exec在远程服务器上执行命令 解压 删除压缩包 => 删除本地压缩包 关闭连接。
archiver fs 压缩文件
const fs =require("fs");
const archiver = require("archiver");
const output = fs.createWriteStream(__dirname + "/example.zip");
//archiver(format, options): 创建一个新的 archiver 实例。
//format: 压缩文件的格式,如 'zip'、'tar' 等。
//options: 可选配置对象,用于设置压缩选项,例如压缩级别等
const archive = archiver("zip",{zlib:{level:9}});
// 捕获错误事件
output.on('close', () => {
console.log(`Archive created. Total bytes: ${archive.pointer()}`);
});
archive.on('error', (err) => {
throw err;
});
//archive.pipe(outputStream): 将 archiver 的输出流连接到指定的可写流(例如文件写入流),以便将压缩文件写入磁盘或发送到其他目标。
archive.pipe(output);
//archive.file(filename, options): 将指定的文件添加到压缩文件中。
//filename: 要添加的文件路径。
//options: 可选配置对象,用于设置文件在压缩文件中的路径、权限等
archive.file("test.js",{name:"test.js"}); //单个文件
archive.file("test1.js",{name:"test1.js"}); //单个文件
//archive.directory(directory, dest, data): 将指定目录中的所有文件添加到压缩文件中。
//directory: 要添加的目录路径。
//dest: 目录在压缩文件中的路径。
//data: 可选配置对象,用于设置目录添加选项,例如递归、过滤等。
// 添加整个目录到压缩包
archive.directory('my-directory/', 'my-directory');
archive.finalize(): 完成压缩操作,生成压缩文件。
// 最后关闭压缩流,这将触发 `close` 事件
archive.finalize();
ssh2用法
建立连接、监听
let Client = require("ssh2").Client;
let conn = new Client();
conn.on("ready",()=>{ //连接完成监听
//主要逻辑在建立连接后操作
}).on("error",()=>{ //连接错误监听
}).on("end",()=>{ //连接已结束监听
}).on("close",()=>{ //关闭连接监听
}).connect({ //连接配置/建立连接动作
host:"XXX.XXX.X.XXX",
port:"22",
username:"username",
password:"password",
})
sftp ssh文件传输
sftp.fastPut
(localPath, remoteFilePath[, options], callback): 快速将本地文件上传到远程服务器。
localPath
: 本地文件路径。
remoteFilePath
: 远程文件路径。
options
: 可选参数,用于设置上传选项,例如并发数等。
callback
: 上传完成后的回调函数。
//省略了ssh建立连接的步骤 在on("ready)这种操作
conn.sftp((err,sftp)=>{
if(err){ //错误
console.log(err)
}else{
sftp.fastPut(localPath,remoteFilePath,(err,result)=>{
if(err){
console.log(err);
return
}
console.log(result)
})
}
})
exec ssh 在远程服务器上执行命令
conn.exec
(command, [options], callback):在远程服务器上执行命令。
command
:是要执行的命令
options
:是一个可选的对象,用于指定执行命令的参数
callback
:是一个回调函数,用于处理命令执行结果。
//省略了ssh建立连接的步骤 在on("ready)这种操作
conn.exec("ll",(err,stream)=>{
stream.on("close",(code,signal)=>{
console.log("当流被关闭时触发。回调函数的参数为流的状态码和信号")
}).on("data",data=>{
console.log("当收到数据时触发。回调函数的参数为收到的数据")
console.log(data.toString())
}).stderr.on("data",data=>{
//捕获远程命令执行过程中的标准错误输出
console.log('STDERR: ' + data);
})
})
整套部署流程代码
let Config = {
buildDist:"XXX", //你需要的打包后的文件夹名称 默认dist
buildCommand:"npm run build", //打包命令
}
const fs =require("fs");
const archiver = require("archiver");
const {Client} = require("ssh2");
//ssh连接
Class SSH {
constructor({host,port,username,password}){
this.server={
host,
port,
username,
password
};
this.conn = new Client()
}
//连接服务器
connectServer(){
return new Promise((resolve,reject)=>{
this.conn.on("ready",()=>{
resolve({success:true})
}).on("error",(error)=>{
resolve({success:true,error})
}).on("end",()=>{
console.log("SSH连接已结束")
}).on("close",()=>{
console.log("SSH连接已关闭")
}).connect(this.server)
})
}
//上传文件
uploadFile({localPath,remoteFilePath}){
return new Promise((resolve,reject)=>{
this.conn.sftp((error,sftp)=>{
if(err)reject({success:false,error});
sftp.fastPut(localPath,remoteFilePath,(error,result)=>{
if(err)reject({success:false,error})
resolve({success:true,result})
})
})
})
}
//执行ssh命令
execSSH(command){
return new Promise((resolve,reject)=>{
this.conn.exec(command,(error,stream)=>{
if (err || !stream)reject({success: false,error});
stream.on("data",data=>{
console.log(data.toString().trim())
}).on("close",(code,signal)=>{
resolve({success:true})
}).stderr.on("data",data=>{
resolve({success:false,error.data.toString().trim()})
})
})
})
}
//结束连接
connEnd(){
this.conn.end();
}
}
const {exec} = require("child_process");
//用子进程调用打包命令 node.js
//本地压缩删除构建操作
class File {
construction(fileName){
this.fileName=fileName;
}
//执行cli中的打包命令
buildProject(){
console.log("---开始编译打包文件---");
return new Promise((resolve,reject)=>{
exec(Config.buildCommand,{maxBuffer: 200000 * 1024},async (error,stdout,stderr)=>{
if (error) {
console.error(error);
reject({
error,
success: false,
});
} else if (stdout) {
resolve({
stdout,
success: true,
});
} else {
console.error(stderr);
reject({
error: stderr,
success: false,
});
}
})
})
}
//压缩文件
zipFile(filePath){
return new Promise((reslove,reject)=>{
letoutput = fs.createWriteStream(__dirname + "/example.zip");
let archive = archiver("zip", {
zlib: { level: 9 }, // 设置压缩级别
});
output.on("close", function () {
console.log(`---压缩文件总共 ${archive.pointer()} 字节---`);
resolve({
success: true,
});
});
// 存档警告
archive.on("warning", function (err) {
if (err.code === "ENOENT") {
console.error("----stat故障和其他非阻塞错误----");
} else {
console.error("----压缩失败----");
}
reject(err);
});
archive.pipe(output);
archive.directory(filePath, false);
archive.finalize().then(() => {});
})
}
//删除文件方法
deleteLocalFile(){
return new Promise((resolve, reject) => {
fs.unlink(this.fileName, function (error) {
if (error) {
reject({
success: false,
error,
});
} else {
resolve({
success: true,
});
}
});
});
}
stopProgress(){
this.deleteLocalFile()
.catch((e) => {
console.error("----删除本地文件失败,请手动删除----");
console.error(e);
})
.then(() => {
console.log("----已删除本地压缩包文件----");
});
}
}
//操作函数
async function handleUpload (sshConfig,fileName) {
let sshCon = new SSH(sshConfig);
let sshRes = await sshCon.connectServer();
if(!sshRes || !sshRes.success){
console.error("服务器连接失败");
return false
}
console.log("---连接服务器成功,开始上传文件---");
let uploadRes = await sshCon.uploadFile({
localPath:path.resolve(__dirname,fileName);
remoteFilePath:`${sshConfig.catalog}/${fileName})`;
})
if (!uploadRes || !uploadRes.success) {
console.error("----上传文件失败,请重新上传----");
return false;
}
let timeStr = getTimeStr();
let file_bak = `${Config.buildDist}-${timeStr}-bak.zip`;
let zipCopyFile = await sshCon.execSsh(
`if [ -d ${sshConfig.catalog}/${Config.buildDist} ]; then cd ${sshConfig.catalog}; zip -r ${file_bak} ${Config.buildDist}; else echo "not exists"; fi`
);
console.log("----开始解压文件----");
let zipRes = await sshCon
.execSsh(
`unzip -o ${sshConfig.catalog + "/" + fileName} -d ${sshConfig.catalog}/${
Config.buildDist
}`
)
if (!zipRes || !zipRes.success) {
console.error("----解压文件失败,请手动解压zip文件----");
console.error(`----错误原因:${zipRes.error}----`);
}
f (Config.deleteFile) {
console.log("----解压文件成功,开始删除上传的压缩包----");
// 注意:rm -rf为危险操作,请勿对此段代码做其他非必须更改
let deleteZipRes = await sshCon
.execSsh(`rm -rf ${sshConfig.catalog + "/" + fileName}`)
if (!deleteZipRes || !deleteZipRes.success) {
console.error("----删除文件失败,请手动删除zip文件----");
console.error(`----错误原因:${deleteZipRes.error}----`);
}
// 结束ssh连接
sshCon.connEnd();
}
function getTimeStr() {
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
return `${year}_${month}_${day}`;
}
// 执行前端部署
(async () => {
// 压缩包的名字
let timeStr = getTimeStr();
const fileName =
`${Config.buildDist}-` +
timeStr +
"-" +
Math.random().toString(16).slice(2) +
".zip";
let file = new File(fileName);
// 打包文件
let buildRes = await file.buildProject().catch((e) => {
console.error(e);
});
if (!buildRes || !buildRes.success) {
console.error("----编译打包文件出错----");
return false;
}
console.log(buildRes.stdout);
console.log("----编译打包文件完成----");
// 压缩文件
let res = await file.zipFile(`${Config.buildDist}/`).catch(() => {});
if (!res || !res.success) return false;
console.log("----开始进行SSH连接----");
await sshUpload(Config, fileName);
console.log("----部署成功,正在为您删除本地压缩包----");
file.stopProgress();
})();