From 83c2d743bc53caefd24926ac837fcb17961583cb Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Mon, 24 Mar 2025 23:48:34 +0800 Subject: [PATCH] chore: --- .../plugins/plugin-lib/src/scp/constant.ts | 35 - packages/plugins/plugin-lib/src/scp/index.ts | 568 ---------------- packages/plugins/plugin-lib/src/scp/types.ts | 124 ---- packages/plugins/plugin-lib/src/scp/utils.ts | 639 ------------------ packages/plugins/plugin-lib/src/ssh/ssh.ts | 4 +- 5 files changed, 2 insertions(+), 1368 deletions(-) delete mode 100644 packages/plugins/plugin-lib/src/scp/constant.ts delete mode 100644 packages/plugins/plugin-lib/src/scp/index.ts delete mode 100644 packages/plugins/plugin-lib/src/scp/types.ts delete mode 100644 packages/plugins/plugin-lib/src/scp/utils.ts diff --git a/packages/plugins/plugin-lib/src/scp/constant.ts b/packages/plugins/plugin-lib/src/scp/constant.ts deleted file mode 100644 index 89b7cce4..00000000 --- a/packages/plugins/plugin-lib/src/scp/constant.ts +++ /dev/null @@ -1,35 +0,0 @@ -export enum errorCode { - generic = "ERR_GENERIC_CLIENT", - connect = "ERR_NOT_CONNECTED", - badPath = "ERR_BAD_PATH", - permission = "EACCES", - notexist = "ENOENT", - notdir = "ENOTDIR", -} - -export enum targetType { - writeFile = 1, - readFile = 2, - writeDir = 3, - readDir = 4, - readObj = 5, - writeObj = 6, -} - -export const CLIENT_EVENTS = new Set([ - "banner", - "ready", - "tcp connection", - "x11", - "keyboard-interactive", - "change password", - "error", - "end", - "close", - "timeout", - "connect", - "greeting", - "handshake", - "hostkeys", - "unix connection", -]); diff --git a/packages/plugins/plugin-lib/src/scp/index.ts b/packages/plugins/plugin-lib/src/scp/index.ts deleted file mode 100644 index 751fa3fa..00000000 --- a/packages/plugins/plugin-lib/src/scp/index.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { EventEmitter } from "events"; -import { mkdirSync, readdirSync, existsSync } from "fs"; -import { join, win32, posix } from "path"; -import { Client as SSHClient, ConnectConfig, InputAttributes, SFTPWrapper, Stats, TransferOptions, WriteFileOptions, ParsedKey, UNIXConnectionDetails, AcceptConnection, RejectConnection } from "ssh2"; -import { targetType } from "./constant.js"; -import * as utils from "./utils.js"; -import { ClientEvents } from "./types.js"; - -export type TScpOptions = ConnectConfig & { - remoteOsType?: "posix" | "win32"; - events?: ClientEvents; -}; - -export class ScpClient extends EventEmitter { - sftpWrapper: SFTPWrapper | null = null; - sshClient: SSHClient | null = null; - remotePathSep = posix.sep; - endCalled = false; - errorHandled = false; - - constructor(options: TScpOptions) { - super(); - - const ssh = new SSHClient(); - ssh - .on("connect", () => this.emit("connect")) - .on("ready", () => { - ssh.sftp((err, sftp) => { - if (err) { - throw err; - } - // save for reuse - this.sftpWrapper = sftp; - this.emit("ready"); - }); - }) - .on("error", err => this.emit("error", err)) - .on("end", () => this.emit("end")) - .on("close", () => { - if (!this.endCalled) { - this.sftpWrapper = null; - } - this.emit("close"); - }) - .on("keyboard-interactive", (name, instructions, instructionsLang, prompts, finish) => this.emit("keyboard-interactive", name, instructions, instructionsLang, prompts, finish)) - .on("change password", (message, done) => this.emit("change password", message, done)) - .on("tcp connection", (details, accept, reject) => this.emit("tcp connection", details, accept, reject)) - .on("banner", message => this.emit("banner", message)) - .on("greeting", greeting => this.emit("banner", greeting)) - .on("handshake", negotiated => this.emit("handshake", negotiated)) - .on("hostkeys", (keys: ParsedKey[]) => this.emit("hostkeys", keys)) - .on("timeout", () => this.emit("timeout")) - .on("unix connection", (info: UNIXConnectionDetails, accept: AcceptConnection, reject: RejectConnection) => this.emit("unix connection", info, accept, reject)) - .on("x11", message => this.emit("x11", message)); - ssh.connect(options); - this.sshClient = ssh; - - if (options.remoteOsType === "win32") { - this.remotePathSep = win32.sep; - } - } - - /** - * Uploads a file from `localPath` to `remotePath` using parallel reads for faster throughput. - */ - public async uploadFile(localPath: string, remotePath: string, options: TransferOptions = {}): Promise { - utils.haveConnection(this, "uploadFile"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.fastPut(localPath, remotePath, options, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Downloads a file at `remotePath` to `localPath` using parallel reads for faster throughput. - */ - public async downloadFile(remotePath: string, localPath: string, options: TransferOptions = {}): Promise { - utils.haveConnection(this, "downloadFile"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.fastGet(remotePath, localPath, options, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Clean a directory in remote server - */ - public async emptyDir(dir: string): Promise { - utils.haveConnection(this, "uploadDir"); - try { - const isExist = await this.exists(dir); - - if (!isExist) { - await this.mkdir(dir); - } else if (isExist === "d") { - await this.rmdir(dir); - await this.mkdir(dir); - } - } catch (error) { - throw error; - } - } - - public async uploadDir(src: string, dest: string): Promise { - utils.haveConnection(this, "uploadDir"); - try { - const isExist = await this.exists(dest); - - if (!isExist) { - await this.mkdir(dest); - } - - const dirEntries = readdirSync(src, { - encoding: "utf8", - withFileTypes: true, - }); - - for (const e of dirEntries) { - if (e.isDirectory()) { - const newSrc = join(src, e.name); - const newDst = utils.joinRemote(this, dest, e.name); - await this.uploadDir(newSrc, newDst); - } else if (e.isFile()) { - const newSrc = join(src, e.name); - const newDst = utils.joinRemote(this, dest, e.name); - await this.uploadFile(newSrc, newDst); - - // this.client.emit('upload', {source: src, destination: dst}) - } - } - } catch (error) { - throw error; - } - } - - public async downloadDir(remotePath: string, localPath: string) { - utils.haveConnection(this, "downloadDir"); - const remoteInfo: any = await utils.checkRemotePath(this, remotePath, targetType.readDir); - if (!remoteInfo.valid) { - throw new Error(remoteInfo.msg); - } - - if (!existsSync(localPath)) { - mkdirSync(localPath); - } - - const localInfo = await utils.checkLocalPath(localPath, targetType.writeDir); - - if (localInfo.valid && !localInfo.type) { - mkdirSync(localInfo.path, { recursive: true }); - } - - if (!localInfo.valid) { - throw new Error(localInfo.msg); - } - const fileList = await this.list(remoteInfo.path); - for (const f of fileList) { - if (f.type === "d") { - const newSrc = remoteInfo.path + this.remotePathSep + f.name; - const newDst = join(localInfo.path, f.name); - await this.downloadDir(newSrc, newDst); - } else if (f.type === "-") { - const src = remoteInfo.path + this.remotePathSep + f.name; - const dst = join(localInfo.path, f.name); - await this.downloadFile(src, dst); - this.sshClient!.emit("download", { source: src, destination: dst }); - } else { - console.log(`downloadDir: File ignored: ${f.name} not regular file`); - } - } - return `${remoteInfo.path} downloaded to ${localInfo.path}`; - } - - /** - * Retrieves attributes for `path`. - */ - public async stat(remotePath: string): Promise { - utils.haveConnection(this, "stat"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.stat(remotePath, (err, stats) => { - if (err) { - reject(err); - } else { - resolve(stats); - } - }); - }); - } - - /** - * Sets the attributes defined in `attributes` for `path`. - */ - public async setstat(path: string, attributes: InputAttributes = {}): Promise { - utils.haveConnection(this, "setstat"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.setstat(path, attributes, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Removes the file/symlink at `path`. - */ - public async unlink(remotePath: string): Promise { - utils.haveConnection(this, "unlink"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.unlink(remotePath, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - // _rmdir - only works with an empty directory - async _rmdir(remotePath: string): Promise { - return new Promise(async (resolve, reject) => { - this.sftpWrapper!.rmdir(remotePath, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - public async rmdir(remotePath: string): Promise { - const files = await this.list(remotePath); - for (const file of files) { - const fullFilename = utils.joinRemote(this, remotePath, file.name); - if (file.type === "d") { - await this.rmdir(fullFilename); - } else { - await this.unlink(fullFilename); - } - } - await this._rmdir(remotePath); - } - - /** - * Creates a new directory `path`. - */ - public async mkdir(remotePath: string, attributes: InputAttributes = {}): Promise { - utils.haveConnection(this, "mkdir"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.mkdir(remotePath, attributes, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - public async exists(remotePath: string): Promise { - utils.haveConnection(this, "exists"); - try { - const stats = await this.stat(remotePath); - - if (stats.isDirectory()) { - return "d"; - } - if (stats.isSymbolicLink()) { - return "l"; - } - if (stats.isFile()) { - return "-"; - } - return false; - } catch (error) { - return false; - } - } - - /** - * Writes data to a file - */ - public async writeFile(remotePath: string, data: string | Buffer, options: WriteFileOptions = {}): Promise { - utils.haveConnection(this, "writeFile"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.writeFile(remotePath, data, options, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Sets the access time and modified time for `path`. - */ - public async utimes(path: string, atime: number | Date, mtime: number | Date): Promise { - utils.haveConnection(this, "utimes"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.utimes(path, atime, mtime, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Creates a symlink at `linkPath` to `targetPath`. - */ - public async symlink(targetPath: string, linkPath: string): Promise { - utils.haveConnection(this, "symlink"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.symlink(targetPath, linkPath, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Renames/moves `srcPath` to `destPath`. - */ - public async rename(srcPath: string, destPath: string): Promise { - utils.haveConnection(this, "rename"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.rename(srcPath, destPath, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Retrieves the target for a symlink at `path`. - */ - public async readlink(path: string): Promise { - utils.haveConnection(this, "readlink"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.readlink(path, (err, target) => { - if (err) { - reject(err); - } else { - resolve(target); - } - }); - }); - } - - /** - * Reads a file in memory and returns its contents - */ - public async readFile(remotePath: string): Promise { - utils.haveConnection(this, "readFile"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.readFile(remotePath, (err, handle) => { - if (err) { - reject(err); - } else { - resolve(handle); - } - }); - }); - } - - /** - * Retrieves attributes for `path`. If `path` is a symlink, the link itself is stat'ed - * instead of the resource it refers to. - */ - public async lstat(path: string): Promise { - utils.haveConnection(this, "lstat"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.lstat(path, (err, stats) => { - if (err) { - reject(err); - } else { - resolve(stats); - } - }); - }); - } - - /** - * Appends data to a file - */ - public async appendFile(remotePath: string, data: string | Buffer, options: WriteFileOptions): Promise { - utils.haveConnection(this, "appendFile"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.appendFile(remotePath, data, options, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Sets the mode for `path`. - */ - public async chmod(path: string, mode: number | string): Promise { - utils.haveConnection(this, "chmod"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.chmod(path, mode, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Sets the owner for `path`. - */ - public async chown(path: string, uid: number, gid: number): Promise { - utils.haveConnection(this, "chown"); - return new Promise((resolve, reject) => { - this.sftpWrapper!.chown(path, uid, gid, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - /** - * Close SSH connection - */ - public close() { - if (this.sshClient && this.sftpWrapper) { - this.sshClient.end(); - this.sshClient = null; - this.sftpWrapper = null; - } - - this.endCalled = true; - } - - /** - * List all files and directories at remotePath - */ - public async list(remotePath: string, pattern = /.*/): Promise { - const _list = (aPath: string, filter: RegExp | string) => { - return new Promise((resolve, reject) => { - const reg = /-/gi; - this.sftpWrapper!.readdir(aPath, (err, fileList) => { - if (err) { - reject(err); - } else { - let newList: any = []; - // reset file info - if (fileList) { - newList = fileList.map(item => { - return { - type: item.longname.substr(0, 1), - name: item.filename, - size: item.attrs.size, - modifyTime: item.attrs.mtime * 1000, - accessTime: item.attrs.atime * 1000, - rights: { - user: item.longname.substr(1, 3).replace(reg, ""), - group: item.longname.substr(4, 3).replace(reg, ""), - other: item.longname.substr(7, 3).replace(reg, ""), - }, - owner: item.attrs.uid, - group: item.attrs.gid, - }; - }); - } - // provide some compatibility for auxList - let regex: RegExp; - if (filter instanceof RegExp) { - regex = filter; - } else { - const newPattern = filter.replace(/\*([^*])*?/gi, ".*"); - regex = new RegExp(newPattern); - } - resolve(newList.filter((item: any) => regex.test(item.name))); - } - }); - }); - }; - - utils.haveConnection(this, "list"); - const pathInfo = await utils.checkRemotePath(this, remotePath, targetType.readDir); - if (!pathInfo.valid) { - throw new Error("Remote path is invalid"); - } - return _list(pathInfo.path, pattern); - } - - /** - * Resolves `path` to an absolute path. - */ - public realPath(remotePath: string): Promise { - return new Promise((resolve, reject) => { - const closeListener = utils.makeCloseListener(this, reject, "realPath"); - this.sshClient!.prependListener("close", closeListener); - const errorListener = utils.makeErrorListener(reject, this, "realPath"); - this.sshClient!.prependListener("error", errorListener); - if (utils.haveConnection(this, "realPath", reject)) { - this.sftpWrapper!.realpath(remotePath, (err, absPath) => { - if (err) { - reject(utils.formatError(`${err.message} ${remotePath}`, "realPath")); - } - resolve(absPath); - this.removeListener("error", errorListener); - this.removeListener("close", closeListener); - }); - } - }); - } -} - -export async function Client(options: TScpOptions): Promise { - const client = new ScpClient(options); - - return new Promise((resolve, reject) => { - client.on("ready", () => { - resolve(client); - }); - - client.on("error", err => { - reject(err); - }); - - client.on("close", () => { - client.removeAllListeners(); - }); - - for (const event in options.events) { - client.on(event, (...args) => { - (options.events![event as keyof ClientEvents] as (...args: any[]) => void)(...args); - }); - } - }); -} - -export default Client; diff --git a/packages/plugins/plugin-lib/src/scp/types.ts b/packages/plugins/plugin-lib/src/scp/types.ts deleted file mode 100644 index bbc69494..00000000 --- a/packages/plugins/plugin-lib/src/scp/types.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { - AcceptConnection, - ChangePasswordCallback, - ClientChannel, - ClientErrorExtensions, - KeyboardInteractiveCallback, - NegotiatedAlgorithms, - ParsedKey, - Prompt, - RejectConnection, - TcpConnectionDetails, - UNIXConnectionDetails, - X11Details, -} from "ssh2"; - -export class ErrorCustom extends Error { - custom?: boolean; - code?: string; - level?: string; - hostname?: string; - address?: string; -} -export interface CheckResult { - path: string; - type?: string; - valid?: boolean; - msg?: string; - code?: string; -} - -export interface ClientEvents { - /** - * Emitted when a notice was sent by the server upon connection. - */ - banner?: (message: string) => void; - - /** - * Emitted when authentication was successful. - */ - ready?: () => void; - - /** - * Emitted when an incoming forwarded TCP connection is being requested. - * - * Calling `accept()` accepts the connection and returns a `Channel` object. - * Calling `reject()` rejects the connection and no further action is needed. - */ - "tcp connection"?: (details: TcpConnectionDetails, accept: AcceptConnection, reject: RejectConnection) => void; - - /** - * Emitted when an incoming X11 connection is being requested. - * - * Calling `accept()` accepts the connection and returns a `Channel` object. - * Calling `reject()` rejects the connection and no further action is needed. - */ - x11?: (details: X11Details, accept: AcceptConnection, reject: RejectConnection) => void; - - /** - * Emitted when the server is asking for replies to the given `prompts` for keyboard- - * interactive user authentication. - * - * * `name` is generally what you'd use as a window title (for GUI apps). - * * `prompts` is an array of `Prompt` objects. - * - * The answers for all prompts must be provided as an array of strings and passed to - * `finish` when you are ready to continue. - * - * NOTE: It's possible for the server to come back and ask more questions. - */ - "keyboard-interactive"?: (name: string, instructions: string, lang: string, prompts: Prompt[], finish: KeyboardInteractiveCallback) => void; - - /** - * Emitted when the server has requested that the user's password be changed, if using - * password-based user authentication. - * - * Call `done` with the new password. - */ - "change password"?: (message: string, done: ChangePasswordCallback) => void; - - /** - * Emitted when an error occurred. - */ - error?: (err: Error & ClientErrorExtensions) => void; - - /** - * Emitted when the socket was disconnected. - */ - end?: () => void; - - /** - * Emitted when the socket was closed. - */ - close?: () => void; - - /** - * Emitted when the socket has timed out. - */ - timeout?: () => void; - - /** - * Emitted when the socket has connected. - */ - connect?: () => void; - - /** - * Emitted when the server responds with a greeting message. - */ - greeting?: (greeting: string) => void; - - /** - * Emitted when a handshake has completed (either initial or rekey). - */ - handshake?: (negotiated: NegotiatedAlgorithms) => void; - - /** - * Emitted when the server announces its available host keys. - */ - hostkeys?: (keys: ParsedKey[]) => void; - - /** - * An incoming forwarded UNIX socket connection is being requested. - */ - "unix connection"?: (info: UNIXConnectionDetails, accept: AcceptConnection, reject: RejectConnection) => void; -} diff --git a/packages/plugins/plugin-lib/src/scp/utils.ts b/packages/plugins/plugin-lib/src/scp/utils.ts deleted file mode 100644 index 6e54f4bc..00000000 --- a/packages/plugins/plugin-lib/src/scp/utils.ts +++ /dev/null @@ -1,639 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { errorCode, targetType } from "./constant.js"; -import { EventEmitter } from "events"; -import { ScpClient } from "."; -import { CheckResult, ErrorCustom } from "./types.js"; - -/** - * Generate a new Error object with a reformatted error message which - * is a little more informative and useful to users. - * - * @param {Error|string} err - The Error object the new error will be based on - * @param {number} retryCount - For those functions which use retry. Number of - * attempts to complete before giving up - * @returns {Error} New error with custom error message - */ -export function formatError(err: ErrorCustom | string, name = "sftp", eCode = errorCode.generic, retryCount?: number) { - let msg = ""; - let code = ""; - const retry = retryCount ? ` after ${retryCount} ${retryCount > 1 ? "attempts" : "attempt"}` : ""; - - if (err === undefined) { - msg = `${name}: Undefined error - probably a bug!`; - code = errorCode.generic; - } else if (typeof err === "string") { - msg = `${name}: ${err}${retry}`; - code = eCode; - } else if (err.custom) { - msg = `${name}->${err.message}${retry}`; - code = err.code!; - } else { - switch (err.code) { - case "ENOTFOUND": - msg = `${name}: ${err.level} error. ` + `Address lookup failed for host ${err.hostname}${retry}`; - break; - case "ECONNREFUSED": - msg = `${name}: ${err.level} error. Remote host at ` + `${err.address} refused connection${retry}`; - break; - case "ECONNRESET": - msg = `${name}: Remote host has reset the connection: ` + `${err.message}${retry}`; - break; - case "ENOENT": - msg = `${name}: ${err.message}${retry}`; - break; - default: - msg = `${name}: ${err.message}${retry}`; - } - code = err.code ? err.code : eCode; - } - const newError = new ErrorCustom(msg); - newError.code = code; - newError.custom = true; - return newError; -} - -/** - * Tests an error to see if it is one which has already been customised - * by this module or not. If not, applies appropriate customisation. - * - * @param {Error} err - an Error object - * @param {String} name - name to be used in customised error message - * @param {Function} reject - If defined, call this function instead of - * throwing the error - * @throws {Error} - */ -export function handleError(err: ErrorCustom, name: string, reject: (e: any) => void) { - if (reject) { - if (err.custom) { - reject(err); - } else { - reject(formatError(err, name, undefined, undefined)); - } - } else { - if (err.custom) { - throw err; - } else { - throw formatError(err, name, undefined, undefined); - } - } -} - -/** - * Remove all ready, error and end listeners. - * - * @param {Emitter} emitter - The emitter object to remove listeners from - */ -// function removeListeners(emitter) { -// const listeners = emitter.eventNames() -// listeners.forEach((name) => { -// emitter.removeAllListeners(name) -// }) -// } - -/** - * Simple default error listener. Will reformat the error message and - * throw a new error. - * - * @param {Error} err - source for defining new error - * @throws {Error} Throws new error - */ -export function makeErrorListener(reject: (e: any) => void, client: ScpClient, name: string) { - return (err: Error) => { - client.errorHandled = true; - reject(formatError(err, name)); - }; -} - -export function makeEndListener(client: ScpClient) { - return () => { - if (!client.endCalled) { - console.error("End Listener: Connection ended unexpectedly"); - } - }; -} - -export function makeCloseListener(client: ScpClient, reject?: (e: any) => void, name?: string) { - return () => { - if (!client.endCalled) { - if (reject) { - reject(formatError("Connection closed unexpectedly", name)); - } else { - console.error("Connection closed unexpectedly"); - } - } - client.sftpWrapper = null; - }; -} - -/** - * @async - * - * Tests to see if a path identifies an existing item. Returns either - * 'd' = directory, 'l' = sym link or '-' regular file if item exists. Returns - * false if it does not - * - * @param {String} localPath - * @returns {Boolean | String} - */ -export function localExists(localPath: string): Promise { - return new Promise((resolve, reject) => { - fs.stat(localPath, (err, stats) => { - if (err) { - if (err.code === "ENOENT") { - resolve("ENOENT"); - } else { - reject(err); - } - } else { - if (stats.isDirectory()) { - resolve("d"); - } else if (stats.isSymbolicLink()) { - resolve("l"); - } else if (stats.isFile()) { - resolve("-"); - } else { - resolve(""); - } - } - }); - }); -} - -/** - * Used by checkRemotePath and checkLocalPath to help ensure consistent - * error messages. - * - * @param {Error} err - original error - * @param {String} testPath - path associated with the error - * @returns {Object} with properties of 'msg' and 'code'. - */ -export function classifyError(err: ErrorCustom, testPath: string) { - switch (err.code) { - case "EACCES": - return { - msg: `Permission denied: ${testPath}`, - code: errorCode.permission, - }; - case "ENOENT": - return { - msg: `No such file: ${testPath}`, - code: errorCode.notexist, - }; - case "ENOTDIR": - return { - msg: `Not a directory: ${testPath}`, - code: errorCode.notdir, - }; - default: - return { - msg: err.message, - code: err.code ? err.code : errorCode.generic, - }; - } -} - -export function localAccess(localPath: string, mode: number): Promise { - return new Promise(resolve => { - fs.access(localPath, mode, err => { - if (err) { - const { msg, code } = classifyError(err, localPath); - resolve({ - path: localPath, - valid: false, - msg, - code, - }); - } else { - resolve({ - path: localPath, - valid: true, - }); - } - }); - }); -} - -export async function checkLocalReadFile(localPath: string, localType: string) { - try { - const rslt: CheckResult = { - path: localPath, - type: localType, - }; - if (localType === "d") { - rslt.valid = false; - rslt.msg = `Bad path: ${localPath} must be a file`; - rslt.code = errorCode.badPath; - return rslt; - } else { - const access = await localAccess(localPath, fs.constants.R_OK); - if (access.valid) { - rslt.valid = true; - return rslt; - } else { - rslt.valid = false; - rslt.msg = access.msg; - rslt.code = access.code; - return rslt; - } - } - } catch (err) { - throw formatError(err as ErrorCustom, "checkLocalReadFile"); - } -} - -export async function checkLocalReadDir(localPath: string, localType: string) { - try { - const rslt: CheckResult = { - path: localPath, - type: localType, - }; - if (!localType) { - rslt.valid = false; - rslt.msg = `No such directory: ${localPath}`; - rslt.code = errorCode.notdir; - return rslt; - } else if (localType !== "d") { - rslt.valid = false; - rslt.msg = `Bad path: ${localPath} must be a directory`; - rslt.code = errorCode.badPath; - return rslt; - } else { - const access = await localAccess(localPath, fs.constants.R_OK | fs.constants.X_OK); - if (!access.valid) { - rslt.valid = false; - rslt.msg = access.msg; - rslt.code = access.code; - return rslt; - } - rslt.valid = true; - return rslt; - } - } catch (err) { - throw formatError(err as ErrorCustom, "checkLocalReadDir"); - } -} - -export async function checkLocalWriteFile(localPath: string, localType: string) { - try { - const rslt: CheckResult = { - path: localPath, - type: localType, - }; - if (localType === "d") { - rslt.valid = false; - rslt.msg = `Bad path: ${localPath} must be a file`; - rslt.code = errorCode.badPath; - return rslt; - } else if (!localType) { - const dir = path.parse(localPath).dir; - const parent = await localAccess(dir, fs.constants.W_OK); - if (parent.valid) { - rslt.valid = true; - return rslt; - } else { - rslt.valid = false; - rslt.msg = parent.msg; - rslt.code = parent.code; - return rslt; - } - } else { - const access = await localAccess(localPath, fs.constants.W_OK); - if (access.valid) { - rslt.valid = true; - return rslt; - } else { - rslt.valid = false; - rslt.msg = access.msg; - rslt.code = access.code; - return rslt; - } - } - } catch (err) { - throw formatError(err as ErrorCustom, "checkLocalWriteFile"); - } -} - -export async function checkLocalWriteDir(localPath: string, localType: string) { - try { - const rslt: CheckResult = { - path: localPath, - type: localType, - }; - if (!localType) { - const parent = path.parse(localPath).dir; - const access = await localAccess(parent, fs.constants.W_OK); - if (access.valid) { - rslt.valid = true; - return rslt; - } else { - rslt.valid = false; - rslt.msg = access.msg; - rslt.code = access.code; - return rslt; - } - } else if (localType !== "d") { - rslt.valid = false; - rslt.msg = `Bad path: ${localPath} must be a directory`; - rslt.code = errorCode.badPath; - return rslt; - } else { - const access = await localAccess(localPath, fs.constants.W_OK); - if (access.valid) { - rslt.valid = true; - return rslt; - } else { - rslt.valid = false; - rslt.msg = access.msg; - rslt.code = access.code; - return rslt; - } - } - } catch (err) { - throw formatError(err as ErrorCustom, "checkLocalWriteDir"); - } -} - -export async function checkLocalPath(lPath: string, target = targetType.readFile) { - const localPath = path.resolve(lPath); - const type = await localExists(localPath); - switch (target) { - case targetType.readFile: - return checkLocalReadFile(localPath, type); - case targetType.readDir: - return checkLocalReadDir(localPath, type); - case targetType.writeFile: - return checkLocalWriteFile(localPath, type); - case targetType.writeDir: - return checkLocalWriteDir(localPath, type); - default: - return { - path: localPath, - type, - valid: true, - }; - } -} - -export async function normalizeRemotePath(client: ScpClient, aPath: string) { - try { - if (aPath.startsWith("..")) { - const root = await client.realPath(".."); - return root + client.remotePathSep + aPath.substring(3); - } else if (aPath.startsWith(".")) { - const root = await client.realPath("."); - return root + client.remotePathSep + aPath.substring(2); - } - return aPath; - } catch (err) { - throw formatError(err as ErrorCustom, "normalizeRemotePath"); - } -} - -export function checkReadObject(aPath: string, type: string) { - return { - path: aPath, - type, - valid: type ? true : false, - msg: type ? undefined : `No such file ${aPath}`, - code: type ? undefined : errorCode.notexist, - }; -} - -export function checkReadFile(aPath: string, type: string) { - if (!type) { - return { - path: aPath, - type, - valid: false, - msg: `No such file: ${aPath}`, - code: errorCode.notexist, - }; - } else if (type === "d") { - return { - path: aPath, - type, - valid: false, - msg: `Bad path: ${aPath} must be a file`, - code: errorCode.badPath, - }; - } - return { - path: aPath, - type, - valid: true, - }; -} - -export function checkReadDir(aPath: string, type: string) { - if (!type) { - return { - path: aPath, - type, - valid: false, - msg: `No such directory: ${aPath}`, - code: errorCode.notdir, - }; - } else if (type !== "d") { - return { - path: aPath, - type, - valid: false, - msg: `Bad path: ${aPath} must be a directory`, - code: errorCode.badPath, - }; - } - return { - path: aPath, - type, - valid: true, - }; -} - -export async function checkWriteFile(client: ScpClient, aPath: string, type: string) { - if (type && type === "d") { - return { - path: aPath, - type, - valid: false, - msg: `Bad path: ${aPath} must be a regular file`, - code: errorCode.badPath, - }; - } else if (!type) { - const { root, dir } = path.parse(aPath); - // let parentDir = path.parse(aPath).dir; - if (!dir) { - return { - path: aPath, - type: false, - valid: false, - msg: `Bad path: ${aPath} cannot determine parent directory`, - code: errorCode.badPath, - }; - } - if (root === dir) { - return { - path: aPath, - type, - valid: true, - }; - } - const parentType = await client.exists(dir); - if (!parentType) { - return { - path: aPath, - type, - valid: false, - msg: `Bad path: ${dir} parent not exist`, - code: errorCode.badPath, - }; - } else if (parentType !== "d") { - return { - path: aPath, - type, - valid: false, - msg: `Bad path: ${dir} must be a directory`, - code: errorCode.badPath, - }; - } - return { - path: aPath, - type, - valid: true, - }; - } - return { - path: aPath, - type, - valid: true, - }; -} - -export async function checkWriteDir(client: ScpClient, aPath: string, type: string) { - if (type && type !== "d") { - return { - path: aPath, - type, - valid: false, - msg: `Bad path: ${aPath} must be a directory`, - code: errorCode.badPath, - }; - } else if (!type) { - const { root, dir } = path.parse(aPath); - if (root === dir) { - return { - path: aPath, - type, - valid: true, - }; - } - if (!dir) { - return { - path: aPath, - type: false, - valid: false, - msg: `Bad path: ${aPath} cannot determine directory parent`, - code: errorCode.badPath, - }; - } - const parentType = await client.exists(dir); - if (parentType && parentType !== "d") { - return { - path: aPath, - type, - valid: false, - msg: "Bad path: Parent Directory must be a directory", - code: errorCode.badPath, - }; - } - } - // don't care if parent does not exist as it might be created - // via recursive call to mkdir. - return { - path: aPath, - type, - valid: true, - }; -} - -export function checkWriteObject(aPath: string, type: string) { - // for writeObj, not much error checking we can do - // Just return path, type and valid indicator - return { - path: aPath, - type, - valid: true, - }; -} - -export async function checkRemotePath(client: ScpClient, rPath: string, target = targetType.readFile) { - const aPath = await normalizeRemotePath(client, rPath); - const type = await client.exists(aPath); - switch (target) { - case targetType.readObj: - return checkReadObject(aPath, type as string); - case targetType.readFile: - return checkReadFile(aPath, type as string); - case targetType.readDir: - return checkReadDir(aPath, type as string); - case targetType.writeFile: - return checkWriteFile(client, aPath, type as string); - case targetType.writeDir: - return checkWriteDir(client, aPath, type as string); - case targetType.writeObj: - return checkWriteObject(aPath, type as string); - default: - throw formatError(`Unknown target type: ${target}`, "checkRemotePath", errorCode.generic); - } -} - -/** - * Check to see if there is an active sftp connection - * - * @param {Object} client - current sftp object - * @param {String} name - name given to this connection - * @param {Function} reject - if defined, call this rather than throw - * an error - * @returns {Boolean} True if connection OK - * @throws {Error} - */ -export function haveConnection(client: ScpClient, name: string, reject?: (e: any) => void) { - if (!client.sftpWrapper) { - const newError = formatError("No SFTP connection available", name, errorCode.connect); - if (reject) { - reject(newError); - return false; - } else { - throw newError; - } - } - return true; -} - -export function dumpListeners(emitter: EventEmitter) { - const eventNames = emitter.eventNames(); - if (eventNames.length) { - console.log("Listener Data"); - eventNames.map((n: any) => { - const listeners = emitter.listeners(n); - console.log(`${n}: ${emitter.listenerCount(n)}`); - console.dir(listeners); - listeners.map(l => { - console.log(`listener name = ${l.name}`); - }); - }); - } -} - -export function hasListener(emitter: EventEmitter, eventName: string, listenerName: string) { - const listeners = emitter.listeners(eventName); - const matches = listeners.filter(l => l.name === listenerName); - return matches.length === 0 ? false : true; -} - -export function joinRemote(client: ScpClient, ...args: string[]) { - if (client.remotePathSep === path.win32.sep) { - return path.win32.join(...args); - } - return path.posix.join(...args); -} diff --git a/packages/plugins/plugin-lib/src/ssh/ssh.ts b/packages/plugins/plugin-lib/src/ssh/ssh.ts index 338c08a1..dc956849 100644 --- a/packages/plugins/plugin-lib/src/ssh/ssh.ts +++ b/packages/plugins/plugin-lib/src/ssh/ssh.ts @@ -7,8 +7,8 @@ import { SshAccess } from "./ssh-access.js"; import stripAnsi from "strip-ansi"; import { SocksClient } from "socks"; import { SocksProxy, SocksProxyType } from "socks/typings/common/constants.js"; -import { ScpClient } from "../scp/index.js"; import fs from "fs"; + export type TransportItem = { localPath: string; remotePath: string }; export class AsyncSsh2Client { @@ -313,7 +313,7 @@ export class SshClient { } async scpUpload(options: { conn: any; localPath: string; remotePath: string; opts?: { mode?: string } }) { - const { conn, localPath, remotePath, opts } = options; + const { conn, localPath, remotePath } = options; return new Promise((resolve, reject) => { // 关键步骤:构造 SCP 命令 try {