ssh2-sftp-client 一行命令部署项目到服务器(含进度条、日志美化)

658 阅读10分钟

一、背景介绍

在开发过程中,常规的项目部署都是通过 gitlab runner 或者 jenkins 等工具,通过配置 ssh key,将代码拉取下来,然后通过 shell 脚本进行打包,上传到服务器,最后通过 shell 脚本进行部署,这些是常规的 CI/CD 流程。但是,这些工具的配置和使用相对复杂,而且需要一定的技术知识。

有些项目没有配置 CI/CD 或者公司内没有完备的部署流程,只能我们自己来操作。窃以为直接把 build 之后的 dist 文件包,上传/下载/删除文件到服务器,更方便直接,虽然看起来没有那么规范😜。更多还是看使用场景吧,如果是个人项目或者一些小项目,就没有必要去部署繁琐的 CI/CD ,毕竟像 jenkins 这类工具,最终也是要生成 dist 文件,放到 nginx 配置的根目录下,由 nginx 托管。

恰好本司有这个需求场景,就轮到大名鼎鼎的 ssh2-sftp-client 出场了。

二、关键工具 ssh2-sftp-client

ssh2-sftp-client 是一个为 Node.js 设计的 SFTP 客户端,它基于 SSH2 模块,提供了高级别的抽象和基于 PromiseAPI

几个关键点:

1.SSH2: 这里的 SSH2 不是指协议,而是指一个 node.jsSSH2 client(SSH2客户端)的实现。
github/SSH2
2.SFTP客户端: SSH File Transfer Protocol的缩写,安全文件传送协议

优点:

  • Promise 支持:所有方法均返回 Promise,便于异步编程和错误处理。
  • 丰富的 API:提供了从基本的文件列表查询到复杂的目录上传下载等全方位功能。
  • 性能优化:针对 Node.js 18 及以上版本进行了性能优化,特别是在处理大量文件时表现更佳。
  • 跨平台兼容:虽然主要使用 Unix 风格的路径分隔符,但也支持通过配置适应 Windows 服务器。

简单来说:

ssh2-sftp-client 是基于 SSH2 库进行了封装,让 文件传输 相关的方法变得更为简洁,还支持了 Promise

三、需求场景分析与实现

直接向服务器传输文件,一般使用 SCP、FTP 工具,或者是使用 ssh 通过命令行操作,这两种方式,一个是需要安装第三方软件,一个是没有 gui 界面,还得记忆命令行指令,开发体验不是很友好。

程序猿就要用程序猿的方式解决问题 🐒

我决定自己写一个工具脚本,通过 ssh2-sftp-client 库,实现一个简单的文件上传和删除功能,这是最终版的截图:

vscode:
image.png
mac shell:
image.png

怎么样是不是很炫酷,高大上🤭

下面我通过三步,来详细讲解一下实现过程。

  1. 手动操作 & ssh 命令行模式
  2. 使用 ssh2-sftp-client 编程实现
  3. 美化界面,完全版v1 v2

1. 手动操作 & ssh 命令行模式

手动模式一般是这样的:

  • npm run build
  • 打开 xftp 软件
  • 进入相应的目录
  • 删除现存的 html 目录
  • 把打包出来的 dist 重命名为 html
  • xftp 上传新打包的目录

也可以是这样:

  • npm run build
  • 登录公司服务器/堡垒机,输入帐号密码
  • 进入相应的目录
  • 删除现存的 dist 目录
  • 把打包出来的 dist 拖动到指定目录

命令行模式:

ssh zhangsan@root@192.168.10.220@qytjms.pekingulaw.cn -p 2345

有时会出现错误🙅‍‍,本人就遇到了🙉,比如:

image.png

这是当你使用 SSH 连接到一个新的主机时,系统会提示你验证该主机的真实性。因为 SSH 客户端没有该主机的公钥指纹,出于安全考虑,需要你确认是否继续连接。

继续连接:
如果你确认该主机是可信的,并且指纹是正确的,可以输入 yes 以继续连接。输入后,SSH 客户端会将该主机的公钥添加到你的 ~/.ssh/known_hosts 文件中,以便下次连接时不会再显示此警告。

再次输入命令,添加 ssh-rsa 密钥

ssh -o HostKeyAlgorithms=+ssh-rsa  zhangsan@root@192.168.10.220@qytjms.pekingulaw.cn -p 2345

image.png

之后你将能够访问目标主机。

