UtilsSSH.mjs

import path from "path";
import { NodeSSH } from "node-ssh";

/**
 * Utilities functions for SSH
 * @module SSH
 *
 * @example
 * import {
 *   getSSHClient,
 *   sshConnect,
 *   joinPath,
 *   zipFolder,
 *   sshPutFile,
 *   sshExecCommand,
 * } from "nsuite";
 * import { PATH_ROOT } from "#scripts/ConstantUtils";
 * import { sshConfig } from "#hosts/Shanghai-Tencent/nginx/build/config";
 *
 * const sshClient = getSSHClient();
 * await sshConnect({
 *   ssh: sshClient,
 *   ...sshConfig,
 * });
 * const pathDist = joinPath(PATH_ROOT, "apps-home/blog", "dist");
 * const pathDistZip = joinPath(pathDist, "../dist.zip");
 * await zipFolder({
 *   pathFolder: pathDist,
 *   pathOutputFile: pathDistZip,
 * });
 *
 * const pathRemote = "/www/sites/www.orzzone.com/public";
 * const pathRemoteZip = `${pathRemote}/dist.zip`;
 * await sshPutFile({
 *   ssh: sshClient,
 *   localFile: pathDistZip,
 *   remoteFile: pathRemoteZip,
 * });
 *
 * const execCommand = async (command: string): Promise<void> => {
 *   await sshExecCommand({
 *     ssh: sshClient,
 *     cwd: pathRemote,
 *     command,
 *   });
 * };
 * await execCommand("rm dist");
 * await execCommand("unzip -o dist.zip");
 * await execCommand("rm dist.zip");
 *
 * process.exit(0);
 */

/**
 * @typedef {import('node-ssh').NodeSSH} SSH
 * @typedef {import('node-ssh').SSHGetPutDirectoryOptions} GetPutDirectoryOptions
 * @typedef {import('node-ssh').SSHPutFilesOptions} PutFilesOptions
 */

/**
 * @typedef {Object} PathPair
 * @property {string} local
 * @property {string} remote
 */

/**
 * Get SSH instance
 * @returns {SSH}
 */
export function getSSHClient() {
  return new NodeSSH();
}

/**
 * @typedef {Object} ParamsConnect
 * @property {SSH} ssh
 * @property {string} host
 * @property {number} port
 * @property {string} username
 * @property {string} password
 */

/**
 * Connect to SSH
 * @param {ParamsConnect} payload
 * @returns {Promise<void>}
 */
export async function sshConnect(payload) {
  const { ssh, ...config } = payload;
  await ssh.connect(config);
}

/**
 * @callback SSHUploadFileCallback
 * @param {string} local
 * @param {string} remote
 * @param {Error | null} error
 */

/**
 * @typedef {Object} ParamsPutDir
 * @property {SSH} ssh
 * @property {string} fromPath
 * @property {string} toPath
 * @property {GetPutDirectoryOptions} [options]
 * @property {SSHUploadFileCallback} [uploadCallback] - callback function for upload progress
 */

/**
 * @typedef {Object} ReturnPutDir
 * @property {boolean} success
 * @property {PathPair[]} failItems
 * @property {PathPair[]} successItems
 */

/**
 * Put directory
 * @param {ParamsPutDir} payload
 * @returns {Promise<ReturnPutDir>}
 */
export async function sshPutDirectory(payload) {
  const { ssh, fromPath, toPath, options, uploadCallback } = payload;
  /** @type {PathPair[]} */
  const failItems = [];
  /** @type {PathPair[]} */
  const successItems = [];
  const success = await ssh.putDirectory(fromPath, toPath, {
    recursive: true,
    concurrency: 10,
    validate(itemPath) {
      const baseName = path.basename(itemPath);
      return baseName !== "node_modules";
    },
    tick(local, remote, error) {
      if (typeof uploadCallback === "function") {
        uploadCallback(local, remote, error);
      }
      if (error) {
        failItems.push({ local, remote });
      } else {
        successItems.push({ local, remote });
      }
    },
    ...(options || {}),
  });

  return {
    success,
    failItems,
    successItems,
  };
}

/**
 * @typedef {Object} ParamsSSHGetDir
 * @property {SSH} ssh
 * @property {string} localDirectory
 * @property {string} remoteDirectory
 * @property {GetPutDirectoryOptions} [options]
 */

/**
 * Download directory from remote server
 * @param { ParamsSSHGetDir } payload
 * @returns {Promise<boolean>}
 */
export async function sshGetDirectory(payload) {
  const { ssh, localDirectory, remoteDirectory, options } = payload;
  return await ssh.getDirectory(localDirectory, remoteDirectory, options);
}

/**
 * @typedef {Object} ParamsSSHGetFile
 * @property {SSH} ssh
 * @property {string} localFile
 * @property {string} remoteFile
 * @property {import('ssh2').SFTPWrapper | null} [givenSftp]
 * @property {import('ssh2').TransferOptions} [transferOptions]
 */

/**
 * Download file from server
 * @param {ParamsSSHGetFile} payload
 * @returns {Promise<void>}
 */
export async function sshGetFile(payload) {
  const { ssh, localFile, remoteFile, givenSftp, transferOptions } = payload;
  return await ssh.getFile(localFile, remoteFile, givenSftp, transferOptions);
}

/**
 * Put file to server
 * @param {ParamsSSHGetFile} payload
 * @returns {Promise<void>}
 */
export async function sshPutFile(payload) {
  const { ssh, localFile, remoteFile, givenSftp, transferOptions } = payload;
  return await ssh.putFile(localFile, remoteFile, givenSftp, transferOptions);
}

/**
 * @typedef {Object} ParamsPutFiles
 * @property {SSH} ssh
 * @property {PathPair[]} files
 * @property {PutFilesOptions} [options]
 */

/**
 * Put files
 * @param {ParamsPutFiles} payload
 * @returns {Promise<void>}
 */
export async function sshPutFiles(payload) {
  const { ssh, files, options } = payload;
  return ssh.putFiles(files, options);
}

/**
 * @typedef {Object} ParamsExecCommand
 * @property {SSH} ssh
 * @property {string} cwd
 * @property {string} command
 * @property {function(Buffer): void} [onStdout]
 * @property {function(Buffer): void} [onStderr]
 */

/**
 * Execute command
 * @param {ParamsExecCommand} payload
 * @returns {Promise<void>}
 */
export async function sshExecCommand(payload) {
  const { ssh, cwd, command, onStdout, onStderr } = payload;
  await ssh.execCommand(command, {
    cwd,
    onStdout(chunk) {
      if (typeof onStdout === "function") {
        onStdout(chunk);
      }
    },
    onStderr(chunk) {
      if (typeof onStderr === "function") {
        onStderr(chunk);
      }
    },
  });
}