ssh-client-wrapper 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -2
- package/lib/index.d.ts +8 -2
- package/lib/index.js +59 -5
- package/lib/rsyncExec.js +78 -5
- package/lib/util.js +44 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -60,6 +60,10 @@ Creates a new SSH client instance.
|
|
|
60
60
|
* `keyFile` (string, optional): The path to the private key file for key-based authentication.
|
|
61
61
|
* `passphrase` (string | Function, optional): The passphrase for the private key, or a function that returns it.
|
|
62
62
|
* `noStrictHostKeyChecking` (boolean, optional): If `true`, bypasses strict host key checking.
|
|
63
|
+
* `maxRetry` (number, optional): Max number of retries for a retryable error. Defaults to `3`.
|
|
64
|
+
* `retryDuration` (number, optional): Time (msec) to wait between each retry. Defaults to `1000`.
|
|
65
|
+
* `retryableExitCodes` (number[], optional): Additional rsync exit codes to treat as retryable for `.send()`/`.recv()`. See [Retry behavior for `.send()` / `.recv()`](#retry-behavior-for-send--recv) below.
|
|
66
|
+
* `replaceRetryableExitCodes` (boolean, optional): If `true`, `retryableExitCodes` replaces the built-in retryable exit code list instead of adding to it.
|
|
63
67
|
* And more... see `lib/index.js` for all available options.
|
|
64
68
|
|
|
65
69
|
### `.exec(cmd, [timeout], [outputCallback], [rcfile], [prependCmd])`
|
|
@@ -82,20 +86,91 @@ Executes a command and interacts with it, similar to the `expect` tool.
|
|
|
82
86
|
|
|
83
87
|
* Returns: `Promise<number>` - The return code of the command.
|
|
84
88
|
|
|
85
|
-
### `.send(src, dst, [opt], [timeout])`
|
|
89
|
+
### `.send(src, dst, [opt], [timeout], [retryableExitCodes], [replaceRetryableExitCodes])`
|
|
86
90
|
Uploads files or directories to the remote host using `rsync`.
|
|
87
91
|
|
|
88
92
|
* `src` (string[]): An array of local paths to send.
|
|
89
93
|
* `dst` (string): The remote destination path.
|
|
94
|
+
* `retryableExitCodes` (number[], optional): Per-call override of retryable rsync exit codes for this call only (takes precedence over `hostInfo.retryableExitCodes`). See [Retry behavior for `.send()` / `.recv()`](#retry-behavior-for-send--recv).
|
|
95
|
+
* `replaceRetryableExitCodes` (boolean, optional): If `true`, `retryableExitCodes` replaces the built-in default instead of adding to it. Defaults to `false`.
|
|
90
96
|
* Returns: `Promise<void>`
|
|
91
97
|
|
|
92
|
-
### `.recv(src, dst, [opt], [timeout])`
|
|
98
|
+
### `.recv(src, dst, [opt], [timeout], [retryableExitCodes], [replaceRetryableExitCodes])`
|
|
93
99
|
Downloads files or directories from the remote host using `rsync`.
|
|
94
100
|
|
|
95
101
|
* `src` (string[]): An array of remote paths to receive.
|
|
96
102
|
* `dst` (string): The local destination path.
|
|
103
|
+
* `retryableExitCodes` (number[], optional): Per-call override of retryable rsync exit codes for this call only (takes precedence over `hostInfo.retryableExitCodes`). See [Retry behavior for `.send()` / `.recv()`](#retry-behavior-for-send--recv).
|
|
104
|
+
* `replaceRetryableExitCodes` (boolean, optional): If `true`, `retryableExitCodes` replaces the built-in default instead of adding to it. Defaults to `false`.
|
|
97
105
|
* Returns: `Promise<void>`
|
|
98
106
|
|
|
107
|
+
### Retry behavior for `.send()` / `.recv()`
|
|
108
|
+
|
|
109
|
+
`.send()` and `.recv()` automatically retry (up to `hostInfo.maxRetry` times, default `3`, waiting `hostInfo.retryDuration` msec between attempts, default `1000`) when:
|
|
110
|
+
|
|
111
|
+
* the underlying `rsync` process exits with one of the built-in retryable exit codes: `10, 11, 12, 13, 14` (I/O and protocol errors that are commonly transient), or
|
|
112
|
+
* the SSH connection fails with a transient `kex_exchange_identification` error.
|
|
113
|
+
|
|
114
|
+
If your environment sees other rsync exit codes that should also be retried (for example on a shared HPC filesystem where a "partial transfer" exit code can occur while output files are still being flushed), you can add to or replace the retryable list without changing the library's default, at two levels:
|
|
115
|
+
|
|
116
|
+
* **Per instance**, via `hostInfo.retryableExitCodes` / `hostInfo.replaceRetryableExitCodes` — applies to every `.send()`/`.recv()` call made through that `SshClientWrapper`.
|
|
117
|
+
* **Per call**, via the trailing `retryableExitCodes` / `replaceRetryableExitCodes` arguments on `.send()`/`.recv()` — applies only to that call, and fully overrides the instance-level setting if provided.
|
|
118
|
+
|
|
119
|
+
By default (`replaceRetryableExitCodes: false`), `retryableExitCodes` is **added** to the built-in list — the common case, since it doesn't require knowing or repeating the built-in codes. Set `replaceRetryableExitCodes: true` to use `retryableExitCodes` verbatim instead (pass `[]` to disable exit-code-based retry entirely).
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// add exit codes 23 and 24 to the built-in retryable list, for every send/recv on this instance
|
|
123
|
+
const ssh = new SshClientWrapper({
|
|
124
|
+
host: 'remote.server.com',
|
|
125
|
+
user: 'username',
|
|
126
|
+
password: 'password',
|
|
127
|
+
retryableExitCodes: [23, 24]
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// or, just for a single call:
|
|
131
|
+
await ssh.recv(['/remote/output/*'], './local-output', [], 0, [23, 24]);
|
|
132
|
+
|
|
133
|
+
// full replace: only retry on 23 for this call
|
|
134
|
+
await ssh.recv(['/remote/output/*'], './local-output', [], 0, [23], true);
|
|
135
|
+
|
|
136
|
+
// disable exit-code-based retry entirely for this call
|
|
137
|
+
await ssh.recv(['/remote/output/*'], './local-output', [], 0, [], true);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `.remoteToRemoteCopy(src, dstHostInfo, dst, [opt], [timeout])`
|
|
141
|
+
Copies files or directories directly from this remote host to another remote host using `rsync` with SSH agent forwarding.
|
|
142
|
+
|
|
143
|
+
* `src` (string[]): An array of file or directory paths on this (source) remote host.
|
|
144
|
+
* `dstHostInfo` (Object): Destination host information (requires `host` property; optionally `user`, `port`, `noStrictHostKeyChecking`).
|
|
145
|
+
* `dst` (string): Destination path on the destination remote host.
|
|
146
|
+
* `opt` (string[], optional): Additional options for rsync (e.g., `["--exclude=*.log"]`).
|
|
147
|
+
* `timeout` (number, optional): Timeout in seconds.
|
|
148
|
+
* Returns: `Promise<void>`
|
|
149
|
+
|
|
150
|
+
**Note:** This method requires SSH agent forwarding to be enabled. Your SSH key must be loaded in the SSH agent on localhost, and the connection to the source host must forward the agent. No password or keyFile is needed for the source → destination connection as it uses the forwarded SSH agent.
|
|
151
|
+
|
|
152
|
+
**Example:**
|
|
153
|
+
```javascript
|
|
154
|
+
const srcHost = new SshClientWrapper({
|
|
155
|
+
host: 'source.server.com',
|
|
156
|
+
user: 'user1',
|
|
157
|
+
keyFile: '/path/to/key'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const dstHostInfo = {
|
|
161
|
+
host: 'destination.server.com',
|
|
162
|
+
user: 'user2'
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Copy files directly from source to destination
|
|
166
|
+
await srcHost.remoteToRemoteCopy(
|
|
167
|
+
['/remote/path/file.txt', '/remote/path/dir/'],
|
|
168
|
+
dstHostInfo,
|
|
169
|
+
'/destination/path/',
|
|
170
|
+
['--exclude=*.tmp']
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
99
174
|
### `.canConnect([timeout])`
|
|
100
175
|
Checks if a connection to the remote host can be established.
|
|
101
176
|
|
package/lib/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ declare module "ssh-client-wrapper" {
|
|
|
11
11
|
ConnectTimeout?: number;
|
|
12
12
|
maxRetry?: number;
|
|
13
13
|
retryDuration?: number;
|
|
14
|
+
retryableExitCodes?: number[];
|
|
15
|
+
replaceRetryableExitCodes?: boolean;
|
|
14
16
|
rcfile?: string;
|
|
15
17
|
prependCmd?: string;
|
|
16
18
|
sshOpt?: string[];
|
|
@@ -45,13 +47,17 @@ declare module "ssh-client-wrapper" {
|
|
|
45
47
|
src: string[],
|
|
46
48
|
dst: string,
|
|
47
49
|
opt?: string[],
|
|
48
|
-
timeout?: number
|
|
50
|
+
timeout?: number,
|
|
51
|
+
retryableExitCodes?: number[],
|
|
52
|
+
replaceRetryableExitCodes?: boolean
|
|
49
53
|
): Promise<void>;
|
|
50
54
|
recv(
|
|
51
55
|
src: string[],
|
|
52
56
|
dst: string,
|
|
53
57
|
opt?: string[],
|
|
54
|
-
timeout?: number
|
|
58
|
+
timeout?: number,
|
|
59
|
+
retryableExitCodes?: number[],
|
|
60
|
+
replaceRetryableExitCodes?: boolean
|
|
55
61
|
): Promise<void>;
|
|
56
62
|
canConnect(timeout?: number): Promise<boolean>;
|
|
57
63
|
disconnect(): Promise<void>;
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import Debug from "debug";
|
|
4
4
|
import * as sshExecModule from "./sshExec.js";
|
|
5
5
|
import * as rsyncExecModule from "./rsyncExec.js";
|
|
6
|
-
import { isArrayOfString, sanityCheck } from "./util.js";
|
|
6
|
+
import { isArrayOfString, isArrayOfInteger, sanityCheck } from "./util.js";
|
|
7
7
|
|
|
8
8
|
const debug = Debug("sshClientWrapper:debug:interface");
|
|
9
9
|
const debugVerbose = Debug("sshClientWrapper:verbose:interface");
|
|
@@ -30,6 +30,8 @@ const debugVerbose = Debug("sshClientWrapper:verbose:interface");
|
|
|
30
30
|
* @param {Integer} hostInfo.ConnectTimeout - timeout used when connectiong to ths SSH server
|
|
31
31
|
* @param {Integer} hostInfo.maxRetry - max number of retry
|
|
32
32
|
* @param {Integer} hostInfo.retryDuration - duration time (msec) between each retry
|
|
33
|
+
* @param {number[]} hostInfo.retryableExitCodes - rsync exit codes to treat as retryable for send/recv, in addition to the built-in default (unless hostInfo.replaceRetryableExitCodes is true)
|
|
34
|
+
* @param {boolean} hostInfo.replaceRetryableExitCodes - if true, hostInfo.retryableExitCodes replaces the built-in default set of retryable rsync exit codes instead of merging with it
|
|
33
35
|
* @param {string} hostInfo.rcfile - rcfile path which will be sourced before executing actual command
|
|
34
36
|
* @param {string} hostInfo.prependCmd - command string which will be executed before executing actual command
|
|
35
37
|
* @param {string[]} hostInfo.sshOpt - additional options for ssh
|
|
@@ -149,9 +151,11 @@ class SshClientWrapper {
|
|
|
149
151
|
* @param {string} dst - Destination path.
|
|
150
152
|
* @param {string[]} opt - option for rsync
|
|
151
153
|
* @param {Integer} timeout - timeout in secconds
|
|
154
|
+
* @param {number[]} retryableExitCodes - rsync exit codes to treat as retryable for this call (overrides hostInfo.retryableExitCodes)
|
|
155
|
+
* @param {boolean} replaceRetryableExitCodes - if true, retryableExitCodes replaces the default set instead of merging with it
|
|
152
156
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
153
157
|
*/
|
|
154
|
-
async send(src, dst, opt = [], timeout = 0) {
|
|
158
|
+
async send(src, dst, opt = [], timeout = 0, retryableExitCodes = null, replaceRetryableExitCodes = false) {
|
|
155
159
|
debug("send called", src, dst, opt);
|
|
156
160
|
|
|
157
161
|
if (!isArrayOfString(src)) {
|
|
@@ -171,7 +175,13 @@ class SshClientWrapper {
|
|
|
171
175
|
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
172
176
|
return logAndReject("opt must be array of string");
|
|
173
177
|
}
|
|
174
|
-
|
|
178
|
+
if (retryableExitCodes !== null && !isArrayOfInteger(retryableExitCodes)) {
|
|
179
|
+
return logAndReject("retryableExitCodes must be array of integer");
|
|
180
|
+
}
|
|
181
|
+
if (typeof replaceRetryableExitCodes !== "boolean") {
|
|
182
|
+
return logAndReject("replaceRetryableExitCodes must be boolean");
|
|
183
|
+
}
|
|
184
|
+
return this.deps.send(this.hostInfo, src, dst, opt, timeout, retryableExitCodes, replaceRetryableExitCodes);
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
/**
|
|
@@ -180,9 +190,11 @@ class SshClientWrapper {
|
|
|
180
190
|
* @param {string} dst - Destination path.
|
|
181
191
|
* @param {string[]} opt - option for rsync
|
|
182
192
|
* @param {Integer} timeout - timeout in secconds
|
|
193
|
+
* @param {number[]} retryableExitCodes - rsync exit codes to treat as retryable for this call (overrides hostInfo.retryableExitCodes)
|
|
194
|
+
* @param {boolean} replaceRetryableExitCodes - if true, retryableExitCodes replaces the default set instead of merging with it
|
|
183
195
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
184
196
|
*/
|
|
185
|
-
async recv(src, dst, opt = [], timeout = 0) {
|
|
197
|
+
async recv(src, dst, opt = [], timeout = 0, retryableExitCodes = null, replaceRetryableExitCodes = false) {
|
|
186
198
|
debug("recv called", src, dst, opt);
|
|
187
199
|
|
|
188
200
|
if (!isArrayOfString(src)) {
|
|
@@ -202,7 +214,13 @@ class SshClientWrapper {
|
|
|
202
214
|
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
203
215
|
return logAndReject("opt must be array of string");
|
|
204
216
|
}
|
|
205
|
-
|
|
217
|
+
if (retryableExitCodes !== null && !isArrayOfInteger(retryableExitCodes)) {
|
|
218
|
+
return logAndReject("retryableExitCodes must be array of integer");
|
|
219
|
+
}
|
|
220
|
+
if (typeof replaceRetryableExitCodes !== "boolean") {
|
|
221
|
+
return logAndReject("replaceRetryableExitCodes must be boolean");
|
|
222
|
+
}
|
|
223
|
+
return this.deps.recv(this.hostInfo, src, dst, opt, timeout, retryableExitCodes, replaceRetryableExitCodes);
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
/**
|
|
@@ -220,6 +238,42 @@ class SshClientWrapper {
|
|
|
220
238
|
return this.deps.canConnect(this.hostInfo, timeout);
|
|
221
239
|
}
|
|
222
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Copy files directly from this remote host to another remote host using SSH agent forwarding.
|
|
243
|
+
* @param {string[]} src - File or directory paths on this (source) remote host.
|
|
244
|
+
* @param {object} dstHostInfo - Destination host information (needs host and user properties).
|
|
245
|
+
* @param {string} dst - Destination path on the destination remote host.
|
|
246
|
+
* @param {string[]} opt - options for rsync
|
|
247
|
+
* @param {Integer} timeout - timeout in seconds
|
|
248
|
+
* @returns {Promise} - Resolved when file transfer is done.
|
|
249
|
+
*
|
|
250
|
+
* Note: This requires SSH agent forwarding to be available.
|
|
251
|
+
* The SSH agent on localhost must be forwarded to the source host,
|
|
252
|
+
* and then forwarded again to the destination host.
|
|
253
|
+
*/
|
|
254
|
+
async remoteToRemoteCopy(src, dstHostInfo, dst, opt = [], timeout = 0) {
|
|
255
|
+
debug("remoteToRemoteCopy called", src, "to", dstHostInfo.host, dst);
|
|
256
|
+
|
|
257
|
+
if (!isArrayOfString(src)) {
|
|
258
|
+
return logAndReject("src must be array of string");
|
|
259
|
+
}
|
|
260
|
+
if (!src.some((e)=>{
|
|
261
|
+
return e !== "";
|
|
262
|
+
})) {
|
|
263
|
+
return logAndReject("src must contain non-empty string");
|
|
264
|
+
}
|
|
265
|
+
if (typeof dstHostInfo !== "object" || !dstHostInfo.host) {
|
|
266
|
+
return logAndReject("dstHostInfo must be object with host property");
|
|
267
|
+
}
|
|
268
|
+
if (typeof dst !== "string" || dst === "") {
|
|
269
|
+
return logAndReject("dst must be non-empty string");
|
|
270
|
+
}
|
|
271
|
+
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
272
|
+
return logAndReject("opt must be array of string");
|
|
273
|
+
}
|
|
274
|
+
return this.deps.remoteToRemoteCopy(this.hostInfo, src, dstHostInfo, dst, opt, timeout);
|
|
275
|
+
}
|
|
276
|
+
|
|
223
277
|
/**
|
|
224
278
|
* Disconnect master session
|
|
225
279
|
*/
|
package/lib/rsyncExec.js
CHANGED
|
@@ -6,7 +6,7 @@ import { glob, hasMagic } from "glob";
|
|
|
6
6
|
import { connect, retryWrapper, sshExec } from "./sshExec.js";
|
|
7
7
|
import { fork } from "./fork.js";
|
|
8
8
|
|
|
9
|
-
import { sshCmd, rsyncCmd,
|
|
9
|
+
import { sshCmd, rsyncCmd, getSshOption, resolveRetryableExitCodes } from "./util.js";
|
|
10
10
|
|
|
11
11
|
const debug = Debug("sshClientWrapper:debug:rsyncExec");
|
|
12
12
|
const debugVerbose = Debug("sshClientWrapper:verbose:rsyncExec");
|
|
@@ -32,10 +32,12 @@ export async function checkRsyncVersion(timeout) {
|
|
|
32
32
|
* @param {string} dst - Destination path.
|
|
33
33
|
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
34
34
|
* @param {Integer} timeout - timeout in secconds
|
|
35
|
+
* @param {number[]} retryableExitCodes - rsync exit codes to treat as retryable for this call (overrides hostInfo-level setting)
|
|
36
|
+
* @param {boolean} replaceRetryableExitCodes - if true, retryableExitCodes replaces the default instead of merging with it
|
|
35
37
|
*
|
|
36
38
|
* this function execute rsync -avv --copy-unsafe-links src rsync://[user@]host[:port]/dst
|
|
37
39
|
*/
|
|
38
|
-
export async function send(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
40
|
+
export async function send(hostInfo, argSrc, dst, rsyncOpt, timeout, retryableExitCodes, replaceRetryableExitCodes) {
|
|
39
41
|
await connect(hostInfo);
|
|
40
42
|
const promises = await Promise.allSettled(argSrc.map((e)=>{
|
|
41
43
|
return glob(e);
|
|
@@ -81,7 +83,8 @@ export async function send(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
81
83
|
}
|
|
82
84
|
debug(`send ${src} to ${hostInfo.host}:${dst}`);
|
|
83
85
|
debugVerbose(`send: ${rsyncCmd} ${args.join(" ")}`);
|
|
84
|
-
|
|
86
|
+
const exitCodes = resolveRetryableExitCodes(hostInfo, retryableExitCodes, replaceRetryableExitCodes);
|
|
87
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout, null, exitCodes), hostInfo);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -106,10 +109,12 @@ async function isNewRsync(hostInfo) {
|
|
|
106
109
|
* @param {string} dst - Destination path.
|
|
107
110
|
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
108
111
|
* @param {Integer} timeout - timeout in secconds
|
|
112
|
+
* @param {number[]} retryableExitCodes - rsync exit codes to treat as retryable for this call (overrides hostInfo-level setting)
|
|
113
|
+
* @param {boolean} replaceRetryableExitCodes - if true, retryableExitCodes replaces the default instead of merging with it
|
|
109
114
|
*
|
|
110
115
|
* this function execute rsync -av --copy-unsafe-links rsync://[user@]host[:port]/src dst
|
|
111
116
|
*/
|
|
112
|
-
export async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
117
|
+
export async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout, retryableExitCodes, replaceRetryableExitCodes) {
|
|
113
118
|
await connect(hostInfo);
|
|
114
119
|
const src = `${hostInfo.host}:${argSrc.join(" ")}`;
|
|
115
120
|
const sshArgs = getSshOption(hostInfo, true);
|
|
@@ -125,5 +130,73 @@ export async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
125
130
|
|
|
126
131
|
debug(`recv ${src} to ${dst}`);
|
|
127
132
|
debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
|
|
128
|
-
|
|
133
|
+
const exitCodes = resolveRetryableExitCodes(hostInfo, retryableExitCodes, replaceRetryableExitCodes);
|
|
134
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout, null, exitCodes), hostInfo);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Copy files directly between remote hosts using rsync over SSH with agent forwarding.
|
|
139
|
+
* This function executes rsync on the source remote host to transfer files directly
|
|
140
|
+
* to the destination remote host, avoiding the need to download and upload through localhost.
|
|
141
|
+
* @param {object} srcHostInfo - source host information object
|
|
142
|
+
* @param {string[]} argSrc - File or directory paths on the source remote host
|
|
143
|
+
* @param {object} dstHostInfo - destination host information object (needs host and user properties)
|
|
144
|
+
* @param {string} dst - Destination path on the destination remote host
|
|
145
|
+
* @param {string[]} rsyncOpt - optional arguments for rsync
|
|
146
|
+
* @param {Integer} timeout - timeout in seconds
|
|
147
|
+
* @returns {Promise} - Resolved when transfer is complete
|
|
148
|
+
*
|
|
149
|
+
* Authentication from source to destination uses SSH agent forwarding,
|
|
150
|
+
* so dstHostInfo does not need keyFile or password.
|
|
151
|
+
*/
|
|
152
|
+
export async function remoteToRemoteCopy(srcHostInfo, argSrc, dstHostInfo, dst, rsyncOpt, timeout) {
|
|
153
|
+
await connect(srcHostInfo);
|
|
154
|
+
|
|
155
|
+
//Construct destination specification for rsync: user@host:path
|
|
156
|
+
let dstSpec = dstHostInfo.host;
|
|
157
|
+
if (dstHostInfo.user) {
|
|
158
|
+
dstSpec = `${dstHostInfo.user}@${dstHostInfo.host}`;
|
|
159
|
+
}
|
|
160
|
+
dstSpec = `${dstSpec}:${dst}`;
|
|
161
|
+
|
|
162
|
+
//Build SSH options for the nested SSH connection (from src to dst)
|
|
163
|
+
const nestedSshArgs = ["-A"]; //Enable agent forwarding
|
|
164
|
+
|
|
165
|
+
if (dstHostInfo.port) {
|
|
166
|
+
nestedSshArgs.push("-p", dstHostInfo.port.toString());
|
|
167
|
+
}
|
|
168
|
+
if (dstHostInfo.noStrictHostKeyChecking) {
|
|
169
|
+
nestedSshArgs.push("-oStrictHostKeyChecking=no");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//Build rsync command to be executed on the source host
|
|
173
|
+
const rshOpt = `-e '${sshCmd} ${nestedSshArgs.join(" ")}'`;
|
|
174
|
+
const rsyncOptStr = rsyncOpt.length > 0 ? rsyncOpt.join(" ") : "";
|
|
175
|
+
const srcPaths = argSrc.join(" ");
|
|
176
|
+
|
|
177
|
+
//Create destination directory on the destination host
|
|
178
|
+
let remoteMkdirCmd = "";
|
|
179
|
+
if (dst.endsWith("/")) {
|
|
180
|
+
remoteMkdirCmd = `mkdir -p ${dst}`;
|
|
181
|
+
} else {
|
|
182
|
+
const { dir } = path.parse(dst);
|
|
183
|
+
if (dir !== "" && dir !== "/") {
|
|
184
|
+
remoteMkdirCmd = `mkdir -p ${dir}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (remoteMkdirCmd) {
|
|
189
|
+
//Execute mkdir on destination host via source host with agent forwarding
|
|
190
|
+
const dstUser = dstHostInfo.user ? `${dstHostInfo.user}@` : "";
|
|
191
|
+
const mkdirSshCmd = `${sshCmd} -A ${nestedSshArgs.filter(arg=>{return arg !== "-A";}).join(" ")} ${dstUser}${dstHostInfo.host} '${remoteMkdirCmd}'`;
|
|
192
|
+
debug(`creating remote directory: ${mkdirSshCmd}`);
|
|
193
|
+
await sshExec(srcHostInfo, mkdirSshCmd, timeout, debugVerbose);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//Execute rsync on source host
|
|
197
|
+
const rsyncRemoteCmd = `rsync -avv --copy-unsafe-links ${rshOpt} ${rsyncOptStr} ${srcPaths} ${dstSpec}`;
|
|
198
|
+
debug(`remote to remote copy: ${rsyncRemoteCmd}`);
|
|
199
|
+
debugVerbose(`executing on ${srcHostInfo.host}: ${rsyncRemoteCmd}`);
|
|
200
|
+
|
|
201
|
+
return sshExec(srcHostInfo, rsyncRemoteCmd, timeout, debugVerbose);
|
|
129
202
|
}
|
package/lib/util.js
CHANGED
|
@@ -42,6 +42,11 @@ const hostInfoSchema = {
|
|
|
42
42
|
ControlPersistDir: { type: "string", pattern: "\\S+", transform: ["trim"] },
|
|
43
43
|
maxRetry: { type: "number", minimum: 0, default: 3 },
|
|
44
44
|
retryDuration: { type: "number", minimum: 0, default: 1000 },
|
|
45
|
+
retryableExitCodes: {
|
|
46
|
+
type: "array",
|
|
47
|
+
items: { type: "integer" }
|
|
48
|
+
},
|
|
49
|
+
replaceRetryableExitCodes: { type: "boolean" },
|
|
45
50
|
rcfile: { type: "string", pattern: "\\S+", transform: ["trim"] },
|
|
46
51
|
prependCmd: { type: "string", pattern: "\\S+", transform: ["trim"] },
|
|
47
52
|
sshOpt: {
|
|
@@ -201,6 +206,12 @@ export const getSshOption = (hostInfo, withoutDestination)=>{
|
|
|
201
206
|
if (hostInfo.noStrictHostKeyChecking) {
|
|
202
207
|
args.push("-oStrictHostKeyChecking=no");
|
|
203
208
|
}
|
|
209
|
+
|
|
210
|
+
//Enable SSH agent forwarding if keyFile is set (indicates we're using key-based auth)
|
|
211
|
+
if (hostInfo.keyFile) {
|
|
212
|
+
args.push("-A");
|
|
213
|
+
}
|
|
214
|
+
|
|
204
215
|
const controlPersist = Number.isInteger(hostInfo.ControlPersist) && hostInfo.ControlPersist >= 0 ? hostInfo.ControlPersist : "180";
|
|
205
216
|
const controlPersistDir = getControlPersistDir(hostInfo);
|
|
206
217
|
args.push("-oControlMaster=auto");
|
|
@@ -229,6 +240,39 @@ export const isArrayOfString = (target)=>{
|
|
|
229
240
|
});
|
|
230
241
|
};
|
|
231
242
|
|
|
243
|
+
export const isArrayOfInteger = (target)=>{
|
|
244
|
+
if (!Array.isArray(target)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return !target.some((e)=>{
|
|
248
|
+
//target has non integer member
|
|
249
|
+
return !Number.isInteger(e);
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* resolve which rsync exit codes should be treated as retryable for a single send/recv call
|
|
255
|
+
* @param {object} hostInfo - hostinfo object
|
|
256
|
+
* @param {number[]} methodCodes - retryableExitCodes argument passed directly to send/recv (highest priority)
|
|
257
|
+
* @param {boolean} methodReplace - replaceRetryableExitCodes argument passed directly to send/recv
|
|
258
|
+
* @returns {number[]} - exit codes which should be treated as retryable
|
|
259
|
+
*
|
|
260
|
+
* precedence: method-level argument (if given) fully supersedes hostInfo-level settings.
|
|
261
|
+
* at whichever level is used, replace=false (default) merges with acceptableRsyncRetrunCodes,
|
|
262
|
+
* replace=true uses the given codes verbatim (so [] disables exit-code-based retry entirely).
|
|
263
|
+
*/
|
|
264
|
+
export const resolveRetryableExitCodes = (hostInfo, methodCodes, methodReplace)=>{
|
|
265
|
+
if (methodCodes != null) {
|
|
266
|
+
return methodReplace ? methodCodes : [...acceptableRsyncRetrunCodes, ...methodCodes];
|
|
267
|
+
}
|
|
268
|
+
if (hostInfo.retryableExitCodes != null) {
|
|
269
|
+
return hostInfo.replaceRetryableExitCodes
|
|
270
|
+
? hostInfo.retryableExitCodes
|
|
271
|
+
: [...acceptableRsyncRetrunCodes, ...hostInfo.retryableExitCodes];
|
|
272
|
+
}
|
|
273
|
+
return acceptableRsyncRetrunCodes;
|
|
274
|
+
};
|
|
275
|
+
|
|
232
276
|
/**
|
|
233
277
|
* log and send message to pty
|
|
234
278
|
* @param {object} pty - pty object which message will be send to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh-client-wrapper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "wrapper library for open ssh client and rsync",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ssh",
|
|
@@ -34,11 +34,14 @@
|
|
|
34
34
|
"lint-staged": {
|
|
35
35
|
"*.js": "eslint --fix"
|
|
36
36
|
},
|
|
37
|
+
"overrides": {
|
|
38
|
+
"@semantic-release/npm": "^13.1.0"
|
|
39
|
+
},
|
|
37
40
|
"dependencies": {
|
|
38
41
|
"ajv": "^8.17.1",
|
|
39
42
|
"ajv-keywords": "^5.1.0",
|
|
40
43
|
"debug": "^4.4.3",
|
|
41
|
-
"glob": "^
|
|
44
|
+
"glob": "^12.0.0",
|
|
42
45
|
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
@@ -46,11 +49,12 @@
|
|
|
46
49
|
"@semantic-release/changelog": "^6.0.3",
|
|
47
50
|
"@semantic-release/git": "^10.0.1",
|
|
48
51
|
"@semantic-release/github": "^11.0.4",
|
|
49
|
-
"@semantic-release/npm": "^
|
|
52
|
+
"@semantic-release/npm": "^13.1.0",
|
|
50
53
|
"@stylistic/eslint-plugin": "^5.2.3",
|
|
51
54
|
"c8": "^10.1.3",
|
|
52
55
|
"chai": "^5.3.1",
|
|
53
56
|
"chai-as-promised": "^8.0.1",
|
|
57
|
+
"eslint-plugin-chai-expect": "^3.1.0",
|
|
54
58
|
"eslint-plugin-chai-friendly": "^1.1.0",
|
|
55
59
|
"eslint-plugin-jsdoc": "^54.1.0",
|
|
56
60
|
"eslint-plugin-node": "^11.1.0",
|