命令行虽然快捷,但是可能会出现一些不可知的问题。

2. 使用 ssh2-sftp-client 编程实现

自己写一个工具脚本,用程序编码实现上面手动模式的1、2、3、4...步。

安装依赖:
建议 node 版本18.x,本人使用的是 18.20.5 的18最新稳定版。

pnpm install ssh2-sftp-client -D

引入依赖并实例化:

const SftpClient = require('ssh2-sftp-client');
const sftp = new SftpClient();

连接服务器:
sftp 支持 Promise,本人习惯 使用 async/await,所以使用 await 关键字。

await sftp.connect({
    host: 'qytjms.pekingulaw.cn',
    port: 2345,
    username: 'zhangsan',
    password: '123456',    
    algorithms: { // 如有需要
        serverHostKey: ['ssh-rsa'],
    },
});

列出远程目录中的文件:

const remotePath = '/home/zhangsan/'; // 远程目录
const fileList = await sftp.list(remotePath);
console.log(fileList);

如果你看到命令行输出这些,说明我们已经连接成功了。

image.png

完成所有文件操作后,别忘了断开与SFTP服务器的连接:

await sftp.end()

ssh2-sftp-client支持多种操作,比如下载文件、上传文件、删除文件、重命名文件等等。

// 上传文件
await sftp.put('/local/path/to/file.txt', '/remote/path/to/file.txt')
// 下载文件
await sftp.get('/remote/path/to/file.txt', '/local/path/to/file.txt')
// 列出目录内容
await sftp.list('/remote/path')
// 删除文件
await sftp.delete('/remote/path/to/file.txt')
// 上传文件夹
await sftp.uploadDir('本地目录''远程目录')
// 下载服务器的文件夹
await sftp.downloadDir('远程目录''本地目录')
// 重命名服务器文件夹
await sftp.rename('远程目录''远程新目录')
......

因为我们的功能是上传,删除服务器dist文件夹,同时要考虑第一次服务器没有dist文件的情况,再加上一些输出日志,所以代码如下:

const SftpClient = require('ssh2-sftp-client');
const path = require('path');

const sftp = new SftpClient();
const localPath = path.resolve(__dirname, './dist')
const remotePath = '/Default/192.168.10.220/project/front/admin'
const remotePathDir = `${remotePath}/dist`

const configFtp = {
    host: 'qytjms.pekingulaw.cn', // 使用你的测试 host
    port: 2345,              // 使用你的测试端口
    username: 'zhangsan',     // 使用拼接后的用户名
    password: '123456',        // 使用你的密码
    algorithms: { // 添加支持 ssh-rsa 算法 如有需要
        serverHostKey: ['ssh-rsa'],
    },
    // debug: console.log // 输出调试信息
}

const main = async () => {
  try {
    await sftp.connect(configFtp);
        console.log('Connected to SFTP server successfully!');
        // 文件操作
        if (await sftp.exists(remotePath)) {
            // 第一次上传没有dist文件夹,创建dist文件夹
            if (!(await sftp.exists(remotePathDir))) {
                // 递归创建目录
                await sftp.mkdir(remotePathDir, true); 
                console.log(`Directory created: ${remotePathDir}`);
            } else {
                // 删除dist文件夹
                await sftp.rmdir(remotePathDir, true)
                console.log('File deleted successfully')  
            }
     
            // 上传文件
            await sftp.uploadDir(localPath, remotePathDir);
            console.log('File uploadDir successfully');
        }
  } catch (err) {
        // 错误处理
        console.error('Error connecting to SFTP server:', err);
  } finally {
    // 关闭连接(别忘了)
    await sftp.end();
  }
};

main();

至此,一个基本的上传,下载就完成了。

这样就完成了吗?还没有,作为一个代码geek患者,当然要继续优化啦。

3. 美化界面,完全版v1 v2

上传&删除文件,需要显示进度条、百分比、文件数,这样才能知道我的进度到哪里了,否则只能是干等,也没有任何提示。

内心OS: 程序出错了?还是在上传?还是下载?还是什么情况??🤔

要想展示进度条,首先要知道操作的文件数量,然后才能知道进度。这里需要用到 fs path 模块,获取文件数量。

const fs = require('fs');
const path = require('path');

