Initial commit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dinlo
2026-05-31 18:45:31 +08:00
commit e0a986eb30
1018 changed files with 615974 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2021 Steel Brain
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+320
View File
@@ -0,0 +1,320 @@
Node-SSH - SSH2 with Promises
=========
Node-SSH is an extremely lightweight Promise wrapper for [ssh2][ssh2].
#### Installation
```sh
$ npm install node-ssh # If you're using npm
$ yarn add node-ssh # If you're using Yarn
```
#### Example
```js
const fs = require('fs')
const path = require('path')
const {NodeSSH} = require('node-ssh')
const ssh = new NodeSSH()
ssh.connect({
host: 'localhost',
username: 'steel',
privateKeyPath: '/home/steel/.ssh/id_rsa'
})
// or with inline privateKey
ssh.connect({
host: 'localhost',
username: 'steel',
privateKey: Buffer.from('...')
})
.then(function() {
// Local, Remote
ssh.putFile('/home/steel/Lab/localPath/fileName', '/home/steel/Lab/remotePath/fileName').then(function() {
console.log("The File thing is done")
}, function(error) {
console.log("Something's wrong")
console.log(error)
})
// Array<Shape('local' => string, 'remote' => string)>
ssh.putFiles([{ local: '/home/steel/Lab/localPath/fileName', remote: '/home/steel/Lab/remotePath/fileName' }]).then(function() {
console.log("The File thing is done")
}, function(error) {
console.log("Something's wrong")
console.log(error)
})
// Local, Remote
ssh.getFile('/home/steel/Lab/localPath', '/home/steel/Lab/remotePath').then(function(Contents) {
console.log("The File's contents were successfully downloaded")
}, function(error) {
console.log("Something's wrong")
console.log(error)
})
// Putting entire directories
const failed = []
const successful = []
ssh.putDirectory('/home/steel/Lab', '/home/steel/Lab', {
recursive: true,
concurrency: 10,
// ^ WARNING: Not all servers support high concurrency
// try a bunch of values and see what works on your server
validate: function(itemPath) {
const baseName = path.basename(itemPath)
return baseName.substr(0, 1) !== '.' && // do not allow dot files
baseName !== 'node_modules' // do not allow node_modules
},
tick: function(localPath, remotePath, error) {
if (error) {
failed.push(localPath)
} else {
successful.push(localPath)
}
}
}).then(function(status) {
console.log('the directory transfer was', status ? 'successful' : 'unsuccessful')
console.log('failed transfers', failed.join(', '))
console.log('successful transfers', successful.join(', '))
})
// Command
ssh.execCommand('hh_client --json', { cwd:'/var/www' }).then(function(result) {
console.log('STDOUT: ' + result.stdout)
console.log('STDERR: ' + result.stderr)
})
// Command with escaped params
ssh.exec('hh_client', ['--json'], { cwd: '/var/www', stream: 'stdout', options: { pty: true } }).then(function(result) {
console.log('STDOUT: ' + result)
})
// With streaming stdout/stderr callbacks
ssh.exec('hh_client', ['--json'], {
cwd: '/var/www',
onStdout(chunk) {
console.log('stdoutChunk', chunk.toString('utf8'))
},
onStderr(chunk) {
console.log('stderrChunk', chunk.toString('utf8'))
},
})
})
```
#### API
```ts
// API reference in Typescript typing format:
import SSH2, {
AcceptConnection,
Channel,
ClientChannel,
ConnectConfig,
ExecOptions,
Prompt,
PseudoTtyOptions,
RejectConnection,
SFTPWrapper,
ShellOptions,
TcpConnectionDetails,
TransferOptions,
UNIXConnectionDetails,
} from 'ssh2'
import stream from 'stream'
// ^ You do NOT need to import these package, these are here for reference of where the
// types are coming from.
export type Config = ConnectConfig & {
password?: string
privateKey?: string
privateKeyPath?: string
tryKeyboard?: boolean
onKeyboardInteractive?: (
name: string,
instructions: string,
lang: string,
prompts: Prompt[],
finish: (responses: string[]) => void,
) => void
}
export interface SSHExecCommandOptions {
cwd?: string
stdin?: string | stream.Readable
execOptions?: ExecOptions
encoding?: BufferEncoding
noTrim?: boolean
onChannel?: (clientChannel: ClientChannel) => void
onStdout?: (chunk: Buffer) => void
onStderr?: (chunk: Buffer) => void
}
export interface SSHExecCommandResponse {
stdout: string
stderr: string
code: number | null
signal: string | null
}
export interface SSHExecOptions extends SSHExecCommandOptions {
stream?: 'stdout' | 'stderr' | 'both'
}
export interface SSHPutFilesOptions {
sftp?: SFTPWrapper | null
concurrency?: number
transferOptions?: TransferOptions
}
export interface SSHGetPutDirectoryOptions extends SSHPutFilesOptions {
tick?: (localFile: string, remoteFile: string, error: Error | null) => void
validate?: (path: string) => boolean
recursive?: boolean
}
export type SSHMkdirMethod = 'sftp' | 'exec'
export type SSHForwardInListener = (
details: TcpConnectionDetails,
accept: AcceptConnection<ClientChannel>,
reject: RejectConnection,
) => void
export interface SSHForwardInDetails {
dispose(): Promise<void>
port: number
}
export type SSHForwardInStreamLocalListener = (
info: UNIXConnectionDetails,
accept: AcceptConnection,
reject: RejectConnection,
) => void
export interface SSHForwardInStreamLocalDetails {
dispose(): Promise<void>
}
export class SSHError extends Error {
code: string | null
constructor(message: string, code?: string | null)
}
export class NodeSSH {
connection: SSH2.Client | null
connect(config: Config): Promise<this>
isConnected(): boolean
requestShell(options?: PseudoTtyOptions | ShellOptions | false): Promise<ClientChannel>
withShell(
callback: (channel: ClientChannel) => Promise<void>,
options?: PseudoTtyOptions | ShellOptions | false,
): Promise<void>
requestSFTP(): Promise<SFTPWrapper>
withSFTP(callback: (sftp: SFTPWrapper) => Promise<void>): Promise<void>
execCommand(givenCommand: string, options?: SSHExecCommandOptions): Promise<SSHExecCommandResponse>
exec(
command: string,
parameters: string[],
options?: SSHExecOptions & {
stream?: 'stdout' | 'stderr'
},
): Promise<string>
exec(
command: string,
parameters: string[],
options?: SSHExecOptions & {
stream: 'both'
},
): Promise<SSHExecCommandResponse>
mkdir(path: string, method?: SSHMkdirMethod, givenSftp?: SFTPWrapper | null): Promise<void>
getFile(
localFile: string,
remoteFile: string,
givenSftp?: SFTPWrapper | null,
transferOptions?: TransferOptions | null,
): Promise<void>
putFile(
localFile: string,
remoteFile: string,
givenSftp?: SFTPWrapper | null,
transferOptions?: TransferOptions | null,
): Promise<void>
putFiles(
files: {
local: string
remote: string
}[],
{ concurrency, sftp: givenSftp, transferOptions }?: SSHPutFilesOptions,
): Promise<void>
putDirectory(
localDirectory: string,
remoteDirectory: string,
{ concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate }?: SSHGetPutDirectoryOptions,
): Promise<boolean>
getDirectory(
localDirectory: string,
remoteDirectory: string,
{ concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate }?: SSHGetPutDirectoryOptions,
): Promise<boolean>
forwardIn(remoteAddr: string, remotePort: number, onConnection?: SSHForwardInListener): Promise<SSHForwardInDetails>
forwardOut(srcIP: string, srcPort: number, dstIP: string, dstPort: number): Promise<Channel>
forwardInStreamLocal(
socketPath: string,
onConnection?: SSHForwardInStreamLocalListener,
): Promise<SSHForwardInStreamLocalDetails>
forwardOutStreamLocal(socketPath: string): Promise<Channel>
dispose(): void
}
```
### Typescript support
`node-ssh` requires extra dependencies while working under Typescript. Please install them as shown below
```
yarn add --dev @types/ssh2
# OR
npm install --save-dev @types/ssh2
```
If you're still running into issues, try adding these to your `tsconfig.json`
```json
{
"compilerOptions": {
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
}
}
```
### Keyboard-interactive user authentication
In some cases you have to enable keyboard-interactive user authentication.
Otherwise you will get an `All configured authentication methods failed` error.
#### Example:
```js
const password = 'test'
ssh.connect({
host: 'localhost',
username: 'steel',
port: 22,
password,
tryKeyboard: true,
})
// Or if you want to add some custom keyboard-interactive logic:
ssh.connect({
host: 'localhost',
username: 'steel',
port: 22,
tryKeyboard: true,
onKeyboardInteractive(name, instructions, instructionsLang, prompts, finish) {
if (prompts.length > 0 && prompts[0].prompt.toLowerCase().includes('password')) {
finish([password])
}
}
})
```
For further information see: https://github.com/mscdex/ssh2/issues/604
### License
This project is licensed under the terms of MIT license. See the LICENSE file for more info.
[ssh2]:https://github.com/mscdex/ssh2
+735
View File
@@ -0,0 +1,735 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeSSH = exports.SSHError = void 0;
const assert_1 = __importStar(require("assert"));
const fs_1 = __importDefault(require("fs"));
const is_stream_1 = __importDefault(require("is-stream"));
const make_dir_1 = __importDefault(require("make-dir"));
const path_1 = __importDefault(require("path"));
const sb_promise_queue_1 = require("sb-promise-queue");
const sb_scandir_1 = __importDefault(require("sb-scandir"));
const shell_escape_1 = __importDefault(require("shell-escape"));
const ssh2_1 = __importDefault(require("ssh2"));
const DEFAULT_CONCURRENCY = 1;
const DEFAULT_VALIDATE = (path) => !path_1.default.basename(path).startsWith('.');
const DEFAULT_TICK = () => {
/* No Op */
};
class SSHError extends Error {
constructor(message, code = null) {
super(message);
this.code = code;
}
}
exports.SSHError = SSHError;
function unixifyPath(path) {
if (path.includes('\\')) {
return path.split('\\').join('/');
}
return path;
}
async function readFile(filePath) {
return new Promise((resolve, reject) => {
fs_1.default.readFile(filePath, 'utf8', (err, res) => {
if (err) {
reject(err);
}
else {
resolve(res);
}
});
});
}
const SFTP_MKDIR_ERR_CODE_REGEXP = /Error: (E[\S]+): /;
async function makeDirectoryWithSftp(path, sftp) {
let stats = null;
try {
stats = await new Promise((resolve, reject) => {
sftp.stat(path, (err, res) => {
if (err) {
reject(err);
}
else {
resolve(res);
}
});
});
}
catch (_) {
/* No Op */
}
if (stats) {
if (stats.isDirectory()) {
// Already exists, nothing to worry about
return;
}
throw new Error('mkdir() failed, target already exists and is not a directory');
}
try {
await new Promise((resolve, reject) => {
sftp.mkdir(path, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
catch (err) {
if (err != null && typeof err.stack === 'string') {
const matches = SFTP_MKDIR_ERR_CODE_REGEXP.exec(err.stack);
if (matches != null) {
throw new SSHError(err.message, matches[1]);
}
throw err;
}
}
}
class NodeSSH {
constructor() {
this.connection = null;
}
getConnection() {
const { connection } = this;
if (connection == null) {
throw new Error('Not connected to server');
}
return connection;
}
async connect(givenConfig) {
(0, assert_1.default)(givenConfig != null && typeof givenConfig === 'object', 'config must be a valid object');
const config = { ...givenConfig };
(0, assert_1.default)(config.username != null && typeof config.username === 'string', 'config.username must be a valid string');
if (config.host != null) {
(0, assert_1.default)(typeof config.host === 'string', 'config.host must be a valid string');
}
else if (config.sock != null) {
(0, assert_1.default)(typeof config.sock === 'object', 'config.sock must be a valid object');
}
else {
throw new assert_1.AssertionError({ message: 'Either config.host or config.sock must be provided' });
}
if (config.privateKey != null || config.privateKeyPath != null) {
if (config.privateKey != null) {
(0, assert_1.default)(typeof config.privateKey === 'string', 'config.privateKey must be a valid string');
(0, assert_1.default)(config.privateKeyPath == null, 'config.privateKeyPath must not be specified when config.privateKey is specified');
}
else if (config.privateKeyPath != null) {
(0, assert_1.default)(typeof config.privateKeyPath === 'string', 'config.privateKeyPath must be a valid string');
(0, assert_1.default)(config.privateKey == null, 'config.privateKey must not be specified when config.privateKeyPath is specified');
}
(0, assert_1.default)(config.passphrase == null || typeof config.passphrase === 'string', 'config.passphrase must be null or a valid string');
if (config.privateKeyPath != null) {
// Must be an fs path
try {
config.privateKey = await readFile(config.privateKeyPath);
}
catch (err) {
if (err != null && err.code === 'ENOENT') {
throw new assert_1.AssertionError({ message: 'config.privateKeyPath does not exist at given fs path' });
}
throw err;
}
}
}
else if (config.password != null) {
(0, assert_1.default)(typeof config.password === 'string', 'config.password must be a valid string');
}
if (config.tryKeyboard != null) {
(0, assert_1.default)(typeof config.tryKeyboard === 'boolean', 'config.tryKeyboard must be a valid boolean');
}
if (config.tryKeyboard) {
const { password } = config;
if (config.onKeyboardInteractive != null) {
(0, assert_1.default)(typeof config.onKeyboardInteractive === 'function', 'config.onKeyboardInteractive must be a valid function');
}
else if (password != null) {
config.onKeyboardInteractive = (name, instructions, instructionsLang, prompts, finish) => {
if (prompts.length > 0 && prompts[0].prompt.toLowerCase().includes('password')) {
finish([password]);
}
};
}
}
const connection = new ssh2_1.default.Client();
this.connection = connection;
await new Promise((resolve, reject) => {
connection.on('error', reject);
if (config.onKeyboardInteractive) {
connection.on('keyboard-interactive', config.onKeyboardInteractive);
}
connection.on('ready', () => {
connection.removeListener('error', reject);
resolve();
});
connection.on('end', () => {
if (this.connection === connection) {
this.connection = null;
}
});
connection.on('close', () => {
if (this.connection === connection) {
this.connection = null;
}
reject(new SSHError('No response from server', 'ETIMEDOUT'));
});
connection.connect(config);
});
return this;
}
isConnected() {
return this.connection != null;
}
async requestShell(options) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.on('error', reject);
const callback = (err, res) => {
connection.removeListener('error', reject);
if (err) {
reject(err);
}
else {
resolve(res);
}
};
if (options == null) {
connection.shell(callback);
}
else {
connection.shell(options, callback);
}
});
}
async withShell(callback, options) {
(0, assert_1.default)(typeof callback === 'function', 'callback must be a valid function');
const shell = await this.requestShell(options);
try {
await callback(shell);
}
finally {
shell.destroy();
}
}
async requestSFTP() {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.on('error', reject);
connection.sftp((err, res) => {
connection.removeListener('error', reject);
if (err) {
reject(err);
}
else {
resolve(res);
}
});
});
}
async withSFTP(callback) {
(0, assert_1.default)(typeof callback === 'function', 'callback must be a valid function');
const sftp = await this.requestSFTP();
try {
await callback(sftp);
}
finally {
sftp.end();
}
}
async execCommand(givenCommand, options = {}) {
(0, assert_1.default)(typeof givenCommand === 'string', 'command must be a valid string');
(0, assert_1.default)(options != null && typeof options === 'object', 'options must be a valid object');
(0, assert_1.default)(options.cwd == null || typeof options.cwd === 'string', 'options.cwd must be a valid string');
(0, assert_1.default)(options.stdin == null || typeof options.stdin === 'string' || is_stream_1.default.readable(options.stdin), 'options.stdin must be a valid string or readable stream');
(0, assert_1.default)(options.execOptions == null || typeof options.execOptions === 'object', 'options.execOptions must be a valid object');
(0, assert_1.default)(options.encoding == null || typeof options.encoding === 'string', 'options.encoding must be a valid string');
(0, assert_1.default)(options.onChannel == null || typeof options.onChannel === 'function', 'options.onChannel must be a valid function');
(0, assert_1.default)(options.onStdout == null || typeof options.onStdout === 'function', 'options.onStdout must be a valid function');
(0, assert_1.default)(options.onStderr == null || typeof options.onStderr === 'function', 'options.onStderr must be a valid function');
(0, assert_1.default)(options.noTrim == null || typeof options.noTrim === 'boolean', 'options.noTrim must be a boolean');
let command = givenCommand;
if (options.cwd) {
command = `cd ${(0, shell_escape_1.default)([options.cwd])} ; ${command}`;
}
const connection = this.getConnection();
const output = { stdout: [], stderr: [] };
return new Promise((resolve, reject) => {
connection.on('error', reject);
connection.exec(command, options.execOptions != null ? options.execOptions : {}, (err, channel) => {
connection.removeListener('error', reject);
if (err) {
reject(err);
return;
}
if (options.onChannel) {
options.onChannel(channel);
}
channel.on('data', (chunk) => {
if (options.onStdout)
options.onStdout(chunk);
output.stdout.push(chunk.toString(options.encoding));
});
channel.stderr.on('data', (chunk) => {
if (options.onStderr)
options.onStderr(chunk);
output.stderr.push(chunk.toString(options.encoding));
});
if (options.stdin != null) {
if (is_stream_1.default.readable(options.stdin)) {
options.stdin.pipe(channel, {
end: true,
});
}
else {
channel.write(options.stdin);
channel.end();
}
}
else {
channel.end();
}
let code = null;
let signal = null;
channel.on('exit', (code_, signal_) => {
code = code_ !== null && code_ !== void 0 ? code_ : null;
signal = signal_ !== null && signal_ !== void 0 ? signal_ : null;
});
channel.on('close', () => {
let stdout = output.stdout.join('');
let stderr = output.stderr.join('');
if (options.noTrim !== true) {
stdout = stdout.trim();
stderr = stderr.trim();
}
resolve({
code: code != null ? code : null,
signal: signal != null ? signal : null,
stdout,
stderr,
});
});
});
});
}
async exec(command, parameters, options = {}) {
(0, assert_1.default)(typeof command === 'string', 'command must be a valid string');
(0, assert_1.default)(Array.isArray(parameters), 'parameters must be a valid array');
(0, assert_1.default)(options != null && typeof options === 'object', 'options must be a valid object');
(0, assert_1.default)(options.stream == null || ['both', 'stdout', 'stderr'].includes(options.stream), 'options.stream must be one of both, stdout, stderr');
for (let i = 0, { length } = parameters; i < length; i += 1) {
(0, assert_1.default)(typeof parameters[i] === 'string', `parameters[${i}] must be a valid string`);
}
const completeCommand = `${command}${parameters.length > 0 ? ` ${(0, shell_escape_1.default)(parameters)}` : ''}`;
const response = await this.execCommand(completeCommand, options);
if (options.stream == null || options.stream === 'stdout') {
if (response.stderr) {
throw new Error(response.stderr);
}
return response.stdout;
}
if (options.stream === 'stderr') {
return response.stderr;
}
return response;
}
async mkdir(path, method = 'sftp', givenSftp = null) {
(0, assert_1.default)(typeof path === 'string', 'path must be a valid string');
(0, assert_1.default)(typeof method === 'string' && (method === 'sftp' || method === 'exec'), 'method must be either sftp or exec');
(0, assert_1.default)(givenSftp == null || typeof givenSftp === 'object', 'sftp must be a valid object');
if (method === 'exec') {
await this.exec('mkdir', ['-p', unixifyPath(path)]);
return;
}
const sftp = givenSftp || (await this.requestSFTP());
const makeSftpDirectory = async (retry) => makeDirectoryWithSftp(unixifyPath(path), sftp).catch(async (error) => {
if (!retry || error == null || (error.message !== 'No such file' && error.code !== 'ENOENT')) {
throw error;
}
await this.mkdir(path_1.default.dirname(path), 'sftp', sftp);
await makeSftpDirectory(false);
});
try {
await makeSftpDirectory(true);
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async getFile(localFile, remoteFile, givenSftp = null, transferOptions = null) {
(0, assert_1.default)(typeof localFile === 'string', 'localFile must be a valid string');
(0, assert_1.default)(typeof remoteFile === 'string', 'remoteFile must be a valid string');
(0, assert_1.default)(givenSftp == null || typeof givenSftp === 'object', 'sftp must be a valid object');
(0, assert_1.default)(transferOptions == null || typeof transferOptions === 'object', 'transferOptions must be a valid object');
const sftp = givenSftp || (await this.requestSFTP());
try {
await new Promise((resolve, reject) => {
sftp.fastGet(unixifyPath(remoteFile), localFile, transferOptions || {}, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async putFile(localFile, remoteFile, givenSftp = null, transferOptions = null) {
(0, assert_1.default)(typeof localFile === 'string', 'localFile must be a valid string');
(0, assert_1.default)(typeof remoteFile === 'string', 'remoteFile must be a valid string');
(0, assert_1.default)(givenSftp == null || typeof givenSftp === 'object', 'sftp must be a valid object');
(0, assert_1.default)(transferOptions == null || typeof transferOptions === 'object', 'transferOptions must be a valid object');
(0, assert_1.default)(await new Promise((resolve) => {
fs_1.default.access(localFile, fs_1.default.constants.R_OK, (err) => {
resolve(err === null);
});
}), `localFile does not exist at ${localFile}`);
const sftp = givenSftp || (await this.requestSFTP());
const putFile = (retry) => {
return new Promise((resolve, reject) => {
sftp.fastPut(localFile, unixifyPath(remoteFile), transferOptions || {}, (err) => {
if (err == null) {
resolve();
return;
}
if (err.message === 'No such file' && retry) {
resolve(this.mkdir(path_1.default.dirname(remoteFile), 'sftp', sftp).then(() => putFile(false)));
}
else {
reject(err);
}
});
});
};
try {
await putFile(true);
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async putFiles(files, { concurrency = DEFAULT_CONCURRENCY, sftp: givenSftp = null, transferOptions = {} } = {}) {
(0, assert_1.default)(Array.isArray(files), 'files must be an array');
for (let i = 0, { length } = files; i < length; i += 1) {
const file = files[i];
(0, assert_1.default)(file, 'files items must be valid objects');
(0, assert_1.default)(file.local && typeof file.local === 'string', `files[${i}].local must be a string`);
(0, assert_1.default)(file.remote && typeof file.remote === 'string', `files[${i}].remote must be a string`);
}
const transferred = [];
const sftp = givenSftp || (await this.requestSFTP());
const queue = new sb_promise_queue_1.PromiseQueue({ concurrency });
try {
await new Promise((resolve, reject) => {
files.forEach((file) => {
queue
.add(async () => {
await this.putFile(file.local, file.remote, sftp, transferOptions);
transferred.push(file);
})
.catch(reject);
});
queue.waitTillIdle().then(resolve);
});
}
catch (error) {
if (error != null) {
error.transferred = transferred;
}
throw error;
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async putDirectory(localDirectory, remoteDirectory, { concurrency = DEFAULT_CONCURRENCY, sftp: givenSftp = null, transferOptions = {}, recursive = true, tick = DEFAULT_TICK, validate = DEFAULT_VALIDATE, } = {}) {
(0, assert_1.default)(typeof localDirectory === 'string' && localDirectory, 'localDirectory must be a string');
(0, assert_1.default)(typeof remoteDirectory === 'string' && remoteDirectory, 'remoteDirectory must be a string');
const localDirectoryStat = await new Promise((resolve) => {
fs_1.default.stat(localDirectory, (err, stat) => {
resolve(stat || null);
});
});
(0, assert_1.default)(localDirectoryStat != null, `localDirectory does not exist at ${localDirectory}`);
(0, assert_1.default)(localDirectoryStat.isDirectory(), `localDirectory is not a directory at ${localDirectory}`);
const sftp = givenSftp || (await this.requestSFTP());
const scanned = await (0, sb_scandir_1.default)(localDirectory, {
recursive,
validate,
});
const files = scanned.files.map((item) => path_1.default.relative(localDirectory, item));
const directories = scanned.directories.map((item) => path_1.default.relative(localDirectory, item));
// Sort shortest to longest
directories.sort((a, b) => a.length - b.length);
let failed = false;
try {
// Do the directories first.
await new Promise((resolve, reject) => {
const queue = new sb_promise_queue_1.PromiseQueue({ concurrency });
directories.forEach((directory) => {
queue
.add(async () => {
await this.mkdir(path_1.default.join(remoteDirectory, directory), 'sftp', sftp);
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
// and now the files
await new Promise((resolve, reject) => {
const queue = new sb_promise_queue_1.PromiseQueue({ concurrency });
files.forEach((file) => {
queue
.add(async () => {
const localFile = path_1.default.join(localDirectory, file);
const remoteFile = path_1.default.join(remoteDirectory, file);
try {
await this.putFile(localFile, remoteFile, sftp, transferOptions);
tick(localFile, remoteFile, null);
}
catch (_) {
failed = true;
tick(localFile, remoteFile, _);
}
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
}
finally {
if (!givenSftp) {
sftp.end();
}
}
return !failed;
}
async getDirectory(localDirectory, remoteDirectory, { concurrency = DEFAULT_CONCURRENCY, sftp: givenSftp = null, transferOptions = {}, recursive = true, tick = DEFAULT_TICK, validate = DEFAULT_VALIDATE, } = {}) {
(0, assert_1.default)(typeof localDirectory === 'string' && localDirectory, 'localDirectory must be a string');
(0, assert_1.default)(typeof remoteDirectory === 'string' && remoteDirectory, 'remoteDirectory must be a string');
const localDirectoryStat = await new Promise((resolve) => {
fs_1.default.stat(localDirectory, (err, stat) => {
resolve(stat || null);
});
});
(0, assert_1.default)(localDirectoryStat != null, `localDirectory does not exist at ${localDirectory}`);
(0, assert_1.default)(localDirectoryStat.isDirectory(), `localDirectory is not a directory at ${localDirectory}`);
const sftp = givenSftp || (await this.requestSFTP());
const scanned = await (0, sb_scandir_1.default)(remoteDirectory, {
recursive,
validate,
concurrency,
fileSystem: {
basename(path) {
return path_1.default.posix.basename(path);
},
join(pathA, pathB) {
return path_1.default.posix.join(pathA, pathB);
},
readdir(path) {
return new Promise((resolve, reject) => {
sftp.readdir(path, (err, res) => {
if (err) {
reject(err);
}
else {
resolve(res.map((item) => item.filename));
}
});
});
},
stat(path) {
return new Promise((resolve, reject) => {
sftp.stat(path, (err, res) => {
if (err) {
reject(err);
}
else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve(res);
}
});
});
},
},
});
const files = scanned.files.map((item) => path_1.default.relative(remoteDirectory, item));
const directories = scanned.directories.map((item) => path_1.default.relative(remoteDirectory, item));
// Sort shortest to longest
directories.sort((a, b) => a.length - b.length);
let failed = false;
try {
// Do the directories first.
await new Promise((resolve, reject) => {
const queue = new sb_promise_queue_1.PromiseQueue({ concurrency });
directories.forEach((directory) => {
queue
.add(async () => {
await (0, make_dir_1.default)(path_1.default.join(localDirectory, directory));
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
// and now the files
await new Promise((resolve, reject) => {
const queue = new sb_promise_queue_1.PromiseQueue({ concurrency });
files.forEach((file) => {
queue
.add(async () => {
const localFile = path_1.default.join(localDirectory, file);
const remoteFile = path_1.default.join(remoteDirectory, file);
try {
await this.getFile(localFile, remoteFile, sftp, transferOptions);
tick(localFile, remoteFile, null);
}
catch (_) {
failed = true;
tick(localFile, remoteFile, _);
}
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
}
finally {
if (!givenSftp) {
sftp.end();
}
}
return !failed;
}
forwardIn(remoteAddr, remotePort, onConnection) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.forwardIn(remoteAddr, remotePort, (error, port) => {
if (error) {
reject(error);
return;
}
const handler = (details, acceptConnection, rejectConnection) => {
if (details.destIP === remoteAddr && details.destPort === port) {
onConnection === null || onConnection === void 0 ? void 0 : onConnection(details, acceptConnection, rejectConnection);
}
};
if (onConnection) {
connection.on('tcp connection', handler);
}
const dispose = () => {
return new Promise((_resolve, _reject) => {
connection.off('tcp connection', handler);
connection.unforwardIn(remoteAddr, port, (_error) => {
if (_error) {
_reject(error);
}
_resolve();
});
});
};
resolve({ port, dispose });
});
});
}
forwardOut(srcIP, srcPort, dstIP, dstPort) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.forwardOut(srcIP, srcPort, dstIP, dstPort, (error, channel) => {
if (error) {
reject(error);
return;
}
resolve(channel);
});
});
}
forwardInStreamLocal(socketPath, onConnection) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.openssh_forwardInStreamLocal(socketPath, (error) => {
if (error) {
reject(error);
return;
}
const handler = (details, acceptConnection, rejectConnection) => {
if (details.socketPath === socketPath) {
onConnection === null || onConnection === void 0 ? void 0 : onConnection(details, acceptConnection, rejectConnection);
}
};
if (onConnection) {
connection.on('unix connection', handler);
}
const dispose = () => {
return new Promise((_resolve, _reject) => {
connection.off('unix connection', handler);
connection.openssh_unforwardInStreamLocal(socketPath, (_error) => {
if (_error) {
_reject(_error);
}
_resolve();
});
});
};
resolve({ dispose });
});
});
}
forwardOutStreamLocal(socketPath) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.openssh_forwardOutStreamLocal(socketPath, (error, channel) => {
if (error) {
reject(error);
return;
}
resolve(channel);
});
});
}
dispose() {
if (this.connection) {
this.connection.end();
this.connection = null;
}
}
}
exports.NodeSSH = NodeSSH;
+704
View File
@@ -0,0 +1,704 @@
import invariant, { AssertionError } from 'assert';
import fs from 'fs';
import isStream from 'is-stream';
import makeDir from 'make-dir';
import fsPath from 'path';
import { PromiseQueue } from 'sb-promise-queue';
import scanDirectory from 'sb-scandir';
import shellEscape from 'shell-escape';
import SSH2 from 'ssh2';
const DEFAULT_CONCURRENCY = 1;
const DEFAULT_VALIDATE = (path) => !fsPath.basename(path).startsWith('.');
const DEFAULT_TICK = () => {
/* No Op */
};
export class SSHError extends Error {
constructor(message, code = null) {
super(message);
this.code = code;
}
}
function unixifyPath(path) {
if (path.includes('\\')) {
return path.split('\\').join('/');
}
return path;
}
async function readFile(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, res) => {
if (err) {
reject(err);
}
else {
resolve(res);
}
});
});
}
const SFTP_MKDIR_ERR_CODE_REGEXP = /Error: (E[\S]+): /;
async function makeDirectoryWithSftp(path, sftp) {
let stats = null;
try {
stats = await new Promise((resolve, reject) => {
sftp.stat(path, (err, res) => {
if (err) {
reject(err);
}
else {
resolve(res);
}
});
});
}
catch (_) {
/* No Op */
}
if (stats) {
if (stats.isDirectory()) {
// Already exists, nothing to worry about
return;
}
throw new Error('mkdir() failed, target already exists and is not a directory');
}
try {
await new Promise((resolve, reject) => {
sftp.mkdir(path, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
catch (err) {
if (err != null && typeof err.stack === 'string') {
const matches = SFTP_MKDIR_ERR_CODE_REGEXP.exec(err.stack);
if (matches != null) {
throw new SSHError(err.message, matches[1]);
}
throw err;
}
}
}
export class NodeSSH {
constructor() {
this.connection = null;
}
getConnection() {
const { connection } = this;
if (connection == null) {
throw new Error('Not connected to server');
}
return connection;
}
async connect(givenConfig) {
invariant(givenConfig != null && typeof givenConfig === 'object', 'config must be a valid object');
const config = { ...givenConfig };
invariant(config.username != null && typeof config.username === 'string', 'config.username must be a valid string');
if (config.host != null) {
invariant(typeof config.host === 'string', 'config.host must be a valid string');
}
else if (config.sock != null) {
invariant(typeof config.sock === 'object', 'config.sock must be a valid object');
}
else {
throw new AssertionError({ message: 'Either config.host or config.sock must be provided' });
}
if (config.privateKey != null || config.privateKeyPath != null) {
if (config.privateKey != null) {
invariant(typeof config.privateKey === 'string', 'config.privateKey must be a valid string');
invariant(config.privateKeyPath == null, 'config.privateKeyPath must not be specified when config.privateKey is specified');
}
else if (config.privateKeyPath != null) {
invariant(typeof config.privateKeyPath === 'string', 'config.privateKeyPath must be a valid string');
invariant(config.privateKey == null, 'config.privateKey must not be specified when config.privateKeyPath is specified');
}
invariant(config.passphrase == null || typeof config.passphrase === 'string', 'config.passphrase must be null or a valid string');
if (config.privateKeyPath != null) {
// Must be an fs path
try {
config.privateKey = await readFile(config.privateKeyPath);
}
catch (err) {
if (err != null && err.code === 'ENOENT') {
throw new AssertionError({ message: 'config.privateKeyPath does not exist at given fs path' });
}
throw err;
}
}
}
else if (config.password != null) {
invariant(typeof config.password === 'string', 'config.password must be a valid string');
}
if (config.tryKeyboard != null) {
invariant(typeof config.tryKeyboard === 'boolean', 'config.tryKeyboard must be a valid boolean');
}
if (config.tryKeyboard) {
const { password } = config;
if (config.onKeyboardInteractive != null) {
invariant(typeof config.onKeyboardInteractive === 'function', 'config.onKeyboardInteractive must be a valid function');
}
else if (password != null) {
config.onKeyboardInteractive = (name, instructions, instructionsLang, prompts, finish) => {
if (prompts.length > 0 && prompts[0].prompt.toLowerCase().includes('password')) {
finish([password]);
}
};
}
}
const connection = new SSH2.Client();
this.connection = connection;
await new Promise((resolve, reject) => {
connection.on('error', reject);
if (config.onKeyboardInteractive) {
connection.on('keyboard-interactive', config.onKeyboardInteractive);
}
connection.on('ready', () => {
connection.removeListener('error', reject);
resolve();
});
connection.on('end', () => {
if (this.connection === connection) {
this.connection = null;
}
});
connection.on('close', () => {
if (this.connection === connection) {
this.connection = null;
}
reject(new SSHError('No response from server', 'ETIMEDOUT'));
});
connection.connect(config);
});
return this;
}
isConnected() {
return this.connection != null;
}
async requestShell(options) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.on('error', reject);
const callback = (err, res) => {
connection.removeListener('error', reject);
if (err) {
reject(err);
}
else {
resolve(res);
}
};
if (options == null) {
connection.shell(callback);
}
else {
connection.shell(options, callback);
}
});
}
async withShell(callback, options) {
invariant(typeof callback === 'function', 'callback must be a valid function');
const shell = await this.requestShell(options);
try {
await callback(shell);
}
finally {
shell.destroy();
}
}
async requestSFTP() {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.on('error', reject);
connection.sftp((err, res) => {
connection.removeListener('error', reject);
if (err) {
reject(err);
}
else {
resolve(res);
}
});
});
}
async withSFTP(callback) {
invariant(typeof callback === 'function', 'callback must be a valid function');
const sftp = await this.requestSFTP();
try {
await callback(sftp);
}
finally {
sftp.end();
}
}
async execCommand(givenCommand, options = {}) {
invariant(typeof givenCommand === 'string', 'command must be a valid string');
invariant(options != null && typeof options === 'object', 'options must be a valid object');
invariant(options.cwd == null || typeof options.cwd === 'string', 'options.cwd must be a valid string');
invariant(options.stdin == null || typeof options.stdin === 'string' || isStream.readable(options.stdin), 'options.stdin must be a valid string or readable stream');
invariant(options.execOptions == null || typeof options.execOptions === 'object', 'options.execOptions must be a valid object');
invariant(options.encoding == null || typeof options.encoding === 'string', 'options.encoding must be a valid string');
invariant(options.onChannel == null || typeof options.onChannel === 'function', 'options.onChannel must be a valid function');
invariant(options.onStdout == null || typeof options.onStdout === 'function', 'options.onStdout must be a valid function');
invariant(options.onStderr == null || typeof options.onStderr === 'function', 'options.onStderr must be a valid function');
invariant(options.noTrim == null || typeof options.noTrim === 'boolean', 'options.noTrim must be a boolean');
let command = givenCommand;
if (options.cwd) {
command = `cd ${shellEscape([options.cwd])} ; ${command}`;
}
const connection = this.getConnection();
const output = { stdout: [], stderr: [] };
return new Promise((resolve, reject) => {
connection.on('error', reject);
connection.exec(command, options.execOptions != null ? options.execOptions : {}, (err, channel) => {
connection.removeListener('error', reject);
if (err) {
reject(err);
return;
}
if (options.onChannel) {
options.onChannel(channel);
}
channel.on('data', (chunk) => {
if (options.onStdout)
options.onStdout(chunk);
output.stdout.push(chunk.toString(options.encoding));
});
channel.stderr.on('data', (chunk) => {
if (options.onStderr)
options.onStderr(chunk);
output.stderr.push(chunk.toString(options.encoding));
});
if (options.stdin != null) {
if (isStream.readable(options.stdin)) {
options.stdin.pipe(channel, {
end: true,
});
}
else {
channel.write(options.stdin);
channel.end();
}
}
else {
channel.end();
}
let code = null;
let signal = null;
channel.on('exit', (code_, signal_) => {
code = code_ !== null && code_ !== void 0 ? code_ : null;
signal = signal_ !== null && signal_ !== void 0 ? signal_ : null;
});
channel.on('close', () => {
let stdout = output.stdout.join('');
let stderr = output.stderr.join('');
if (options.noTrim !== true) {
stdout = stdout.trim();
stderr = stderr.trim();
}
resolve({
code: code != null ? code : null,
signal: signal != null ? signal : null,
stdout,
stderr,
});
});
});
});
}
async exec(command, parameters, options = {}) {
invariant(typeof command === 'string', 'command must be a valid string');
invariant(Array.isArray(parameters), 'parameters must be a valid array');
invariant(options != null && typeof options === 'object', 'options must be a valid object');
invariant(options.stream == null || ['both', 'stdout', 'stderr'].includes(options.stream), 'options.stream must be one of both, stdout, stderr');
for (let i = 0, { length } = parameters; i < length; i += 1) {
invariant(typeof parameters[i] === 'string', `parameters[${i}] must be a valid string`);
}
const completeCommand = `${command}${parameters.length > 0 ? ` ${shellEscape(parameters)}` : ''}`;
const response = await this.execCommand(completeCommand, options);
if (options.stream == null || options.stream === 'stdout') {
if (response.stderr) {
throw new Error(response.stderr);
}
return response.stdout;
}
if (options.stream === 'stderr') {
return response.stderr;
}
return response;
}
async mkdir(path, method = 'sftp', givenSftp = null) {
invariant(typeof path === 'string', 'path must be a valid string');
invariant(typeof method === 'string' && (method === 'sftp' || method === 'exec'), 'method must be either sftp or exec');
invariant(givenSftp == null || typeof givenSftp === 'object', 'sftp must be a valid object');
if (method === 'exec') {
await this.exec('mkdir', ['-p', unixifyPath(path)]);
return;
}
const sftp = givenSftp || (await this.requestSFTP());
const makeSftpDirectory = async (retry) => makeDirectoryWithSftp(unixifyPath(path), sftp).catch(async (error) => {
if (!retry || error == null || (error.message !== 'No such file' && error.code !== 'ENOENT')) {
throw error;
}
await this.mkdir(fsPath.dirname(path), 'sftp', sftp);
await makeSftpDirectory(false);
});
try {
await makeSftpDirectory(true);
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async getFile(localFile, remoteFile, givenSftp = null, transferOptions = null) {
invariant(typeof localFile === 'string', 'localFile must be a valid string');
invariant(typeof remoteFile === 'string', 'remoteFile must be a valid string');
invariant(givenSftp == null || typeof givenSftp === 'object', 'sftp must be a valid object');
invariant(transferOptions == null || typeof transferOptions === 'object', 'transferOptions must be a valid object');
const sftp = givenSftp || (await this.requestSFTP());
try {
await new Promise((resolve, reject) => {
sftp.fastGet(unixifyPath(remoteFile), localFile, transferOptions || {}, (err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
});
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async putFile(localFile, remoteFile, givenSftp = null, transferOptions = null) {
invariant(typeof localFile === 'string', 'localFile must be a valid string');
invariant(typeof remoteFile === 'string', 'remoteFile must be a valid string');
invariant(givenSftp == null || typeof givenSftp === 'object', 'sftp must be a valid object');
invariant(transferOptions == null || typeof transferOptions === 'object', 'transferOptions must be a valid object');
invariant(await new Promise((resolve) => {
fs.access(localFile, fs.constants.R_OK, (err) => {
resolve(err === null);
});
}), `localFile does not exist at ${localFile}`);
const sftp = givenSftp || (await this.requestSFTP());
const putFile = (retry) => {
return new Promise((resolve, reject) => {
sftp.fastPut(localFile, unixifyPath(remoteFile), transferOptions || {}, (err) => {
if (err == null) {
resolve();
return;
}
if (err.message === 'No such file' && retry) {
resolve(this.mkdir(fsPath.dirname(remoteFile), 'sftp', sftp).then(() => putFile(false)));
}
else {
reject(err);
}
});
});
};
try {
await putFile(true);
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async putFiles(files, { concurrency = DEFAULT_CONCURRENCY, sftp: givenSftp = null, transferOptions = {} } = {}) {
invariant(Array.isArray(files), 'files must be an array');
for (let i = 0, { length } = files; i < length; i += 1) {
const file = files[i];
invariant(file, 'files items must be valid objects');
invariant(file.local && typeof file.local === 'string', `files[${i}].local must be a string`);
invariant(file.remote && typeof file.remote === 'string', `files[${i}].remote must be a string`);
}
const transferred = [];
const sftp = givenSftp || (await this.requestSFTP());
const queue = new PromiseQueue({ concurrency });
try {
await new Promise((resolve, reject) => {
files.forEach((file) => {
queue
.add(async () => {
await this.putFile(file.local, file.remote, sftp, transferOptions);
transferred.push(file);
})
.catch(reject);
});
queue.waitTillIdle().then(resolve);
});
}
catch (error) {
if (error != null) {
error.transferred = transferred;
}
throw error;
}
finally {
if (!givenSftp) {
sftp.end();
}
}
}
async putDirectory(localDirectory, remoteDirectory, { concurrency = DEFAULT_CONCURRENCY, sftp: givenSftp = null, transferOptions = {}, recursive = true, tick = DEFAULT_TICK, validate = DEFAULT_VALIDATE, } = {}) {
invariant(typeof localDirectory === 'string' && localDirectory, 'localDirectory must be a string');
invariant(typeof remoteDirectory === 'string' && remoteDirectory, 'remoteDirectory must be a string');
const localDirectoryStat = await new Promise((resolve) => {
fs.stat(localDirectory, (err, stat) => {
resolve(stat || null);
});
});
invariant(localDirectoryStat != null, `localDirectory does not exist at ${localDirectory}`);
invariant(localDirectoryStat.isDirectory(), `localDirectory is not a directory at ${localDirectory}`);
const sftp = givenSftp || (await this.requestSFTP());
const scanned = await scanDirectory(localDirectory, {
recursive,
validate,
});
const files = scanned.files.map((item) => fsPath.relative(localDirectory, item));
const directories = scanned.directories.map((item) => fsPath.relative(localDirectory, item));
// Sort shortest to longest
directories.sort((a, b) => a.length - b.length);
let failed = false;
try {
// Do the directories first.
await new Promise((resolve, reject) => {
const queue = new PromiseQueue({ concurrency });
directories.forEach((directory) => {
queue
.add(async () => {
await this.mkdir(fsPath.join(remoteDirectory, directory), 'sftp', sftp);
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
// and now the files
await new Promise((resolve, reject) => {
const queue = new PromiseQueue({ concurrency });
files.forEach((file) => {
queue
.add(async () => {
const localFile = fsPath.join(localDirectory, file);
const remoteFile = fsPath.join(remoteDirectory, file);
try {
await this.putFile(localFile, remoteFile, sftp, transferOptions);
tick(localFile, remoteFile, null);
}
catch (_) {
failed = true;
tick(localFile, remoteFile, _);
}
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
}
finally {
if (!givenSftp) {
sftp.end();
}
}
return !failed;
}
async getDirectory(localDirectory, remoteDirectory, { concurrency = DEFAULT_CONCURRENCY, sftp: givenSftp = null, transferOptions = {}, recursive = true, tick = DEFAULT_TICK, validate = DEFAULT_VALIDATE, } = {}) {
invariant(typeof localDirectory === 'string' && localDirectory, 'localDirectory must be a string');
invariant(typeof remoteDirectory === 'string' && remoteDirectory, 'remoteDirectory must be a string');
const localDirectoryStat = await new Promise((resolve) => {
fs.stat(localDirectory, (err, stat) => {
resolve(stat || null);
});
});
invariant(localDirectoryStat != null, `localDirectory does not exist at ${localDirectory}`);
invariant(localDirectoryStat.isDirectory(), `localDirectory is not a directory at ${localDirectory}`);
const sftp = givenSftp || (await this.requestSFTP());
const scanned = await scanDirectory(remoteDirectory, {
recursive,
validate,
concurrency,
fileSystem: {
basename(path) {
return fsPath.posix.basename(path);
},
join(pathA, pathB) {
return fsPath.posix.join(pathA, pathB);
},
readdir(path) {
return new Promise((resolve, reject) => {
sftp.readdir(path, (err, res) => {
if (err) {
reject(err);
}
else {
resolve(res.map((item) => item.filename));
}
});
});
},
stat(path) {
return new Promise((resolve, reject) => {
sftp.stat(path, (err, res) => {
if (err) {
reject(err);
}
else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve(res);
}
});
});
},
},
});
const files = scanned.files.map((item) => fsPath.relative(remoteDirectory, item));
const directories = scanned.directories.map((item) => fsPath.relative(remoteDirectory, item));
// Sort shortest to longest
directories.sort((a, b) => a.length - b.length);
let failed = false;
try {
// Do the directories first.
await new Promise((resolve, reject) => {
const queue = new PromiseQueue({ concurrency });
directories.forEach((directory) => {
queue
.add(async () => {
await makeDir(fsPath.join(localDirectory, directory));
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
// and now the files
await new Promise((resolve, reject) => {
const queue = new PromiseQueue({ concurrency });
files.forEach((file) => {
queue
.add(async () => {
const localFile = fsPath.join(localDirectory, file);
const remoteFile = fsPath.join(remoteDirectory, file);
try {
await this.getFile(localFile, remoteFile, sftp, transferOptions);
tick(localFile, remoteFile, null);
}
catch (_) {
failed = true;
tick(localFile, remoteFile, _);
}
})
.catch(reject);
});
resolve(queue.waitTillIdle());
});
}
finally {
if (!givenSftp) {
sftp.end();
}
}
return !failed;
}
forwardIn(remoteAddr, remotePort, onConnection) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.forwardIn(remoteAddr, remotePort, (error, port) => {
if (error) {
reject(error);
return;
}
const handler = (details, acceptConnection, rejectConnection) => {
if (details.destIP === remoteAddr && details.destPort === port) {
onConnection === null || onConnection === void 0 ? void 0 : onConnection(details, acceptConnection, rejectConnection);
}
};
if (onConnection) {
connection.on('tcp connection', handler);
}
const dispose = () => {
return new Promise((_resolve, _reject) => {
connection.off('tcp connection', handler);
connection.unforwardIn(remoteAddr, port, (_error) => {
if (_error) {
_reject(error);
}
_resolve();
});
});
};
resolve({ port, dispose });
});
});
}
forwardOut(srcIP, srcPort, dstIP, dstPort) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.forwardOut(srcIP, srcPort, dstIP, dstPort, (error, channel) => {
if (error) {
reject(error);
return;
}
resolve(channel);
});
});
}
forwardInStreamLocal(socketPath, onConnection) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.openssh_forwardInStreamLocal(socketPath, (error) => {
if (error) {
reject(error);
return;
}
const handler = (details, acceptConnection, rejectConnection) => {
if (details.socketPath === socketPath) {
onConnection === null || onConnection === void 0 ? void 0 : onConnection(details, acceptConnection, rejectConnection);
}
};
if (onConnection) {
connection.on('unix connection', handler);
}
const dispose = () => {
return new Promise((_resolve, _reject) => {
connection.off('unix connection', handler);
connection.openssh_unforwardInStreamLocal(socketPath, (_error) => {
if (_error) {
_reject(_error);
}
_resolve();
});
});
};
resolve({ dispose });
});
});
}
forwardOutStreamLocal(socketPath) {
const connection = this.getConnection();
return new Promise((resolve, reject) => {
connection.openssh_forwardOutStreamLocal(socketPath, (error, channel) => {
if (error) {
reject(error);
return;
}
resolve(channel);
});
});
}
dispose() {
if (this.connection) {
this.connection.end();
this.connection = null;
}
}
}
+85
View File
@@ -0,0 +1,85 @@
/// <reference types="node" />
/// <reference types="node" />
import SSH2, { AcceptConnection, Channel, ClientChannel, ConnectConfig, ExecOptions, Prompt, PseudoTtyOptions, RejectConnection, SFTPWrapper, ShellOptions, TcpConnectionDetails, TransferOptions, UNIXConnectionDetails } from 'ssh2';
import stream from 'stream';
export type Config = ConnectConfig & {
password?: string;
privateKey?: string;
privateKeyPath?: string;
tryKeyboard?: boolean;
onKeyboardInteractive?: (name: string, instructions: string, lang: string, prompts: Prompt[], finish: (responses: string[]) => void) => void;
};
export interface SSHExecCommandOptions {
cwd?: string;
stdin?: string | stream.Readable;
execOptions?: ExecOptions;
encoding?: BufferEncoding;
noTrim?: boolean;
onChannel?: (clientChannel: ClientChannel) => void;
onStdout?: (chunk: Buffer) => void;
onStderr?: (chunk: Buffer) => void;
}
export interface SSHExecCommandResponse {
stdout: string;
stderr: string;
code: number | null;
signal: string | null;
}
export interface SSHExecOptions extends SSHExecCommandOptions {
stream?: 'stdout' | 'stderr' | 'both';
}
export interface SSHPutFilesOptions {
sftp?: SFTPWrapper | null;
concurrency?: number;
transferOptions?: TransferOptions;
}
export interface SSHGetPutDirectoryOptions extends SSHPutFilesOptions {
tick?: (localFile: string, remoteFile: string, error: Error | null) => void;
validate?: (path: string) => boolean;
recursive?: boolean;
}
export type SSHMkdirMethod = 'sftp' | 'exec';
export type SSHForwardInListener = (details: TcpConnectionDetails, accept: AcceptConnection<ClientChannel>, reject: RejectConnection) => void;
export interface SSHForwardInDetails {
dispose(): Promise<void>;
port: number;
}
export type SSHForwardInStreamLocalListener = (info: UNIXConnectionDetails, accept: AcceptConnection, reject: RejectConnection) => void;
export interface SSHForwardInStreamLocalDetails {
dispose(): Promise<void>;
}
export declare class SSHError extends Error {
code: string | null;
constructor(message: string, code?: string | null);
}
export declare class NodeSSH {
connection: SSH2.Client | null;
private getConnection;
connect(givenConfig: Config): Promise<this>;
isConnected(): boolean;
requestShell(options?: PseudoTtyOptions | ShellOptions | false): Promise<ClientChannel>;
withShell(callback: (channel: ClientChannel) => Promise<void>, options?: PseudoTtyOptions | ShellOptions | false): Promise<void>;
requestSFTP(): Promise<SFTPWrapper>;
withSFTP(callback: (sftp: SFTPWrapper) => Promise<void>): Promise<void>;
execCommand(givenCommand: string, options?: SSHExecCommandOptions): Promise<SSHExecCommandResponse>;
exec(command: string, parameters: string[], options?: SSHExecOptions & {
stream?: 'stdout' | 'stderr';
}): Promise<string>;
exec(command: string, parameters: string[], options?: SSHExecOptions & {
stream: 'both';
}): Promise<SSHExecCommandResponse>;
mkdir(path: string, method?: SSHMkdirMethod, givenSftp?: SFTPWrapper | null): Promise<void>;
getFile(localFile: string, remoteFile: string, givenSftp?: SFTPWrapper | null, transferOptions?: TransferOptions | null): Promise<void>;
putFile(localFile: string, remoteFile: string, givenSftp?: SFTPWrapper | null, transferOptions?: TransferOptions | null): Promise<void>;
putFiles(files: {
local: string;
remote: string;
}[], { concurrency, sftp: givenSftp, transferOptions }?: SSHPutFilesOptions): Promise<void>;
putDirectory(localDirectory: string, remoteDirectory: string, { concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate, }?: SSHGetPutDirectoryOptions): Promise<boolean>;
getDirectory(localDirectory: string, remoteDirectory: string, { concurrency, sftp: givenSftp, transferOptions, recursive, tick, validate, }?: SSHGetPutDirectoryOptions): Promise<boolean>;
forwardIn(remoteAddr: string, remotePort: number, onConnection?: SSHForwardInListener): Promise<SSHForwardInDetails>;
forwardOut(srcIP: string, srcPort: number, dstIP: string, dstPort: number): Promise<Channel>;
forwardInStreamLocal(socketPath: string, onConnection?: SSHForwardInStreamLocalListener): Promise<SSHForwardInStreamLocalDetails>;
forwardOutStreamLocal(socketPath: string): Promise<Channel>;
dispose(): void;
}
+80
View File
@@ -0,0 +1,80 @@
{
"name": "node-ssh",
"version": "13.2.1",
"description": "SSH2 with Promises",
"main": "lib/cjs/index.js",
"typings": "lib/typings/index.d.ts",
"module": "lib/esm/index.mjs",
"exports": {
".": {
"types": "./lib/typings/index.d.ts",
"import": "./lib/esm/index.mjs",
"require": "./lib/cjs/index.js"
},
"./package.json": "./package.json"
},
"type": "commonjs",
"scripts": {
"test": "ava",
"lint": "(tsc -p . --noEmit) && (eslint . --ext .ts) && (prettier --list-different src/*.ts)",
"prepare": "yarn build:clean ; yarn build:cjs ; yarn build:esm ; yarn build:typings",
"build:clean": "rm -rf lib",
"build:esm": "tsc --module es2015 --target es2018 --outDir lib/esm && mv lib/esm/index.js lib/esm/index.mjs",
"build:cjs": "tsc --module commonjs --target es2018 --outDir lib/cjs",
"build:typings": "tsc --declaration --outDir lib/typings --emitDeclarationOnly"
},
"repository": {
"type": "git",
"url": "git+https://github.com/steelbrain/node-ssh.git"
},
"keywords": [
"ssh",
"ssh2",
"sftp"
],
"author": "steelbrain",
"license": "MIT",
"bugs": {
"url": "https://github.com/steelbrain/node-ssh/issues"
},
"files": [
"lib/*"
],
"homepage": "https://node-ssh.com",
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/node": "18",
"@types/shell-escape": "^0.2.1",
"@types/ssh2": "^1.11.13",
"ava": "^5.3.1",
"eslint-config-steelbrain": "^11.0.0",
"node-pty": "^1.0.0",
"ssh2": "^1.14.0",
"ssh2-streams": "^0.4.10",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"dependencies": {
"is-stream": "^2.0.0",
"make-dir": "^3.1.0",
"sb-promise-queue": "^2.1.0",
"sb-scandir": "^3.1.0",
"shell-escape": "^0.2.0",
"ssh2": "^1.14.0"
},
"ava": {
"files": [
"test/*-test.ts"
],
"extensions": [
"ts"
],
"require": [
"ts-node/register/transpile-only"
]
},
"engines": {
"node": ">= 10"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}