diff --git a/README.md b/README.md index 5fae0d0..889a4a8 100644 --- a/README.md +++ b/README.md @@ -1296,6 +1296,36 @@ async function test() { test() ``` +## Events +You can listen for any event [provided by SSH2](https://github.com/mscdex/ssh2#client) during an SSH session by passing `events` to `Connection options` + +For example: +```ts +try { + const client = await Client({ + host: "server_ip", + port: 22, + username: "username", + tryKeyboard: true, + events: { + "keyboard-interactive": ( + name, + instructions, + instructionsLang, + prompts, + finish + ) => { + finish(['my_password']) + }, + }, + }); + + client.close(); // remember to close connection after you finish +} catch (e) { + console.log(e); +} +``` + ## Connection options Below are available options you can pass when connecting to server: * **agent** - _string_ - Path to ssh-agent's UNIX socket for ssh-agent-based user authentication. **Windows users: set to 'pageant' for authenticating with Pageant or (actual) path to a cygwin "UNIX socket."** **Default:** (none) @@ -1485,5 +1515,7 @@ Below are available options you can pass when connecting to server: * **tryKeyboard** - _boolean_ - Try keyboard-interactive user authentication if primary user authentication method fails. If you set this to `true`, you need to handle the `keyboard-interactive` event. **Default:** `false` * **username** - _string_ - Username for authentication. **Default:** (none) + +* **events** - Object - List of events to listen to. **Default:** (none) # Support If you like this project, give me 1 ⭐️ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 34555a8..c7a5833 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,21 @@ { "name": "node-scp", - "version": "0.0.21", + "version": "0.0.22", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-scp", - "version": "0.0.18", + "version": "0.0.22", "license": "MIT", "dependencies": { - "ssh2": "^1.11.0" + "ssh2": "^1.14.0" }, "devDependencies": { "@commitlint/cli": "^12.1.4", "@commitlint/config-conventional": "^12.1.4", "@types/node": "^16.0.1", - "@types/ssh2": "^1.11.5", + "@types/ssh2": "^1.11.13", "husky": "^7.0.1", "standard-version": "^9.3.0", "typescript": "^4.3.5" @@ -430,14 +430,20 @@ "dev": true }, "node_modules/@types/ssh2": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.5.tgz", - "integrity": "sha512-RaBsPKr+YP/slH8iR7XfC7chyomU+V57F/gJ5cMSP2n6/YWKVmeRLx7lrkgw4YYLpEW5lXLAdfZJqGo0PXboSA==", + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.13.tgz", + "integrity": "sha512-08WbG68HvQ2YVi74n2iSUnYHYpUdFc/s2IsI0BHBdJwaqYJpWlVv9elL0tYShTv60yr0ObdxJR5NrCRiGJ/0CQ==", "dev": true, "dependencies": { - "@types/node": "*" + "@types/node": "^18.11.18" } }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.17.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.2.tgz", + "integrity": "sha512-wBo3KqP/PBqje5TI9UTiuL3yWfP6sdPtjtygSOqcYZWT232dfDeDOnkDps5wqZBP9NgGgYrNejinl0faAuE+HQ==", + "dev": true + }, "node_modules/add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -484,9 +490,9 @@ } }, "node_modules/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dependencies": { "safer-buffer": "~2.1.0" } @@ -531,9 +537,9 @@ "dev": true }, "node_modules/buildcheck": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.3.tgz", - "integrity": "sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", "optional": true, "engines": { "node": ">=10.0.0" @@ -1077,14 +1083,14 @@ } }, "node_modules/cpu-features": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.4.tgz", - "integrity": "sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.8.tgz", + "integrity": "sha512-BbHBvtYhUhksqTjr6bhNOjGgMnhwhGTQmOoZGD+K7BCaQDCuZl/Ve1ZxUSMRwVC4D/rkCPQ2MAIeYzrWyK7eEg==", "hasInstallScript": true, "optional": true, "dependencies": { - "buildcheck": "0.0.3", - "nan": "^2.15.0" + "buildcheck": "~0.0.6", + "nan": "^2.17.0" }, "engines": { "node": ">=10.0.0" @@ -1974,9 +1980,9 @@ } }, "node_modules/nan": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", - "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", "optional": true }, "node_modules/neo-async": { @@ -2383,20 +2389,20 @@ } }, "node_modules/ssh2": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.11.0.tgz", - "integrity": "sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz", + "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==", "hasInstallScript": true, "dependencies": { - "asn1": "^0.2.4", + "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "engines": { "node": ">=10.16.0" }, "optionalDependencies": { - "cpu-features": "~0.0.4", - "nan": "^2.16.0" + "cpu-features": "~0.0.8", + "nan": "^2.17.0" } }, "node_modules/standard-version": { @@ -3166,12 +3172,20 @@ "dev": true }, "@types/ssh2": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.5.tgz", - "integrity": "sha512-RaBsPKr+YP/slH8iR7XfC7chyomU+V57F/gJ5cMSP2n6/YWKVmeRLx7lrkgw4YYLpEW5lXLAdfZJqGo0PXboSA==", + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.13.tgz", + "integrity": "sha512-08WbG68HvQ2YVi74n2iSUnYHYpUdFc/s2IsI0BHBdJwaqYJpWlVv9elL0tYShTv60yr0ObdxJR5NrCRiGJ/0CQ==", "dev": true, "requires": { - "@types/node": "*" + "@types/node": "^18.11.18" + }, + "dependencies": { + "@types/node": { + "version": "18.17.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.2.tgz", + "integrity": "sha512-wBo3KqP/PBqje5TI9UTiuL3yWfP6sdPtjtygSOqcYZWT232dfDeDOnkDps5wqZBP9NgGgYrNejinl0faAuE+HQ==", + "dev": true + } } }, "add-stream": { @@ -3208,9 +3222,9 @@ "dev": true }, "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "requires": { "safer-buffer": "~2.1.0" } @@ -3252,9 +3266,9 @@ "dev": true }, "buildcheck": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.3.tgz", - "integrity": "sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", "optional": true }, "callsites": { @@ -3681,13 +3695,13 @@ } }, "cpu-features": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.4.tgz", - "integrity": "sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.8.tgz", + "integrity": "sha512-BbHBvtYhUhksqTjr6bhNOjGgMnhwhGTQmOoZGD+K7BCaQDCuZl/Ve1ZxUSMRwVC4D/rkCPQ2MAIeYzrWyK7eEg==", "optional": true, "requires": { - "buildcheck": "0.0.3", - "nan": "^2.15.0" + "buildcheck": "~0.0.6", + "nan": "^2.17.0" } }, "dargs": { @@ -4371,9 +4385,9 @@ "dev": true }, "nan": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", - "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", "optional": true }, "neo-async": { @@ -4679,14 +4693,14 @@ } }, "ssh2": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.11.0.tgz", - "integrity": "sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz", + "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==", "requires": { - "asn1": "^0.2.4", + "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2", - "cpu-features": "~0.0.4", - "nan": "^2.16.0" + "cpu-features": "~0.0.8", + "nan": "^2.17.0" } }, "standard-version": { diff --git a/package.json b/package.json index 9c5e774..348f01e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "node-scp", "title": "SCP module for NodeJS", "description": "Lightweight, fast and secure SCP function for NodeJS", - "version": "0.0.22", + "version": "0.0.23", "main": "lib/index.js", "types": "lib/index.d.ts", "license": "MIT", @@ -35,13 +35,13 @@ "release": "standard-version" }, "dependencies": { - "ssh2": "^1.11.0" + "ssh2": "^1.14.0" }, "devDependencies": { "@commitlint/cli": "^12.1.4", "@commitlint/config-conventional": "^12.1.4", "@types/node": "^16.0.1", - "@types/ssh2": "^1.11.5", + "@types/ssh2": "^1.11.13", "husky": "^7.0.1", "standard-version": "^9.3.0", "typescript": "^4.3.5" diff --git a/src/constant.ts b/src/constant.ts index b455f5c..89b7cce 100644 --- a/src/constant.ts +++ b/src/constant.ts @@ -1,10 +1,10 @@ - export enum errorCode { - generic = 'ERR_GENERIC_CLIENT', - connect = 'ERR_NOT_CONNECTED', - badPath = 'ERR_BAD_PATH', - permission = 'EACCES', - notexist = 'ENOENT', - notdir = 'ENOTDIR' +export enum errorCode { + generic = "ERR_GENERIC_CLIENT", + connect = "ERR_NOT_CONNECTED", + badPath = "ERR_BAD_PATH", + permission = "EACCES", + notexist = "ENOENT", + notdir = "ENOTDIR", } export enum targetType { @@ -13,5 +13,23 @@ export enum targetType { writeDir = 3, readDir = 4, readObj = 5, - writeObj = 6 -} \ No newline at end of file + 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/src/index.ts b/src/index.ts index d03c720..c91cd5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,238 +1,284 @@ -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 } from 'ssh2' -import { targetType } from './constant' -import * as utils from './utils' +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"; +import * as utils from "./utils"; +import { ClientEvents } from "./types"; export type TScpOptions = ConnectConfig & { - remoteOsType?: 'posix' | 'win32', -} + 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 + 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') - }) - ssh.on('ready', () => { - ssh.sftp((err, sftp) => { - if (err) { throw err } - // save for reuse - this.sftpWrapper = sftp - this.emit('ready') + 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"); + }); }) - }) - ssh.on('error', (err) => { - this.emit('error', err) - }) - ssh.on('end', () => { - this.emit('end') - }) - ssh.on('close', () => { - if (!this.endCalled) { - this.sftpWrapper = null - } - this.emit('close') - }) - ssh.on('keyboard-interactive', (name, instructions, instructionsLang, prompts, finish) => { - this.emit('keyboard-interactive', name, instructions, instructionsLang, prompts, finish) - }) - ssh.on('change password', (message, done) => { - this.emit('change password', message, done) - }) - ssh.on('tcp connection', (details, accept, reject) => { - this.emit('tcp connection', details, accept, reject) - }) - - ssh.connect(options) - this.sshClient = ssh - - if (options.remoteOsType === 'win32') { - this.remotePathSep = win32.sep + .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') + 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) + reject(err); } else { - resolve() + 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') + 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) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** * Clean a directory in remote server */ public async emptyDir(dir: string): Promise { - utils.haveConnection(this, 'uploadDir') + utils.haveConnection(this, "uploadDir"); try { - const isExist = await this.exists(dir) + const isExist = await this.exists(dir); if (!isExist) { - await this.mkdir(dir) - } else if (isExist === 'd') { - await this.rmdir(dir) - await this.mkdir(dir) + await this.mkdir(dir); + } else if (isExist === "d") { + await this.rmdir(dir); + await this.mkdir(dir); } - } - catch (error) { - throw error + } catch (error) { + throw error; } } public async uploadDir(src: string, dest: string): Promise { - utils.haveConnection(this, 'uploadDir') + utils.haveConnection(this, "uploadDir"); try { - const isExist = await this.exists(dest) + const isExist = await this.exists(dest); if (!isExist) { - await this.mkdir(dest) + await this.mkdir(dest); } const dirEntries = readdirSync(src, { - encoding: 'utf8', + 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) + 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) + 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 + throw error; } } public async downloadDir(remotePath: string, localPath: string) { - utils.haveConnection(this, 'downloadDir') + utils.haveConnection(this, "downloadDir"); const remoteInfo: any = await utils.checkRemotePath( this, remotePath, targetType.readDir - ) + ); if (!remoteInfo.valid) { - throw new Error(remoteInfo.msg) + throw new Error(remoteInfo.msg); } if (!existsSync(localPath)) { - mkdirSync(localPath) + mkdirSync(localPath); } - const localInfo = await utils.checkLocalPath(localPath, targetType.writeDir) + const localInfo = await utils.checkLocalPath( + localPath, + targetType.writeDir + ); if (localInfo.valid && !localInfo.type) { - mkdirSync(localInfo.path, { recursive: true }) + mkdirSync(localInfo.path, { recursive: true }); } if (!localInfo.valid) { - throw new Error(localInfo.msg) + throw new Error(localInfo.msg); } - const fileList = await this.list(remoteInfo.path) + 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 }) + 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`) + console.log(`downloadDir: File ignored: ${f.name} not regular file`); } } - return `${remoteInfo.path} downloaded to ${localInfo.path}` + return `${remoteInfo.path} downloaded to ${localInfo.path}`; } /** * Retrieves attributes for `path`. */ public async stat(remotePath: string): Promise { - utils.haveConnection(this, 'stat') + utils.haveConnection(this, "stat"); return new Promise((resolve, reject) => { this.sftpWrapper!.stat(remotePath, (err, stats) => { if (err) { - reject(err) + reject(err); } else { - resolve(stats) + resolve(stats); } - }) - }) + }); + }); } /** * Sets the attributes defined in `attributes` for `path`. */ - public async setstat(path: string, attributes: InputAttributes = {}): Promise { - utils.haveConnection(this, 'setstat') + 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) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** * Removes the file/symlink at `path`. */ public async unlink(remotePath: string): Promise { - utils.haveConnection(this, 'unlink') + utils.haveConnection(this, "unlink"); return new Promise((resolve, reject) => { this.sftpWrapper!.unlink(remotePath, (err) => { if (err) { - reject(err) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } // _rmdir - only works with an empty directory @@ -240,157 +286,168 @@ export class ScpClient extends EventEmitter { return new Promise(async (resolve, reject) => { this.sftpWrapper!.rmdir(remotePath, (err) => { if (err) { - reject(err) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } public async rmdir(remotePath: string): Promise { - const files = await this.list(remotePath) + 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) + const fullFilename = utils.joinRemote(this, remotePath, file.name); + if (file.type === "d") { + await this.rmdir(fullFilename); } else { - await this.unlink(fullFilename) + await this.unlink(fullFilename); } } - await this._rmdir(remotePath) + await this._rmdir(remotePath); } /** * Creates a new directory `path`. */ - public async mkdir(remotePath: string, attributes: InputAttributes = {}): Promise { - utils.haveConnection(this, 'mkdir') + 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) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } public async exists(remotePath: string): Promise { - utils.haveConnection(this, 'exists') + utils.haveConnection(this, "exists"); try { - const stats = await this.stat(remotePath) + const stats = await this.stat(remotePath); if (stats.isDirectory()) { - return 'd' + return "d"; } if (stats.isSymbolicLink()) { - return 'l' + return "l"; } if (stats.isFile()) { - return '-' + return "-"; } - return false + return false; } catch (error) { - return false + return false; } } /** * Writes data to a file */ - public async writeFile(remotePath: string, data: string | Buffer, options: WriteFileOptions = {}): Promise { - utils.haveConnection(this, 'writeFile') + 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) + reject(err); } else { - resolve() + 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') + 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) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** * Creates a symlink at `linkPath` to `targetPath`. */ public async symlink(targetPath: string, linkPath: string): Promise { - utils.haveConnection(this, 'symlink') + utils.haveConnection(this, "symlink"); return new Promise((resolve, reject) => { this.sftpWrapper!.symlink(targetPath, linkPath, (err) => { if (err) { - reject(err) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** * Renames/moves `srcPath` to `destPath`. */ public async rename(srcPath: string, destPath: string): Promise { - utils.haveConnection(this, 'rename') + utils.haveConnection(this, "rename"); return new Promise((resolve, reject) => { this.sftpWrapper!.rename(srcPath, destPath, (err) => { if (err) { - reject(err) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** * Retrieves the target for a symlink at `path`. */ public async readlink(path: string): Promise { - utils.haveConnection(this, 'readlink') + utils.haveConnection(this, "readlink"); return new Promise((resolve, reject) => { this.sftpWrapper!.readlink(path, (err, target) => { if (err) { - reject(err) + reject(err); } else { - resolve(target) + resolve(target); } - }) - }) + }); + }); } /** * Reads a file in memory and returns its contents */ public async readFile(remotePath: string): Promise { - utils.haveConnection(this, 'readFile') + utils.haveConnection(this, "readFile"); return new Promise((resolve, reject) => { this.sftpWrapper!.readFile(remotePath, (err, handle) => { if (err) { - reject(err) + reject(err); } else { - resolve(handle) + resolve(handle); } - }) - }) + }); + }); } /** @@ -398,64 +455,68 @@ export class ScpClient extends EventEmitter { * instead of the resource it refers to. */ public async lstat(path: string): Promise { - utils.haveConnection(this, 'lstat') + utils.haveConnection(this, "lstat"); return new Promise((resolve, reject) => { this.sftpWrapper!.lstat(path, (err, stats) => { if (err) { - reject(err) + reject(err); } else { - resolve(stats) + resolve(stats); } - }) - }) + }); + }); } /** * Appends data to a file */ - public async appendFile(remotePath: string, data: string | Buffer, options: WriteFileOptions): Promise { - utils.haveConnection(this, 'appendFile') + 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) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** * Sets the mode for `path`. */ public async chmod(path: string, mode: number | string): Promise { - utils.haveConnection(this, 'chmod') + utils.haveConnection(this, "chmod"); return new Promise((resolve, reject) => { this.sftpWrapper!.chmod(path, mode, (err) => { if (err) { - reject(err) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** * Sets the owner for `path`. */ public async chown(path: string, uid: number, gid: number): Promise { - utils.haveConnection(this, 'chown') + utils.haveConnection(this, "chown"); return new Promise((resolve, reject) => { this.sftpWrapper!.chown(path, uid, gid, (err) => { if (err) { - reject(err) + reject(err); } else { - resolve() + resolve(); } - }) - }) + }); + }); } /** @@ -463,12 +524,12 @@ export class ScpClient extends EventEmitter { */ public close() { if (this.sshClient && this.sftpWrapper) { - this.sshClient.end() - this.sshClient = null - this.sftpWrapper = null + this.sshClient.end(); + this.sshClient = null; + this.sftpWrapper = null; } - this.endCalled = true + this.endCalled = true; } /** @@ -477,12 +538,12 @@ export class ScpClient extends EventEmitter { public async list(remotePath: string, pattern = /.*/): Promise { const _list = (aPath: string, filter: RegExp | string) => { return new Promise((resolve, reject) => { - const reg = /-/gi + const reg = /-/gi; this.sftpWrapper!.readdir(aPath, (err, fileList) => { if (err) { - reject(err) + reject(err); } else { - let newList: any = [] + let newList: any = []; // reset file info if (fileList) { newList = fileList.map((item) => { @@ -493,39 +554,39 @@ export class ScpClient extends EventEmitter { 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, '') + 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 - } - }) + group: item.attrs.gid, + }; + }); } // provide some compatibility for auxList - let regex: RegExp + let regex: RegExp; if (filter instanceof RegExp) { - regex = filter + regex = filter; } else { - const newPattern = filter.replace(/\*([^*])*?/gi, '.*') - regex = new RegExp(newPattern) + const newPattern = filter.replace(/\*([^*])*?/gi, ".*"); + regex = new RegExp(newPattern); } - resolve(newList.filter((item: any) => regex.test(item.name))) + resolve(newList.filter((item: any) => regex.test(item.name))); } - }) - }) - } + }); + }); + }; - utils.haveConnection(this, 'list') + utils.haveConnection(this, "list"); const pathInfo = await utils.checkRemotePath( this, remotePath, targetType.readDir - ) + ); if (!pathInfo.valid) { - throw new Error('Remote path is invalid') + throw new Error("Remote path is invalid"); } - return _list(pathInfo.path, pattern) + return _list(pathInfo.path, pattern); } /** @@ -533,41 +594,45 @@ export class ScpClient extends EventEmitter { */ 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)) { + 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' - ) - ) + utils.formatError(`${err.message} ${remotePath}`, "realPath") + ); } - resolve(absPath) - this.removeListener('error', errorListener) - this.removeListener('close', closeListener) - }) + resolve(absPath); + this.removeListener("error", errorListener); + this.removeListener("close", closeListener); + }); } - }) + }); } } export async function Client(options: TScpOptions): Promise { - const client = new ScpClient(options) + const client = new ScpClient(options); return new Promise((resolve, reject) => { - client.on('ready', () => { - resolve(client) - }) - - client.on('error', (err) => { - reject(err) - }) - }) + client.on("ready", () => { + resolve(client); + }); + + client.on("error", (err) => { + reject(err); + }); + + for (const event in options.events) { + client.on(event, (...args) => { + // @ts-ignore + options.events![event as keyof ClientEvents]!(...args); + }); + } + }); } -export default Client +export default Client; diff --git a/src/types.ts b/src/types.ts index 8aaed4d..cbe9b5c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,142 @@ +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 + custom?: boolean; + code?: string; + level?: string; + hostname?: string; + address?: string; } export interface CheckResult { - path: string, - type?: string, - valid?: boolean, - msg?: string, - code?: string -} \ No newline at end of file + 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/src/utils.ts b/src/utils.ts index 76b84a1..52d39c5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,9 @@ -import fs from 'fs' -import path from 'path' -import { errorCode, targetType } from './constant' -import { EventEmitter } from 'events' -import { ScpClient } from '.' -import { CheckResult, ErrorCustom } from './types' +import fs from "fs"; +import path from "path"; +import { errorCode, targetType } from "./constant"; +import { EventEmitter } from "events"; +import { ScpClient } from "."; +import { CheckResult, ErrorCustom } from "./types"; /** * Generate a new Error object with a reformatted error message which @@ -16,54 +16,54 @@ import { CheckResult, ErrorCustom } from './types' */ export function formatError( err: ErrorCustom | string, - name = 'sftp', + name = "sftp", eCode = errorCode.generic, retryCount?: number ) { - let msg = '' - let code = '' + let msg = ""; + let code = ""; const retry = retryCount - ? ` after ${retryCount} ${retryCount > 1 ? 'attempts' : 'attempt'}` - : '' + ? ` 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 + 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! + msg = `${name}->${err.message}${retry}`; + code = err.code!; } else { switch (err.code) { - case 'ENOTFOUND': + case "ENOTFOUND": msg = `${name}: ${err.level} error. ` + - `Address lookup failed for host ${err.hostname}${retry}` - break - case 'ECONNREFUSED': + `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': + `${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 + `${err.message}${retry}`; + break; + case "ENOENT": + msg = `${name}: ${err.message}${retry}`; + break; default: - msg = `${name}: ${err.message}${retry}` + msg = `${name}: ${err.message}${retry}`; } - code = err.code ? err.code : eCode + code = err.code ? err.code : eCode; } - const newError = new ErrorCustom(msg) - newError.code = code - newError.custom = true - return newError + const newError = new ErrorCustom(msg); + newError.code = code; + newError.custom = true; + return newError; } /** @@ -76,18 +76,22 @@ export function formatError( * throwing the error * @throws {Error} */ -export function handleError(err: ErrorCustom, name: string, reject: (e: any) => void) { +export function handleError( + err: ErrorCustom, + name: string, + reject: (e: any) => void +) { if (reject) { if (err.custom) { - reject(err) + reject(err); } else { - reject(formatError(err, name, undefined, undefined)) + reject(formatError(err, name, undefined, undefined)); } } else { if (err.custom) { - throw err + throw err; } else { - throw formatError(err, name, undefined, undefined) + throw formatError(err, name, undefined, undefined); } } } @@ -111,34 +115,40 @@ export function handleError(err: ErrorCustom, name: string, reject: (e: any) => * @param {Error} err - source for defining new error * @throws {Error} Throws new error */ -export function makeErrorListener(reject: (e: any) => void, client: ScpClient, name: string) { +export function makeErrorListener( + reject: (e: any) => void, + client: ScpClient, + name: string +) { return (err: Error) => { - client.errorHandled = true - reject(formatError(err, name)) - } + client.errorHandled = true; + reject(formatError(err, name)); + }; } export function makeEndListener(client: ScpClient) { return () => { if (!client.endCalled) { - console.error( - 'End Listener: Connection ended unexpectedly' - ) + console.error("End Listener: Connection ended unexpectedly"); } - } + }; } -export function makeCloseListener(client: ScpClient, reject?: (e: any) => void, name?: string) { +export function makeCloseListener( + client: ScpClient, + reject?: (e: any) => void, + name?: string +) { return () => { if (!client.endCalled) { if (reject) { - reject(formatError('Connection closed unexpectedly', name)) + reject(formatError("Connection closed unexpectedly", name)); } else { - console.error('Connection closed unexpectedly') + console.error("Connection closed unexpectedly"); } } - client.sftpWrapper = null - } + client.sftpWrapper = null; + }; } /** @@ -155,24 +165,24 @@ export function localExists(localPath: string): Promise { return new Promise((resolve, reject) => { fs.stat(localPath, (err, stats) => { if (err) { - if (err.code === 'ENOENT') { - resolve('ENOENT') + if (err.code === "ENOENT") { + resolve("ENOENT"); } else { - reject(err) + reject(err); } } else { if (stats.isDirectory()) { - resolve('d') + resolve("d"); } else if (stats.isSymbolicLink()) { - resolve('l') + resolve("l"); } else if (stats.isFile()) { - resolve('-') + resolve("-"); } else { - resolve('') + resolve(""); } } - }) - }) + }); + }); } /** @@ -185,75 +195,78 @@ export function localExists(localPath: string): Promise { */ export function classifyError(err: ErrorCustom, testPath: string) { switch (err.code) { - case 'EACCES': + case "EACCES": return { msg: `Permission denied: ${testPath}`, - code: errorCode.permission - } - case 'ENOENT': + code: errorCode.permission, + }; + case "ENOENT": return { msg: `No such file: ${testPath}`, - code: errorCode.notexist - } - case 'ENOTDIR': + code: errorCode.notexist, + }; + case "ENOTDIR": return { msg: `Not a directory: ${testPath}`, - code: errorCode.notdir - } + code: errorCode.notdir, + }; default: return { msg: err.message, - code: err.code ? err.code : errorCode.generic - } + code: err.code ? err.code : errorCode.generic, + }; } } -export function localAccess(localPath: string, mode: number): Promise { +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) + const { msg, code } = classifyError(err, localPath); resolve({ path: localPath, valid: false, msg, - code - }) + code, + }); } else { resolve({ path: localPath, - valid: true - }) + 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 + 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) + const access = await localAccess(localPath, fs.constants.R_OK); if (access.valid) { - rslt.valid = true - return rslt + rslt.valid = true; + return rslt; } else { - rslt.valid = false - rslt.msg = access.msg - rslt.code = access.code - return rslt + rslt.valid = false; + rslt.msg = access.msg; + rslt.code = access.code; + return rslt; } } } catch (err) { - throw formatError(err as ErrorCustom, 'checkLocalReadFile') + throw formatError(err as ErrorCustom, "checkLocalReadFile"); } } @@ -261,74 +274,77 @@ export async function checkLocalReadDir(localPath: string, localType: string) { try { const rslt: CheckResult = { path: localPath, - type: localType - } + 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 + 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 = false; + rslt.msg = access.msg; + rslt.code = access.code; + return rslt; } - rslt.valid = true - return rslt + rslt.valid = true; + return rslt; } } catch (err) { - throw formatError(err as ErrorCustom, 'checkLocalReadDir') + throw formatError(err as ErrorCustom, "checkLocalReadDir"); } } -export async function checkLocalWriteFile(localPath: string, localType: string) { +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 + 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) + const dir = path.parse(localPath).dir; + const parent = await localAccess(dir, fs.constants.W_OK); if (parent.valid) { - rslt.valid = true - return rslt + rslt.valid = true; + return rslt; } else { - rslt.valid = false - rslt.msg = parent.msg - rslt.code = parent.code - return rslt + rslt.valid = false; + rslt.msg = parent.msg; + rslt.code = parent.code; + return rslt; } } else { - const access = await localAccess(localPath, fs.constants.W_OK) + const access = await localAccess(localPath, fs.constants.W_OK); if (access.valid) { - rslt.valid = true - return rslt + rslt.valid = true; + return rslt; } else { - rslt.valid = false - rslt.msg = access.msg - rslt.code = access.code - return rslt + rslt.valid = false; + rslt.msg = access.msg; + rslt.code = access.code; + return rslt; } } } catch (err) { - throw formatError(err as ErrorCustom, 'checkLocalWriteFile') + throw formatError(err as ErrorCustom, "checkLocalWriteFile"); } } @@ -336,75 +352,78 @@ export async function checkLocalWriteDir(localPath: string, localType: string) { try { const rslt: CheckResult = { path: localPath, - type: localType - } + type: localType, + }; if (!localType) { - const parent = path.parse(localPath).dir - const access = await localAccess(parent, fs.constants.W_OK) + const parent = path.parse(localPath).dir; + const access = await localAccess(parent, fs.constants.W_OK); if (access.valid) { - rslt.valid = true - return rslt + rslt.valid = true; + return rslt; } else { - rslt.valid = false - rslt.msg = access.msg - rslt.code = access.code - return rslt + 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 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) + const access = await localAccess(localPath, fs.constants.W_OK); if (access.valid) { - rslt.valid = true - return rslt + rslt.valid = true; + return rslt; } else { - rslt.valid = false - rslt.msg = access.msg - rslt.code = access.code - return rslt + rslt.valid = false; + rslt.msg = access.msg; + rslt.code = access.code; + return rslt; } } } catch (err) { - throw formatError(err as ErrorCustom, 'checkLocalWriteDir') + 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) +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) + return checkLocalReadFile(localPath, type); case targetType.readDir: - return checkLocalReadDir(localPath, type) + return checkLocalReadDir(localPath, type); case targetType.writeFile: - return checkLocalWriteFile(localPath, type) + return checkLocalWriteFile(localPath, type); case targetType.writeDir: - return checkLocalWriteDir(localPath, type) + return checkLocalWriteDir(localPath, type); default: return { path: localPath, type, - valid: true - } + 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) + 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 + return aPath; } catch (err) { - throw formatError(err as ErrorCustom, 'normalizeRemotePath') + throw formatError(err as ErrorCustom, "normalizeRemotePath"); } } @@ -414,8 +433,8 @@ export function checkReadObject(aPath: string, type: string) { type, valid: type ? true : false, msg: type ? undefined : `No such file ${aPath}`, - code: type ? undefined : errorCode.notexist - } + code: type ? undefined : errorCode.notexist, + }; } export function checkReadFile(aPath: string, type: string) { @@ -425,22 +444,22 @@ export function checkReadFile(aPath: string, type: string) { type, valid: false, msg: `No such file: ${aPath}`, - code: errorCode.notexist - } - } else if (type === 'd') { + code: errorCode.notexist, + }; + } else if (type === "d") { return { path: aPath, type, valid: false, msg: `Bad path: ${aPath} must be a file`, - code: errorCode.badPath - } + code: errorCode.badPath, + }; } return { path: aPath, type, - valid: true - } + valid: true, + }; } export function checkReadDir(aPath: string, type: string) { @@ -450,35 +469,39 @@ export function checkReadDir(aPath: string, type: string) { type, valid: false, msg: `No such directory: ${aPath}`, - code: errorCode.notdir - } - } else if (type !== 'd') { + code: errorCode.notdir, + }; + } else if (type !== "d") { return { path: aPath, type, valid: false, msg: `Bad path: ${aPath} must be a directory`, - code: errorCode.badPath - } + code: errorCode.badPath, + }; } return { path: aPath, type, - valid: true - } + valid: true, + }; } -export async function checkWriteFile(client: ScpClient, aPath: string, type: string) { - if (type && type === 'd') { +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 - } + code: errorCode.badPath, + }; } else if (!type) { - const { root, dir } = path.parse(aPath) + const { root, dir } = path.parse(aPath); // let parentDir = path.parse(aPath).dir; if (!dir) { return { @@ -486,64 +509,68 @@ export async function checkWriteFile(client: ScpClient, aPath: string, type: str type: false, valid: false, msg: `Bad path: ${aPath} cannot determine parent directory`, - code: errorCode.badPath - } + code: errorCode.badPath, + }; } if (root === dir) { return { path: aPath, type, - valid: true - } + valid: true, + }; } - const parentType = await client.exists(dir) + 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') { + code: errorCode.badPath, + }; + } else if (parentType !== "d") { return { path: aPath, type, valid: false, msg: `Bad path: ${dir} must be a directory`, - code: errorCode.badPath - } + code: errorCode.badPath, + }; } return { path: aPath, type, - valid: true - } + valid: true, + }; } return { path: aPath, type, - valid: true - } + valid: true, + }; } -export async function checkWriteDir(client: ScpClient, aPath: string, type: string) { - if (type && type !== 'd') { +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 - } + code: errorCode.badPath, + }; } else if (!type) { - const { root, dir } = path.parse(aPath) + const { root, dir } = path.parse(aPath); if (root === dir) { return { path: aPath, type, - valid: true - } + valid: true, + }; } if (!dir) { return { @@ -551,18 +578,18 @@ export async function checkWriteDir(client: ScpClient, aPath: string, type: stri type: false, valid: false, msg: `Bad path: ${aPath} cannot determine directory parent`, - code: errorCode.badPath - } + code: errorCode.badPath, + }; } - const parentType = await client.exists(dir) - if (parentType && parentType !== 'd') { + 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 - } + 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 @@ -570,8 +597,8 @@ export async function checkWriteDir(client: ScpClient, aPath: string, type: stri return { path: aPath, type, - valid: true - } + valid: true, + }; } export function checkWriteObject(aPath: string, type: string) { @@ -580,32 +607,36 @@ export function checkWriteObject(aPath: string, type: string) { return { path: aPath, type, - valid: true - } + 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) +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) + return checkReadObject(aPath, type as string); case targetType.readFile: - return checkReadFile(aPath, type as string) + return checkReadFile(aPath, type as string); case targetType.readDir: - return checkReadDir(aPath, type as string) + return checkReadDir(aPath, type as string); case targetType.writeFile: - return checkWriteFile(client, aPath, type as string) + return checkWriteFile(client, aPath, type as string); case targetType.writeDir: - return checkWriteDir(client, aPath, type as string) + return checkWriteDir(client, aPath, type as string); case targetType.writeObj: - return checkWriteObject(aPath, type as string) + return checkWriteObject(aPath, type as string); default: throw formatError( `Unknown target type: ${target}`, - 'checkRemotePath', + "checkRemotePath", errorCode.generic - ) + ); } } @@ -619,47 +650,55 @@ export async function checkRemotePath(client: ScpClient, rPath: string, target = * @returns {Boolean} True if connection OK * @throws {Error} */ -export function haveConnection(client: ScpClient, name: string, reject?: (e: any) => void) { +export function haveConnection( + client: ScpClient, + name: string, + reject?: (e: any) => void +) { if (!client.sftpWrapper) { const newError = formatError( - 'No SFTP connection available', + "No SFTP connection available", name, errorCode.connect - ) + ); if (reject) { - reject(newError) - return false + reject(newError); + return false; } else { - throw newError + throw newError; } } - return true + return true; } export function dumpListeners(emitter: EventEmitter) { - const eventNames = emitter.eventNames() + const eventNames = emitter.eventNames(); if (eventNames.length) { - console.log('Listener Data') + console.log("Listener Data"); eventNames.map((n: any) => { - const listeners = emitter.listeners(n) - console.log(`${n}: ${emitter.listenerCount(n)}`) - console.dir(listeners) + const listeners = emitter.listeners(n); + console.log(`${n}: ${emitter.listenerCount(n)}`); + console.dir(listeners); listeners.map((l) => { - console.log(`listener name = ${l.name}`) - }) - }) + 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 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.win32.join(...args); } - return path.posix.join(...args) -} \ No newline at end of file + return path.posix.join(...args); +}