// 计算一个文件夹中文件总数
const calculateTotalSize = (dir) => {
    let totalSize = 0;
    const files = fs.readdirSync(dir);

    for (const file of files) {
        const filePath = path.join(dir, file);
        const stats = fs.statSync(filePath);
        if (stats.isDirectory()) {
            totalSize += calculateTotalSize(filePath);
        } else {
            totalSize += stats.size;
        }
    }

    return totalSize;
};

同样,我们还需要一个进度条,这里用代码实现一个简单的进度条。
长这个样子 [======-----] 60%

// 展示进度条
const displayProgressBar = (progress) => {
    const barLength = 40; // 进度条长度
    const completed = Math.round(progress * barLength);
    const bar = '='.repeat(completed) + '-'.repeat(barLength - completed);
    process.stdout.write(`\r[${bar}] ${(progress * 100).toFixed(2)}%`);
};

在上传中,我们使用 let uploadedFiles = 0 记录初始进度为0,然后每上传一个文件,uploadedFiles 加一,然后计算进度,调用 displayProgressBar(progress) 显示进度条。文件的查找都是递归调用的,因为文件夹中可能还有文件夹。

const localFiles = await getLocalFiles(localDir);
const totalFiles = localFiles.length;
let uploadedFiles = 0;

const uploadRecursive = async (localPath, remotePath) => {
    const items = await fs.readdir(localPath, { withFileTypes: true });
    for (const item of items) {
        const localItemPath = path.join(localPath, item.name);
        const remoteItemPath = `${remotePath}/${item.name}`;

        if (item.isDirectory()) {
            // 如果是目录,递归创建远程目录并上传
            if (!(await sftp.exists(remoteItemPath))) {
                await sftp.mkdir(remoteItemPath, true);
            }
            await uploadRecursive(localItemPath, remoteItemPath);
        } else {
            // 如果是文件,上传文件并更新进度
            await sftp.fastPut(localItemPath, remoteItemPath);
            uploadedFiles += 1;
            displayProgressBar(uploadedFiles / totalFiles);
        }
    }
};

完整的上传逻辑如下:

// 上传目录并显示进度
const uploadDirectoryWithProgress = async (localDir, remoteDir) => {
    const fs = require('fs').promises;

    const getLocalFiles = async (dir) => {
        let files = [];
        const items = await fs.readdir(dir, { withFileTypes: true });
        for (const item of items) {
            const fullPath = path.join(dir, item.name);
            if (item.isDirectory()) {
                const subFiles = await getLocalFiles(fullPath);
                files = files.concat(subFiles);
            } else {
                files.push(fullPath);
            }
        }
        return files;
    };

    const localFiles = await getLocalFiles(localDir);
    const totalFiles = localFiles.length;
    let uploadedFiles = 0;

    const uploadRecursive = async (localPath, remotePath) => {
        const items = await fs.readdir(localPath, { withFileTypes: true });
        for (const item of items) {
            const localItemPath = path.join(localPath, item.name);
            const remoteItemPath = `${remotePath}/${item.name}`;

            if (item.isDirectory()) {
                // 如果是目录,递归创建远程目录并上传
                if (!(await sftp.exists(remoteItemPath))) {
                    await sftp.mkdir(remoteItemPath, true);
                }
                await uploadRecursive(localItemPath, remoteItemPath);
            } else {
                // 如果是文件,上传文件并更新进度
                await sftp.fastPut(localItemPath, remoteItemPath);
                uploadedFiles += 1;
                displayProgressBar(uploadedFiles / totalFiles);
            }
        }
    };

    if (totalFiles === 0) {
        console.log('No files to upload.');
        return;
    }

    console.log('Starting upload...');
    await uploadRecursive(localDir, remoteDir);
    process.stdout.write('\n'); // 进度条后换行
    console.log('Upload completed!');
};

下载也是同样的逻辑,完整版代码如下:

const SftpClient = require('ssh2-sftp-client');
const path = require('path');
const fs = require('fs');

const sftp = new SftpClient();
const localPath = path.resolve(__dirname, './dist');
const remotePath = '/Default/192.168.10.219/project/front/admin';
const remotePathDir = `${remotePath}/dist`;

const configFtp = {
    host: 'qytjms.pekingulaw.cn', // 使用你的测试 host
    port: 2345,              // 使用你的测试端口
    username: 'zhangsan',     // 使用拼接后的用户名
    password: '123456',        // 使用你的密码
    algorithms: { // 添加支持 ssh-rsa 算法 如有需要
        serverHostKey: ['ssh-rsa'],
    },
    // debug: console.log // 输出调试信息
};


