ssh-client-wrapper 2.14.0 → 2.15.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 +34 -0
- package/lib/index.js +36 -0
- package/lib/rsyncExec.js +67 -0
- package/lib/util.js +6 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -96,6 +96,40 @@ Downloads files or directories from the remote host using `rsync`.
|
|
|
96
96
|
* `dst` (string): The local destination path.
|
|
97
97
|
* Returns: `Promise<void>`
|
|
98
98
|
|
|
99
|
+
### `.remoteToRemoteCopy(src, dstHostInfo, dst, [opt], [timeout])`
|
|
100
|
+
Copies files or directories directly from this remote host to another remote host using `rsync` with SSH agent forwarding.
|
|
101
|
+
|
|
102
|
+
* `src` (string[]): An array of file or directory paths on this (source) remote host.
|
|
103
|
+
* `dstHostInfo` (Object): Destination host information (requires `host` property; optionally `user`, `port`, `noStrictHostKeyChecking`).
|
|
104
|
+
* `dst` (string): Destination path on the destination remote host.
|
|
105
|
+
* `opt` (string[], optional): Additional options for rsync (e.g., `["--exclude=*.log"]`).
|
|
106
|
+
* `timeout` (number, optional): Timeout in seconds.
|
|
107
|
+
* Returns: `Promise<void>`
|
|
108
|
+
|
|
109
|
+
**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.
|
|
110
|
+
|
|
111
|
+
**Example:**
|
|
112
|
+
```javascript
|
|
113
|
+
const srcHost = new SshClientWrapper({
|
|
114
|
+
host: 'source.server.com',
|
|
115
|
+
user: 'user1',
|
|
116
|
+
keyFile: '/path/to/key'
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const dstHostInfo = {
|
|
120
|
+
host: 'destination.server.com',
|
|
121
|
+
user: 'user2'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Copy files directly from source to destination
|
|
125
|
+
await srcHost.remoteToRemoteCopy(
|
|
126
|
+
['/remote/path/file.txt', '/remote/path/dir/'],
|
|
127
|
+
dstHostInfo,
|
|
128
|
+
'/destination/path/',
|
|
129
|
+
['--exclude=*.tmp']
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
99
133
|
### `.canConnect([timeout])`
|
|
100
134
|
Checks if a connection to the remote host can be established.
|
|
101
135
|
|
package/lib/index.js
CHANGED
|
@@ -220,6 +220,42 @@ class SshClientWrapper {
|
|
|
220
220
|
return this.deps.canConnect(this.hostInfo, timeout);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Copy files directly from this remote host to another remote host using SSH agent forwarding.
|
|
225
|
+
* @param {string[]} src - File or directory paths on this (source) remote host.
|
|
226
|
+
* @param {object} dstHostInfo - Destination host information (needs host and user properties).
|
|
227
|
+
* @param {string} dst - Destination path on the destination remote host.
|
|
228
|
+
* @param {string[]} opt - options for rsync
|
|
229
|
+
* @param {Integer} timeout - timeout in seconds
|
|
230
|
+
* @returns {Promise} - Resolved when file transfer is done.
|
|
231
|
+
*
|
|
232
|
+
* Note: This requires SSH agent forwarding to be available.
|
|
233
|
+
* The SSH agent on localhost must be forwarded to the source host,
|
|
234
|
+
* and then forwarded again to the destination host.
|
|
235
|
+
*/
|
|
236
|
+
async remoteToRemoteCopy(src, dstHostInfo, dst, opt = [], timeout = 0) {
|
|
237
|
+
debug("remoteToRemoteCopy called", src, "to", dstHostInfo.host, dst);
|
|
238
|
+
|
|
239
|
+
if (!isArrayOfString(src)) {
|
|
240
|
+
return logAndReject("src must be array of string");
|
|
241
|
+
}
|
|
242
|
+
if (!src.some((e)=>{
|
|
243
|
+
return e !== "";
|
|
244
|
+
})) {
|
|
245
|
+
return logAndReject("src must contain non-empty string");
|
|
246
|
+
}
|
|
247
|
+
if (typeof dstHostInfo !== "object" || !dstHostInfo.host) {
|
|
248
|
+
return logAndReject("dstHostInfo must be object with host property");
|
|
249
|
+
}
|
|
250
|
+
if (typeof dst !== "string" || dst === "") {
|
|
251
|
+
return logAndReject("dst must be non-empty string");
|
|
252
|
+
}
|
|
253
|
+
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
254
|
+
return logAndReject("opt must be array of string");
|
|
255
|
+
}
|
|
256
|
+
return this.deps.remoteToRemoteCopy(this.hostInfo, src, dstHostInfo, dst, opt, timeout);
|
|
257
|
+
}
|
|
258
|
+
|
|
223
259
|
/**
|
|
224
260
|
* Disconnect master session
|
|
225
261
|
*/
|
package/lib/rsyncExec.js
CHANGED
|
@@ -127,3 +127,70 @@ export async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
127
127
|
debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
|
|
128
128
|
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout, null, acceptableRsyncRetrunCodes), hostInfo);
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Copy files directly between remote hosts using rsync over SSH with agent forwarding.
|
|
133
|
+
* This function executes rsync on the source remote host to transfer files directly
|
|
134
|
+
* to the destination remote host, avoiding the need to download and upload through localhost.
|
|
135
|
+
* @param {object} srcHostInfo - source host information object
|
|
136
|
+
* @param {string[]} argSrc - File or directory paths on the source remote host
|
|
137
|
+
* @param {object} dstHostInfo - destination host information object (needs host and user properties)
|
|
138
|
+
* @param {string} dst - Destination path on the destination remote host
|
|
139
|
+
* @param {string[]} rsyncOpt - optional arguments for rsync
|
|
140
|
+
* @param {Integer} timeout - timeout in seconds
|
|
141
|
+
* @returns {Promise} - Resolved when transfer is complete
|
|
142
|
+
*
|
|
143
|
+
* Authentication from source to destination uses SSH agent forwarding,
|
|
144
|
+
* so dstHostInfo does not need keyFile or password.
|
|
145
|
+
*/
|
|
146
|
+
export async function remoteToRemoteCopy(srcHostInfo, argSrc, dstHostInfo, dst, rsyncOpt, timeout) {
|
|
147
|
+
await connect(srcHostInfo);
|
|
148
|
+
|
|
149
|
+
//Construct destination specification for rsync: user@host:path
|
|
150
|
+
let dstSpec = dstHostInfo.host;
|
|
151
|
+
if (dstHostInfo.user) {
|
|
152
|
+
dstSpec = `${dstHostInfo.user}@${dstHostInfo.host}`;
|
|
153
|
+
}
|
|
154
|
+
dstSpec = `${dstSpec}:${dst}`;
|
|
155
|
+
|
|
156
|
+
//Build SSH options for the nested SSH connection (from src to dst)
|
|
157
|
+
const nestedSshArgs = ["-A"]; //Enable agent forwarding
|
|
158
|
+
|
|
159
|
+
if (dstHostInfo.port) {
|
|
160
|
+
nestedSshArgs.push("-p", dstHostInfo.port.toString());
|
|
161
|
+
}
|
|
162
|
+
if (dstHostInfo.noStrictHostKeyChecking) {
|
|
163
|
+
nestedSshArgs.push("-oStrictHostKeyChecking=no");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//Build rsync command to be executed on the source host
|
|
167
|
+
const rshOpt = `-e '${sshCmd} ${nestedSshArgs.join(" ")}'`;
|
|
168
|
+
const rsyncOptStr = rsyncOpt.length > 0 ? rsyncOpt.join(" ") : "";
|
|
169
|
+
const srcPaths = argSrc.join(" ");
|
|
170
|
+
|
|
171
|
+
//Create destination directory on the destination host
|
|
172
|
+
let remoteMkdirCmd = "";
|
|
173
|
+
if (dst.endsWith("/")) {
|
|
174
|
+
remoteMkdirCmd = `mkdir -p ${dst}`;
|
|
175
|
+
} else {
|
|
176
|
+
const { dir } = path.parse(dst);
|
|
177
|
+
if (dir !== "" && dir !== "/") {
|
|
178
|
+
remoteMkdirCmd = `mkdir -p ${dir}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (remoteMkdirCmd) {
|
|
183
|
+
//Execute mkdir on destination host via source host with agent forwarding
|
|
184
|
+
const dstUser = dstHostInfo.user ? `${dstHostInfo.user}@` : "";
|
|
185
|
+
const mkdirSshCmd = `${sshCmd} -A ${nestedSshArgs.filter(arg=>{return arg !== "-A";}).join(" ")} ${dstUser}${dstHostInfo.host} '${remoteMkdirCmd}'`;
|
|
186
|
+
debug(`creating remote directory: ${mkdirSshCmd}`);
|
|
187
|
+
await sshExec(srcHostInfo, mkdirSshCmd, timeout, debugVerbose);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//Execute rsync on source host
|
|
191
|
+
const rsyncRemoteCmd = `rsync -avv --copy-unsafe-links ${rshOpt} ${rsyncOptStr} ${srcPaths} ${dstSpec}`;
|
|
192
|
+
debug(`remote to remote copy: ${rsyncRemoteCmd}`);
|
|
193
|
+
debugVerbose(`executing on ${srcHostInfo.host}: ${rsyncRemoteCmd}`);
|
|
194
|
+
|
|
195
|
+
return sshExec(srcHostInfo, rsyncRemoteCmd, timeout, debugVerbose);
|
|
196
|
+
}
|
package/lib/util.js
CHANGED
|
@@ -201,6 +201,12 @@ export const getSshOption = (hostInfo, withoutDestination)=>{
|
|
|
201
201
|
if (hostInfo.noStrictHostKeyChecking) {
|
|
202
202
|
args.push("-oStrictHostKeyChecking=no");
|
|
203
203
|
}
|
|
204
|
+
|
|
205
|
+
//Enable SSH agent forwarding if keyFile is set (indicates we're using key-based auth)
|
|
206
|
+
if (hostInfo.keyFile) {
|
|
207
|
+
args.push("-A");
|
|
208
|
+
}
|
|
209
|
+
|
|
204
210
|
const controlPersist = Number.isInteger(hostInfo.ControlPersist) && hostInfo.ControlPersist >= 0 ? hostInfo.ControlPersist : "180";
|
|
205
211
|
const controlPersistDir = getControlPersistDir(hostInfo);
|
|
206
212
|
args.push("-oControlMaster=auto");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh-client-wrapper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "wrapper library for open ssh client and rsync",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ssh",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"ajv": "^8.17.1",
|
|
39
39
|
"ajv-keywords": "^5.1.0",
|
|
40
40
|
"debug": "^4.4.3",
|
|
41
|
-
"glob": "^
|
|
41
|
+
"glob": "^12.0.0",
|
|
42
42
|
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"c8": "^10.1.3",
|
|
52
52
|
"chai": "^5.3.1",
|
|
53
53
|
"chai-as-promised": "^8.0.1",
|
|
54
|
+
"eslint-plugin-chai-expect": "^3.1.0",
|
|
54
55
|
"eslint-plugin-chai-friendly": "^1.1.0",
|
|
55
56
|
"eslint-plugin-jsdoc": "^54.1.0",
|
|
56
57
|
"eslint-plugin-node": "^11.1.0",
|