// 展示进度条
const displayProgressBar = (progress) => {
    const barLength = 40; // 进度条长度
    const completed = Math.round(progress * barLength);
    const bar = '='.repeat(completed) + '-'.repeat(barLength - completed);
    process.stdout.write(`\r[${bar}] ${(progress * 100).toFixed(2)}%`);
};

// 计算一个文件夹中文件总数
const calculateTotalSize = (dir) => {
    let totalSize = 0;
    const files = fs.readdirSync(dir);

    for (const file of files) {
        const filePath = path.join(dir, file);
        const stats = fs.statSync(filePath);
        if (stats.isDirectory()) {
            totalSize += calculateTotalSize(filePath);
        } else {
            totalSize += stats.size;
        }
    }

    return totalSize;
};

// 上传目录并显示进度
const uploadDirectoryWithProgress = async (localDir, remoteDir) => {
    const fs = require('fs').promises;

    const getLocalFiles = async (dir) => {
        let files = [];
        const items = await fs.readdir(dir, { withFileTypes: true });
        for (const item of items) {
            const fullPath = path.join(dir, item.name);
            if (item.isDirectory()) {
                const subFiles = await getLocalFiles(fullPath);
                files = files.concat(subFiles);
            } else {
                files.push(fullPath);
            }
        }
        return files;
    };

    const localFiles = await getLocalFiles(localDir);
    const totalFiles = localFiles.length;
    let uploadedFiles = 0;

    const uploadRecursive = async (localPath, remotePath) => {
        const items = await fs.readdir(localPath, { withFileTypes: true });
        for (const item of items) {
            const localItemPath = path.join(localPath, item.name);
            const remoteItemPath = `${remotePath}/${item.name}`;

            if (item.isDirectory()) {
                // 如果是目录,递归创建远程目录并上传
                if (!(await sftp.exists(remoteItemPath))) {
                    await sftp.mkdir(remoteItemPath, true);
                }
                await uploadRecursive(localItemPath, remoteItemPath);
            } else {
                // 如果是文件,上传文件并更新进度
                await sftp.fastPut(localItemPath, remoteItemPath);
                uploadedFiles += 1;
                displayProgressBar(uploadedFiles / totalFiles);
            }
        }
    };

    if (totalFiles === 0) {
        console.log('No files to upload.');
        return;
    }

    console.log('Starting upload...');
    await uploadRecursive(localDir, remoteDir);
    process.stdout.write('\n'); // 进度条后换行
    console.log('Upload completed!');
};

// 删除目录并显示进度
const deleteDirectoryWithProgress = async (remoteDir) => {
    if (!(await sftp.exists(remoteDir))) return;

    // 获取所有文件的总数,用于正确计算进度
    const getTotalFiles = async (dir) => {
        let count = 0;
        const files = await sftp.list(dir);
        for (const file of files) {
            // 如果是目录,递归计算目录中的文件数
            if (file.type === 'd') {
                count += await getTotalFiles(`${dir}/${file.name}`);
            } else {
                count += 1;
            }
        }
        return count;
    };

    const totalFiles = await getTotalFiles(remoteDir);
    let deletedFiles = 0;

    const deleteRecursive = async (dir) => {
        const files = await sftp.list(dir);
        for (const file of files) {
            const remoteFile = `${dir}/${file.name}`;
            if (file.type === 'd') {
                await deleteRecursive(remoteFile);
            } else {
                await sftp.delete(remoteFile);
                deletedFiles += 1;
                displayProgressBar(deletedFiles / totalFiles);
            }
        }
        await sftp.rmdir(dir);
    };

    if (totalFiles === 0) {
        console.log('No files to delete.');
        return;
    }

    console.log('Starting deletion...');
    await deleteRecursive(remoteDir);
    process.stdout.write('\n'); // 进度条后面换行
    console.log('Deletion completed!');
};


const main = async () => {
    try {
        await sftp.connect(configFtp);
        console.log('Connected to SFTP server successfully!');

        if (await sftp.exists(remotePath)) {
            if (await sftp.exists(remotePathDir)) {
                console.log(`Remote directory exists. Deleting: ${remotePathDir}`);
                await deleteDirectoryWithProgress(remotePathDir);
            }

            console.log(`Creating remote directory: ${remotePathDir}`);
            await sftp.mkdir(remotePathDir, true);

            console.log('Uploading directory...');
            await uploadDirectoryWithProgress(localPath, remotePathDir);
        } else {
            console.error(`Remote path does not exist: ${remotePath}`);
        }
    } catch (err) {
        console.error('Error:', err);
    } finally {
        await sftp.end();
        console.log('SFTP connection closed.');
    }
};

main();

最终,我们的界面长这个样子:
vscode:
image.png image.png mac shell: image.png

基本已经符合工程化要求,v1版本完成!

还是有几个小问题:

进度条不美观,log日志没有颜色区分,都是代码实现的,阅读体验不好,我们再来优化一下。

这两个库都是业内应用最广泛的,也是社区最活跃的,使用起来非常简单。

// 美化进度条
const cliProgress = require('cli-progress');
const progressBar = new cliProgress.SingleBar({
    format: `${chalk.red('Delete Progress')} |${chalk.cyan('{bar}')}| {percentage}% | {value}/{total} files`,
    barCompleteChar: '\u2588',
    barIncompleteChar: '\u2591',
    hideCursor: true,
});

注意❗️chalk 默认安装最新版本5.x,改为了ESM方式,代码中是CommonJS方式,所以需要降级安装4.x版本。当然,如果你要使用5.x,需要自行修改 package.json 配置,改代码。本人为了保持代码写法的统一,使用的是4.x版本。

const chalk = require('chalk');
const log = {
    success: (msg) => console.log(chalk.green(msg)),
    error: (msg) => console.error(chalk.red(msg)),
    info: (msg) => console.log(chalk.blue(msg)),
    warning: (msg) => console.log(chalk.yellow(msg)),
};

使用起来非常简单,只需要在需要的地方调用log.success、log.error、log.info、log.warning即可。

上传&删除逻辑基本不变,只是修改进度条和日志的输出,最终代码如下:

const SftpClient = require('ssh2-sftp-client');
const path = require('path');
const fs = require('fs').promises;
const cliProgress = require('cli-progress');
const chalk = require('chalk');

const sftp = new SftpClient();
const localPath = path.resolve(__dirname, './dist'); // 本地路径
const remotePath = '/Default/192.168.10.219/project/front/admin'; // 远程基础路径
const remotePathDir = `${remotePath}/dist`; // 远程 dist 文件夹完整路径

const configFtp = {
    host: 'qytjms.pekingulaw.cn', // 使用你的测试 host
    port: 2345,              // 使用你的测试端口
    username: 'zhangsan',     // 使用拼接后的用户名
    password: '123456',        // 使用你的密码
    algorithms: { // 添加支持 ssh-rsa 算法 如有需要
        serverHostKey: ['ssh-rsa'],
    },
    // debug: console.log // 输出调试信息
};

const log = {
    success: (msg) => console.log(chalk.green(msg)),
    error: (msg) => console.error(chalk.red(msg)),
    info: (msg) => console.log(chalk.blue(msg)),
    warning: (msg) => console.log(chalk.yellow(msg)),
};

// 获取所有本地文件(包括子目录)
const getAllLocalFiles = async (dir) => {
    let files = [];
    const items = await fs.readdir(dir, { withFileTypes: true });
    for (const item of items) {
        const fullPath = path.join(dir, item.name);
        if (item.isDirectory()) {
            const subFiles = await getAllLocalFiles(fullPath);
            files = files.concat(subFiles);
        } else {
            files.push(fullPath);
        }
    }
    return files;
};

// 删除远程目录及其内容(整体进度条)
const deleteDirectoryWithProgress = async (remoteDir) => {
    log.info(`Starting deletion of: ${remoteDir}`);

    const allRemoteItems = [];

    // 收集所有需要删除的项目
    const collectRemoteItems = async (dir) => {
        const items = await sftp.list(dir);
        for (const item of items) {
            const itemPath = `${dir}/${item.name}`;
            if (item.type === 'd') {
                await collectRemoteItems(itemPath);
            }
            allRemoteItems.push(itemPath);
        }
        allRemoteItems.push(dir); // 添加目录本身
    };

    await collectRemoteItems(remoteDir);

    const totalItems = allRemoteItems.length;
    if (totalItems === 0) {
        log.warning('No items to delete.');
        return;
    }

    // 删除并更新进度条
    const progressBar = new cliProgress.SingleBar({
        format: `${chalk.red('Delete Progress')} |${chalk.cyan('{bar}')}| {percentage}% | {value}/{total} files`,
        barCompleteChar: '\u2588',
        barIncompleteChar: '\u2591',
        hideCursor: true,
    });

    progressBar.start(totalItems, 0);
    for (let i = 0; i < totalItems; i++) {
        const itemPath = allRemoteItems[totalItems - i - 1]; // 逆序删除(先文件后目录)
        try {
            if (await sftp.exists(itemPath)) {
                if ((await sftp.stat(itemPath)).isDirectory) {
                    await sftp.rmdir(itemPath);
                } else {
                    await sftp.delete(itemPath);
                }
            }
        } catch (err) {
            log.error(`Error deleting: ${itemPath}`);
            log.error(err.message);
        }
        progressBar.update(i + 1);
    }
    progressBar.stop();
    log.success('Deletion completed!');
};

// 上传目录及文件(整体进度条)
const uploadDirectoryWithProgress = async (localDir, remoteDir) => {
    const localFiles = await getAllLocalFiles(localDir);
    const totalFiles = localFiles.length;

    if (totalFiles === 0) {
        log.warning('No files to upload.');
        return;
    }

    log.info(`Starting upload of ${totalFiles} files...`);

    const progressBar = new cliProgress.SingleBar({
        format: `${chalk.blue('Upload Progress')} |${chalk.cyan('{bar}')}| {percentage}% | {value}/{total} files`,
        barCompleteChar: '\u2588',
        barIncompleteChar: '\u2591',
        hideCursor: true,
    });
    
    progressBar.start(totalFiles, 0);

    for (let i = 0; i < totalFiles; i++) {
        const localFilePath = localFiles[i];
        const relativePath = path.relative(localDir, localFilePath);
        const remoteFilePath = `${remoteDir}/${relativePath}`;
        const remoteDirPath = path.dirname(remoteFilePath);

        try {
            if (!(await sftp.exists(remoteDirPath))) {
                // 递归创建远程目录
                await sftp.mkdir(remoteDirPath, true);
            }
            await sftp.fastPut(localFilePath, remoteFilePath);
        } catch (err) {
            log.error(`Error uploading: ${localFilePath}`);
            log.error(err.message);
        }
        progressBar.update(i + 1);
    }
    progressBar.stop();
    log.success('Upload completed!');
};

// 主函数
const main = async () => {
    try {
        await sftp.connect(configFtp);
        log.success('Connected to SFTP server successfully!');

        // 检查远程基础路径是否存在
        if (await sftp.exists(remotePath)) {
            // 检查 dist 文件夹是否存在
            if (await sftp.exists(remotePathDir)) {
                await deleteDirectoryWithProgress(remotePathDir); // 删除已有目录及内容
            } else {
                log.info(`Remote dist folder does not exist. Creating and uploading to: ${remotePathDir}`);
            }
            await uploadDirectoryWithProgress(localPath, remotePathDir); // 上传目录及文件
        } else {
            log.error(`Remote base path does not exist: ${remotePath}`);
        }
    } catch (err) {
        log.error(`Error: ${err.message}`);
    } finally {
        await sftp.end();
        log.info('SFTP connection closed.');
    }
};

main();

最终界面长这个样子,大功告成!

image.png

我们的用户名密码等敏感信息直接写在了代码里,非常不安全,所以我们需要把配置文件单独抽离出来,可以自定义 sftpConfig.js 文件,也可以写在 .env 环境变量里,同时在 .gitignore 文件里忽略下,这样就不会把敏感信息提交到git仓库里了😂。

上传部署项目再和我们的打包构建命令结合起来,在 package.jsonscripts 里加上:

"scripts": {
    "serve": "vite --open --mode dev",
    "serve:mock": "vite --open --mode mock",
    "serve:prod": "vite --open --mode prod",
    "build:dev": "vite build --mode dev",
    "build:prod": "vite build --mode prod",
    "preview": "vite preview",
    "deploy": "pnpm run build:dev && node scripts/uploadToServer.js",
    "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx"
},

这样,只需要一行命令就完成了打包和上传部署,非常方便。

四、总结

ssh2-sftp-client 是一个强大且易用的 Node.js 插件,用于简化 SFTP 文件传输操作。无论是上传、下载、删除文件,还是列出目录内容,这个插件都提供了直观且高效的 API,能够显著降低实现这些功能的复杂性。如果你的 Node.js 应用需要集成文件传输功能,不妨试试 ssh2-sftp-client