ssh-client-wrapper 1.0.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/.eslintrc.json +162 -0
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/lib/fork.js +63 -0
- package/lib/index.js +147 -0
- package/lib/rsyncExec.js +69 -0
- package/lib/sshExec.js +69 -0
- package/lib/util.js +90 -0
- package/package.json +46 -0
- package/test/.eslintrc.json +28 -0
- package/test/rsyncExec.js +388 -0
- package/test/sshExec.js +103 -0
- package/test/util/hostInfo.js +10 -0
- package/test/util/testFiles.js +91 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"extends": [
|
|
4
|
+
"eslint"
|
|
5
|
+
],
|
|
6
|
+
"plugins": [
|
|
7
|
+
"node",
|
|
8
|
+
"jsdoc"
|
|
9
|
+
],
|
|
10
|
+
"env": {
|
|
11
|
+
"node": true,
|
|
12
|
+
"es6": true
|
|
13
|
+
},
|
|
14
|
+
"rules": {
|
|
15
|
+
"no-param-reassign": "warn",
|
|
16
|
+
"arrow-parens": [
|
|
17
|
+
"error",
|
|
18
|
+
"always"
|
|
19
|
+
],
|
|
20
|
+
"arrow-body-style": [
|
|
21
|
+
"error",
|
|
22
|
+
"always"
|
|
23
|
+
],
|
|
24
|
+
"arrow-spacing": [
|
|
25
|
+
"error",
|
|
26
|
+
{
|
|
27
|
+
"before": false,
|
|
28
|
+
"after": false
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"camelcase": [
|
|
32
|
+
"error",
|
|
33
|
+
{
|
|
34
|
+
"properties": "never"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"eqeqeq": [
|
|
38
|
+
"error",
|
|
39
|
+
"always",
|
|
40
|
+
{
|
|
41
|
+
"null": "ignore"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"func-style": [
|
|
45
|
+
"error",
|
|
46
|
+
"declaration",
|
|
47
|
+
{
|
|
48
|
+
"allowArrowFunctions": true
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"indent": [
|
|
52
|
+
"error",
|
|
53
|
+
2,
|
|
54
|
+
{
|
|
55
|
+
"SwitchCase": 1
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"lines-around-comment": [
|
|
59
|
+
"error",
|
|
60
|
+
{
|
|
61
|
+
"beforeBlockComment": true,
|
|
62
|
+
"beforeLineComment": false
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"lines-between-class-members": [
|
|
66
|
+
"error",
|
|
67
|
+
"always",
|
|
68
|
+
{
|
|
69
|
+
"exceptAfterSingleLine": true
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"newline-per-chained-call": "error",
|
|
73
|
+
"no-use-before-define": [
|
|
74
|
+
"error",
|
|
75
|
+
{
|
|
76
|
+
"functions": false
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"no-warning-comments": "warn",
|
|
80
|
+
"padded-blocks": [
|
|
81
|
+
"error",
|
|
82
|
+
"never"
|
|
83
|
+
],
|
|
84
|
+
"padding-line-between-statements": [
|
|
85
|
+
"error",
|
|
86
|
+
{
|
|
87
|
+
"blankLine": "any",
|
|
88
|
+
"prev": [
|
|
89
|
+
"const",
|
|
90
|
+
"let",
|
|
91
|
+
"var"
|
|
92
|
+
],
|
|
93
|
+
"next": "*"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"blankLine": "always",
|
|
97
|
+
"prev": "*",
|
|
98
|
+
"next": [
|
|
99
|
+
"block-like",
|
|
100
|
+
"class",
|
|
101
|
+
"do",
|
|
102
|
+
"for",
|
|
103
|
+
"function",
|
|
104
|
+
"multiline-block-like",
|
|
105
|
+
"switch",
|
|
106
|
+
"try",
|
|
107
|
+
"while"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"blankLine": "any",
|
|
112
|
+
"prev": [
|
|
113
|
+
"const",
|
|
114
|
+
"let",
|
|
115
|
+
"var",
|
|
116
|
+
"for",
|
|
117
|
+
"while",
|
|
118
|
+
"do",
|
|
119
|
+
"block-like",
|
|
120
|
+
"multiline-block-like"
|
|
121
|
+
],
|
|
122
|
+
"next": [
|
|
123
|
+
"block-like",
|
|
124
|
+
"do",
|
|
125
|
+
"for",
|
|
126
|
+
"multiline-block-like",
|
|
127
|
+
"switch",
|
|
128
|
+
"try",
|
|
129
|
+
"while"
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"spaced-comment": [
|
|
134
|
+
"error",
|
|
135
|
+
"never"
|
|
136
|
+
],
|
|
137
|
+
"node/exports-style": [
|
|
138
|
+
"error",
|
|
139
|
+
"module.exports"
|
|
140
|
+
],
|
|
141
|
+
"require-unicode-regexp": "off",
|
|
142
|
+
"node/no-unsupported-features": [
|
|
143
|
+
"error",
|
|
144
|
+
{
|
|
145
|
+
"version": 10
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"jsdoc/require-hyphen-before-param-description": [
|
|
149
|
+
"warn",
|
|
150
|
+
"always"
|
|
151
|
+
],
|
|
152
|
+
"jsdoc/require-param-description": "warn",
|
|
153
|
+
"jsdoc/require-param-name": "warn",
|
|
154
|
+
"jsdoc/require-param-type": "warn",
|
|
155
|
+
"jsdoc/require-jsdoc": "off",
|
|
156
|
+
"jsdoc/require-param": "off",
|
|
157
|
+
"jsdoc/require-returns": "off"
|
|
158
|
+
},
|
|
159
|
+
"parserOptions": {
|
|
160
|
+
"ecmaVersion": 2018
|
|
161
|
+
}
|
|
162
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Naoyuki Sogo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/lib/fork.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const debug = require("debug")("sshClientWrapper:fork");
|
|
3
|
+
const debugVerbose = require("debug")("sshClientWrapper:fork_verbose");
|
|
4
|
+
const debugSendPty = require("debug")("sshClientWrapper:fork_sendpty");
|
|
5
|
+
const { spawn } = require("node-pty");
|
|
6
|
+
|
|
7
|
+
const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String } = require("./util.js");
|
|
8
|
+
|
|
9
|
+
const defaultOpt = {
|
|
10
|
+
|
|
11
|
+
shell: process.platform === "win32" ? process.env.ComSpec : process.env.SHELL,
|
|
12
|
+
windowsHide: true
|
|
13
|
+
};
|
|
14
|
+
async function fork(hostInfo, cmd, args, argOpt, outputCallback) {
|
|
15
|
+
return new Promise((resolve, reject)=>{
|
|
16
|
+
const opt = argOpt || defaultOpt;
|
|
17
|
+
|
|
18
|
+
const ptyProcess = spawn(cmd, args, opt);
|
|
19
|
+
debug(`cmd=${cmd}: args=${args}`);
|
|
20
|
+
ptyProcess.onData(buff2String.bind(null, debugVerbose));
|
|
21
|
+
|
|
22
|
+
ptyProcess.onData((data)=>{
|
|
23
|
+
const output = data.toString();
|
|
24
|
+
|
|
25
|
+
if (reNewHostPrompt.test(output)) {
|
|
26
|
+
debugSendPty("yes");
|
|
27
|
+
ptyProcess.write("yes\n");
|
|
28
|
+
}
|
|
29
|
+
if (rePwPrompt.test(output)) {
|
|
30
|
+
if (typeof hostInfo.password === "string") {
|
|
31
|
+
debugSendPty(hostInfo.password);
|
|
32
|
+
ptyProcess.write(`${hostInfo.password}\n`);
|
|
33
|
+
} else if (typeof hostInfo.password === "function") {
|
|
34
|
+
hostInfo.password(ptyProcess.write);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (rePhPrompt.test(output)) {
|
|
38
|
+
if (typeof hostInfo.passphrase === "string") {
|
|
39
|
+
debugSendPty(hostInfo.passphrase);
|
|
40
|
+
ptyProcess.write(`${hostInfo.passphrase}\n`);
|
|
41
|
+
} else if (typeof hostInfo.passphrase === "function") {
|
|
42
|
+
hostInfo.passphrase(ptyProcess.write);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (typeof outputCallback === "function") {
|
|
48
|
+
ptyProcess.onData(buff2String.bind(null, outputCallback));
|
|
49
|
+
}
|
|
50
|
+
ptyProcess.onExit(({ exitCode, signal })=>{
|
|
51
|
+
debug("onExit", exitCode, signal);
|
|
52
|
+
|
|
53
|
+
if (signal > 0) {
|
|
54
|
+
return reject(signal);
|
|
55
|
+
}
|
|
56
|
+
if ([0, 126, 127].includes(exitCode)) {
|
|
57
|
+
return resolve(exitCode);
|
|
58
|
+
}
|
|
59
|
+
return reject(exitCode);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
module.exports = fork;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const debugLib = require("debug");
|
|
3
|
+
const debug = debugLib("sshClientWrapper:interface");
|
|
4
|
+
const { sshExec, canConnect, disconnect } = require("./sshExec.js");
|
|
5
|
+
const { send, recv } = require("./rsyncExec.js");
|
|
6
|
+
const { isArrayOfString } = require("./util.js");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Integer means integer number but it is defined as Object for now
|
|
10
|
+
* workaround for eslint-plugin-jsdoc's no-undefined-types rule bug.
|
|
11
|
+
* @typedef {Object} Integer
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Facade class.
|
|
16
|
+
* @constructor
|
|
17
|
+
* @param {Object} hostInfo - option object
|
|
18
|
+
* @param {string} hostInfo.host - destination
|
|
19
|
+
* @param {string} hostInfo.user - login name
|
|
20
|
+
* @param {number} hostInfo.port - port number
|
|
21
|
+
* @param {string | Function} hostInfo.password - password
|
|
22
|
+
* @param {string | Function} hostInfo.passphrase - passphrase
|
|
23
|
+
* @param {string} hostInfo.keyFile - private key filename
|
|
24
|
+
* @param {boolean} hostInfo.noStrictHostkeyChecking - bypass host key checking if true
|
|
25
|
+
* @param {Integer} hostInfo.renewInterval=0 - Connection renewal interval (sec).
|
|
26
|
+
*/
|
|
27
|
+
class SshClientWrapper {
|
|
28
|
+
constructor(hostInfo) {
|
|
29
|
+
if (typeof hostInfo.host !== "string") {
|
|
30
|
+
return new Error("hostname must be specified!");
|
|
31
|
+
}
|
|
32
|
+
this.hostInfo = hostInfo;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Execute command on remote host.
|
|
37
|
+
* @param {string} cmd - Cmdline which will be executed.
|
|
38
|
+
* @param { Function } outputCallback - call back routine for output
|
|
39
|
+
* @returns {Integer} - Return code of cmd.
|
|
40
|
+
*
|
|
41
|
+
* If stdout and stderr is array, last 5 line of stdout and stderr is stored in them.
|
|
42
|
+
*/
|
|
43
|
+
async exec(cmd, outputCallback) {
|
|
44
|
+
debug("exec called", cmd);
|
|
45
|
+
|
|
46
|
+
if (typeof cmd !== "string") {
|
|
47
|
+
return Promise.reject(new Error("cmd must be string"));
|
|
48
|
+
}
|
|
49
|
+
return sshExec(this.hostInfo, cmd, outputCallback);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute command repeatedly until specified keyword is found in stdout and/or stderr.
|
|
54
|
+
* @param {string} cmd - Cmdline which will be executed.
|
|
55
|
+
* @param {RegExp} regexp - End condition.
|
|
56
|
+
* @param {number} retryDelay - Duration between each try (in sec).
|
|
57
|
+
* @param {number} maxRetry - Max retry count.
|
|
58
|
+
* @returns {Integer} - Return code of cmd.
|
|
59
|
+
*
|
|
60
|
+
* If stdout and stderr is array, last 10 line of stdout and stderr is stored in them.
|
|
61
|
+
*/
|
|
62
|
+
async watch(cmd, regexp, retryDelay = 3, maxRetry = null) {
|
|
63
|
+
debug("watch called", cmd, regexp, retryDelay, maxRetry);
|
|
64
|
+
|
|
65
|
+
if (typeof cmd !== "string") {
|
|
66
|
+
return Promise.reject(new Error("cmd must be string"));
|
|
67
|
+
}
|
|
68
|
+
if (!(regexp instanceof RegExp)) {
|
|
69
|
+
const err = new Error("illegal regexp specified");
|
|
70
|
+
err.regexp = regexp;
|
|
71
|
+
err.cmd = cmd;
|
|
72
|
+
err.retryDelay = retryDelay;
|
|
73
|
+
err.maxRetry = maxRetry;
|
|
74
|
+
return Promise.reject(err);
|
|
75
|
+
}
|
|
76
|
+
//TODO promise retry
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Send file or directory and its child to server.
|
|
81
|
+
* @param {[string]} src - File or directory name which to be recieve.
|
|
82
|
+
* @param {string} dst - Destination path.
|
|
83
|
+
* @param {[string]} opt - option for rsync
|
|
84
|
+
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
85
|
+
*/
|
|
86
|
+
async send(src, dst, opt = {}) {
|
|
87
|
+
debug("send called", src, dst);
|
|
88
|
+
|
|
89
|
+
if (!isArrayOfString(src)) {
|
|
90
|
+
return new Error("src must be array of string");
|
|
91
|
+
}
|
|
92
|
+
if (typeof dst !== "string") {
|
|
93
|
+
return new Error("dst must be string");
|
|
94
|
+
}
|
|
95
|
+
if (!isArrayOfString(opt)) {
|
|
96
|
+
return new Error("opt must be array of string");
|
|
97
|
+
}
|
|
98
|
+
return send(this.hostInfo, src, dst, opt);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get file or directory and its child from server.
|
|
103
|
+
* @param {[string]} src - File or directory name which to be recieve.
|
|
104
|
+
* @param {string} dst - Destination path.
|
|
105
|
+
* @param {[string]} opt - option for rsync
|
|
106
|
+
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
107
|
+
*/
|
|
108
|
+
async recv(src, dst, opt = {}) {
|
|
109
|
+
debug("recv called", src, dst);
|
|
110
|
+
|
|
111
|
+
if (!isArrayOfString(src)) {
|
|
112
|
+
return new Error("src must be array of string");
|
|
113
|
+
}
|
|
114
|
+
if (typeof dst !== "string") {
|
|
115
|
+
return new Error("dst must be string");
|
|
116
|
+
}
|
|
117
|
+
if (!isArrayOfString(opt)) {
|
|
118
|
+
return new Error("opt must be array of string");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return recv(this.hostInfo, src, dst, opt);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if you can connect to specified server.
|
|
126
|
+
* @param {Integer} timeout - timeout in secconds
|
|
127
|
+
* @returns {Promise} - Resolved with true on success, otherwise rejected with Error.
|
|
128
|
+
*/
|
|
129
|
+
canConnect(timeout) {
|
|
130
|
+
debug("canConnect called timeout=", timeout);
|
|
131
|
+
|
|
132
|
+
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
133
|
+
return new Error("timeout must be positive integer");
|
|
134
|
+
}
|
|
135
|
+
return canConnect(this.hostInfo, timeout);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Disconnect master session
|
|
140
|
+
*/
|
|
141
|
+
disconnect() {
|
|
142
|
+
debug("disconnect called");
|
|
143
|
+
disconnect(this.hostInfo);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = SshClientWrapper;
|
package/lib/rsyncExec.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const debug = require("debug")("sshClientWrapper:rsyncExec");
|
|
4
|
+
const debugVerbose = require("debug")("sshClientWrapper:rsyncExec_verbose");
|
|
5
|
+
const { glob } = require("glob");
|
|
6
|
+
|
|
7
|
+
const { sshExec } = require("./sshExec.js");
|
|
8
|
+
const fork = require("./fork.js");
|
|
9
|
+
|
|
10
|
+
const { sshCmd, rsyncCmd, getSshOption } = require("./util.js");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Send file or directory and its child to server.
|
|
14
|
+
* @param {Object} hostInfo - hostinfo object
|
|
15
|
+
* @param {[string]} argSrc - File or directory name which to be send.
|
|
16
|
+
* @param {string} dst - Destination path.
|
|
17
|
+
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
18
|
+
*
|
|
19
|
+
* this function execute rsync -avvm --copy-unsafe-links src rsync://[user@]host[:port]/dst
|
|
20
|
+
*/
|
|
21
|
+
async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
22
|
+
const promises = await Promise.allSettled(argSrc.map((e)=>{
|
|
23
|
+
return glob(e);
|
|
24
|
+
}));
|
|
25
|
+
const src = promises.flatMap((e)=>{
|
|
26
|
+
if (e.status !== "fulfilled") {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return e.value;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const sshArgs = getSshOption(hostInfo, true);
|
|
33
|
+
const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
|
|
34
|
+
|
|
35
|
+
const args = ["-amvv", "--copy-unsafe-links", ...rshOpt, ...rsyncOpt, ...src, `${hostInfo.host}:${dst}`];
|
|
36
|
+
const { dir: remoteDir } = path.parse(dst);
|
|
37
|
+
if (remoteDir !== "" && remoteDir !== "/") {
|
|
38
|
+
debug(`mkdir ${remoteDir}`);
|
|
39
|
+
sshExec(hostInfo, `mkdir -p ${remoteDir}`, debugVerbose);
|
|
40
|
+
}
|
|
41
|
+
debug(`send ${src} to ${hostInfo.host}:${dst}`);
|
|
42
|
+
debugVerbose(`rsyncCmd=${rsyncCmd}: args=${args}`);
|
|
43
|
+
return fork(hostInfo, rsyncCmd, args);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send file or directory and its child to server.
|
|
48
|
+
* @param {Object} hostInfo - hostinfo object
|
|
49
|
+
* @param {[string]} argSrc - File or directory name which to be send.
|
|
50
|
+
* @param {string} dst - Destination path.
|
|
51
|
+
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
52
|
+
*
|
|
53
|
+
* this function execute rsync -av --copy-unsafe-links rsync://[user@]host[:port]/src dst
|
|
54
|
+
*/
|
|
55
|
+
async function recv(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
56
|
+
const src = `${hostInfo.host}:${argSrc.join(" ")}`;
|
|
57
|
+
const sshArgs = getSshOption(hostInfo, true);
|
|
58
|
+
const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
|
|
59
|
+
|
|
60
|
+
const args = ["-amvv", "--copy-unsafe-links", ...rshOpt, ...rsyncOpt, src, dst];
|
|
61
|
+
|
|
62
|
+
debug(`recv ${src} to ${dst}`);
|
|
63
|
+
debugVerbose(`rsyncCmd=${rsyncCmd}: args=${args}`);
|
|
64
|
+
return fork(hostInfo, rsyncCmd, args);
|
|
65
|
+
}
|
|
66
|
+
module.exports = {
|
|
67
|
+
send,
|
|
68
|
+
recv
|
|
69
|
+
};
|
package/lib/sshExec.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
const debug = require("debug")("sshClientWrapper:sshExec");
|
|
4
|
+
const { sshCmd, getSshOption } = require("./util.js");
|
|
5
|
+
const fork = require("./fork.js");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Execute command on remote host.
|
|
9
|
+
* @param {Object} hostInfo - hostinfo object
|
|
10
|
+
* @param {string} cmd - Cmdline which will be executed on remote host.
|
|
11
|
+
* @param {Function} outputCallback - Callback routine for stdout and stderr.
|
|
12
|
+
*/
|
|
13
|
+
async function sshExec(hostInfo, cmd, outputCallback, opt = null) {
|
|
14
|
+
debug("exec", cmd, "on remote server");
|
|
15
|
+
const args = getSshOption(hostInfo);
|
|
16
|
+
args.push(cmd);
|
|
17
|
+
|
|
18
|
+
return fork(hostInfo, sshCmd, args, opt, outputCallback);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if you can connect to sepecified server
|
|
23
|
+
* @param {Object} hostInfo - hostinfo object
|
|
24
|
+
* @param {Integer} timeout - timeout in secconds
|
|
25
|
+
*/
|
|
26
|
+
async function canConnect(hostInfo, timeout) {
|
|
27
|
+
debug("check connectibity");
|
|
28
|
+
const words = crypto.randomUUID();
|
|
29
|
+
const args = getSshOption(hostInfo);
|
|
30
|
+
if (Number.isInteger(timeout) && timeout > 0) {
|
|
31
|
+
args.push(`-OConnectTimeout=${timeout}`);
|
|
32
|
+
}
|
|
33
|
+
args.push(`echo ${words}`);
|
|
34
|
+
|
|
35
|
+
let output = "";
|
|
36
|
+
const cb = (e)=>{
|
|
37
|
+
output += e;
|
|
38
|
+
};
|
|
39
|
+
await fork(hostInfo, sshCmd, args, null, cb);
|
|
40
|
+
const re = new RegExp(words);
|
|
41
|
+
return re.test(output);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* remove master session
|
|
46
|
+
* @param {Object} hostInfo - hostinfo object
|
|
47
|
+
*/
|
|
48
|
+
async function disconnect(hostInfo) {
|
|
49
|
+
const args = getSshOption(hostInfo);
|
|
50
|
+
args.push("-Oexit");
|
|
51
|
+
|
|
52
|
+
let output = "";
|
|
53
|
+
const cb = (e)=>{
|
|
54
|
+
output += e;
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
await fork(hostInfo, sshCmd, args, null, cb);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err !== 255 || !/Control socket connect.*: No such file or directory/.test(output)) {
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
sshExec,
|
|
67
|
+
canConnect,
|
|
68
|
+
disconnect
|
|
69
|
+
};
|
package/lib/util.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { EOL } = require("os");
|
|
3
|
+
|
|
4
|
+
const sshCmd = "ssh";
|
|
5
|
+
const rsyncCmd = "rsync";
|
|
6
|
+
|
|
7
|
+
const rePwPrompt = /password:/;
|
|
8
|
+
const rePhPrompt = /Enter passphrase for key/;
|
|
9
|
+
const reNewHostPrompt = /Are you sure you want to continue connecting/;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* bridge function to functions which need string
|
|
14
|
+
* @param {Function} func - function
|
|
15
|
+
* @param {data} data - binary data which can be converted to string
|
|
16
|
+
*/
|
|
17
|
+
const buff2String = (func, data)=>{
|
|
18
|
+
func(data.toString().replace(/\r\n/g, EOL));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* return ssh option with some hard corded options
|
|
23
|
+
* @param { Object} hostInfo - host information object defined in index.js
|
|
24
|
+
* @param {boolean} withoutDestination - return except for hostname
|
|
25
|
+
*/
|
|
26
|
+
const getSshOption = (hostInfo, withoutDestination)=>{
|
|
27
|
+
const args = [];
|
|
28
|
+
|
|
29
|
+
if (!withoutDestination) {
|
|
30
|
+
args.push(hostInfo.host);
|
|
31
|
+
}
|
|
32
|
+
if (typeof hostInfo.sshopt === "string") {
|
|
33
|
+
args.push(hostInfo.sshopt);
|
|
34
|
+
}
|
|
35
|
+
if (typeof hostInfo.user === "string") {
|
|
36
|
+
args.push("-l");
|
|
37
|
+
args.push(hostInfo.user);
|
|
38
|
+
}
|
|
39
|
+
if (typeof hostInfo.port === "string" || typeof hostInfo.port === "number") {
|
|
40
|
+
args.push("-p");
|
|
41
|
+
args.push(hostInfo.port);
|
|
42
|
+
}
|
|
43
|
+
if (typeof hostInfo.keyFile === "string") {
|
|
44
|
+
args.push("-i");
|
|
45
|
+
args.push(hostInfo.keyFile);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (hostInfo.noStrictHostkeyChecking) {
|
|
49
|
+
args.push("-oStrictHostKeyChecking=no");
|
|
50
|
+
}
|
|
51
|
+
const renewInterval = Number.isInteger(hostInfo.renewInterval) && hostInfo.renewInterval > 0 ? hostInfo.renewInterval : 180;
|
|
52
|
+
args.push("-oControlMaster=auto");
|
|
53
|
+
args.push("-oControlPath=~/.ssh/wheel-%r@%h:%p");
|
|
54
|
+
args.push(`-oControlPersist=${renewInterval}`);
|
|
55
|
+
return args;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* sleep given time
|
|
60
|
+
* @param {number} time - duration in msec
|
|
61
|
+
*/
|
|
62
|
+
async function sleep(time) {
|
|
63
|
+
return new Promise((resolve)=>{
|
|
64
|
+
setTimeout(()=>{
|
|
65
|
+
resolve();
|
|
66
|
+
}, time);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const isArrayOfString = (target)=>{
|
|
72
|
+
if (!Array.isArray(target)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return target.some((e)=>{
|
|
76
|
+
return typeof e !== "string";
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
sshCmd,
|
|
83
|
+
rsyncCmd,
|
|
84
|
+
rePwPrompt,
|
|
85
|
+
rePhPrompt,
|
|
86
|
+
reNewHostPrompt,
|
|
87
|
+
buff2String,
|
|
88
|
+
getSshOption,
|
|
89
|
+
isArrayOfString
|
|
90
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ssh-client-wrapper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "wrapper library for open ssh client and rsync",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "mocha --recursive test",
|
|
8
|
+
"lint": "eslint --fix lib test",
|
|
9
|
+
"postversion": "git push origin $(git describe --abbrev=0 --tags)"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=12.9"
|
|
13
|
+
},
|
|
14
|
+
"author": "Naoyuki Sogo <sogo@longtail-software.co.jp>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"husky": {
|
|
17
|
+
"hooks": {
|
|
18
|
+
"pre-commit": "lint-staged"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"lint-staged": {
|
|
22
|
+
"*.js": "eslint --fix"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"debug": "^4.3.4",
|
|
26
|
+
"glob": "^10.2.2",
|
|
27
|
+
"node-pty": "^0.10.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"chai": "^4.3.7",
|
|
31
|
+
"chai-as-promised": "^7.1.1",
|
|
32
|
+
"chai-fs": "^2.0.0",
|
|
33
|
+
"eslint": "^8.38.0",
|
|
34
|
+
"eslint-config-eslint": "^7.0.0",
|
|
35
|
+
"eslint-plugin-chai-expect": "^3.0.0",
|
|
36
|
+
"eslint-plugin-chai-friendly": "^0.7.2",
|
|
37
|
+
"eslint-plugin-jsdoc": "^43.0.6",
|
|
38
|
+
"eslint-plugin-node": "^11.1.0",
|
|
39
|
+
"fs-extra": "^11.1.1",
|
|
40
|
+
"husky": "^8.0.3",
|
|
41
|
+
"lint-staged": "^13.2.1",
|
|
42
|
+
"mocha": "^10.2.0",
|
|
43
|
+
"sinon": "^15.0.3",
|
|
44
|
+
"sinon-chai": "^3.7.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"node": true,
|
|
4
|
+
"mocha": true,
|
|
5
|
+
"es6": true
|
|
6
|
+
},
|
|
7
|
+
"settings": {
|
|
8
|
+
"node": {
|
|
9
|
+
"allowModules": [
|
|
10
|
+
"chai",
|
|
11
|
+
"chai-fs",
|
|
12
|
+
"chai-as-promised",
|
|
13
|
+
"sinon",
|
|
14
|
+
"sinon-chai",
|
|
15
|
+
"fs-extra",
|
|
16
|
+
"rewire"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"plugins": [
|
|
21
|
+
"chai-expect",
|
|
22
|
+
"chai-friendly"
|
|
23
|
+
],
|
|
24
|
+
"rules": {
|
|
25
|
+
"no-unused-expressions": 0,
|
|
26
|
+
"chai-friendly/no-unused-expressions": 2
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
|
|
6
|
+
Error.traceLimit = 100000;
|
|
7
|
+
|
|
8
|
+
//setup test framework
|
|
9
|
+
const chai = require("chai");
|
|
10
|
+
const { expect } = require("chai");
|
|
11
|
+
chai.use(require("chai-fs"));
|
|
12
|
+
chai.use(require("chai-as-promised"));
|
|
13
|
+
|
|
14
|
+
//testee
|
|
15
|
+
const { send, recv } = require("../lib/rsyncExec.js");
|
|
16
|
+
|
|
17
|
+
//test helpers
|
|
18
|
+
const { sshExec, disconnect } = require("../lib/sshExec.js");
|
|
19
|
+
const hostInfo = require("./util/hostInfo.js");
|
|
20
|
+
const {
|
|
21
|
+
clearLocalTestFiles,
|
|
22
|
+
clearRemoteTestFiles,
|
|
23
|
+
createLocalFiles,
|
|
24
|
+
localRoot,
|
|
25
|
+
localEmptyDir,
|
|
26
|
+
localFiles,
|
|
27
|
+
createRemoteFiles,
|
|
28
|
+
remoteRoot,
|
|
29
|
+
remoteEmptyDir,
|
|
30
|
+
remoteFiles
|
|
31
|
+
} = require("./util/testFiles");
|
|
32
|
+
|
|
33
|
+
const formatLsOutput = (array)=>{
|
|
34
|
+
return array.join(" ")
|
|
35
|
+
.replace(/\n/g, " ")
|
|
36
|
+
.trim()
|
|
37
|
+
.split(/\s+/);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
//actual test start from here
|
|
42
|
+
describe("test rsync exec", function() {
|
|
43
|
+
this.timeout(10000);//eslint-disable-line no-invalid-this
|
|
44
|
+
const rt = [];
|
|
45
|
+
const output = (data)=>{
|
|
46
|
+
rt.push(data);
|
|
47
|
+
};
|
|
48
|
+
let remoteHome;
|
|
49
|
+
before(async ()=>{
|
|
50
|
+
await sshExec(hostInfo, "pwd", (data)=>{
|
|
51
|
+
remoteHome = data.trim();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
beforeEach(async ()=>{
|
|
55
|
+
await clearRemoteTestFiles(hostInfo);
|
|
56
|
+
await createRemoteFiles(hostInfo);
|
|
57
|
+
await clearLocalTestFiles();
|
|
58
|
+
await createLocalFiles();
|
|
59
|
+
rt.splice(0, rt.length);
|
|
60
|
+
});
|
|
61
|
+
after(async ()=>{
|
|
62
|
+
if (!process.env.TEST_KEEP_FILES) {
|
|
63
|
+
await clearRemoteTestFiles(hostInfo);
|
|
64
|
+
await clearLocalTestFiles();
|
|
65
|
+
}
|
|
66
|
+
after(async ()=>{
|
|
67
|
+
await disconnect(hostInfo);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("#send", async ()=>{
|
|
71
|
+
describe("send single file", ()=>{
|
|
72
|
+
it("should accept relative src file and relative dst dir name", async ()=>{
|
|
73
|
+
await send(hostInfo, [localFiles[0]], remoteEmptyDir);
|
|
74
|
+
|
|
75
|
+
await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
|
|
76
|
+
expect(rt.map((e)=>{
|
|
77
|
+
return path.posix.basename(e);
|
|
78
|
+
})).to.have.members(["foo\n"]);
|
|
79
|
+
});
|
|
80
|
+
it("should accept absolute src file and relative dst dir name", async ()=>{
|
|
81
|
+
await send(hostInfo, [path.resolve(localFiles[3])], remoteEmptyDir);
|
|
82
|
+
|
|
83
|
+
await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
|
|
84
|
+
expect(rt.map((e)=>{
|
|
85
|
+
return path.posix.basename(e);
|
|
86
|
+
})).to.have.members(["piyo\n"]);
|
|
87
|
+
});
|
|
88
|
+
it("should accept relative src file and absolute dst dir name", async ()=>{
|
|
89
|
+
await send(hostInfo, [localFiles[0]], `${path.posix.join(remoteHome, remoteEmptyDir)}/`);
|
|
90
|
+
|
|
91
|
+
await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
|
|
92
|
+
expect(rt.map((e)=>{
|
|
93
|
+
return path.posix.basename(e);
|
|
94
|
+
})).to.have.members(["foo\n"]);
|
|
95
|
+
});
|
|
96
|
+
it("should accept absolute src file and absolute dst dir name", async ()=>{
|
|
97
|
+
await send(hostInfo, [path.resolve(localFiles[0])], path.posix.join(remoteHome, remoteEmptyDir));
|
|
98
|
+
|
|
99
|
+
await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
|
|
100
|
+
expect(rt.map((e)=>{
|
|
101
|
+
return path.posix.basename(e);
|
|
102
|
+
})).to.have.members(["foo\n"]);
|
|
103
|
+
});
|
|
104
|
+
it("should accept relative src file and relative dst file name", async ()=>{
|
|
105
|
+
await send(hostInfo, [localFiles[0]], path.posix.join(remoteEmptyDir, "hoge"));
|
|
106
|
+
|
|
107
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
|
|
108
|
+
expect(rt.map((e)=>{
|
|
109
|
+
return path.posix.basename(e);
|
|
110
|
+
})).to.have.members(["hoge\n"]);
|
|
111
|
+
});
|
|
112
|
+
it("should accept absolute src file and relative dst file name", async ()=>{
|
|
113
|
+
await send(hostInfo, [path.resolve(localFiles[0])], path.posix.join(remoteEmptyDir, "hoge"));
|
|
114
|
+
|
|
115
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
|
|
116
|
+
expect(rt.map((e)=>{
|
|
117
|
+
return path.posix.basename(e);
|
|
118
|
+
})).to.have.members(["hoge\n"]);
|
|
119
|
+
});
|
|
120
|
+
it("should accept relative src file and absolute dst file name", async ()=>{
|
|
121
|
+
await send(hostInfo, [localFiles[0]], path.posix.join(remoteHome, remoteEmptyDir, "hoge"));
|
|
122
|
+
|
|
123
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
|
|
124
|
+
expect(rt.map((e)=>{
|
|
125
|
+
return path.posix.basename(e);
|
|
126
|
+
})).to.have.members(["hoge\n"]);
|
|
127
|
+
});
|
|
128
|
+
it("should accept absolute src file and absolute dst file name", async ()=>{
|
|
129
|
+
await send(hostInfo, [path.resolve(localFiles[0])], path.posix.join(remoteHome, remoteEmptyDir, "hoge"));
|
|
130
|
+
|
|
131
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, "hoge")}`, output);
|
|
132
|
+
expect(rt.map((e)=>{
|
|
133
|
+
return path.posix.basename(e);
|
|
134
|
+
})).to.have.members(["hoge\n"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should overwrite existing file", async ()=>{
|
|
138
|
+
const target = path.posix.join(remoteEmptyDir, "hoge");
|
|
139
|
+
await send(hostInfo, [localFiles[0]], target);
|
|
140
|
+
await sshExec(hostInfo, `ls ${target}`, output);
|
|
141
|
+
expect(rt.map((e)=>{
|
|
142
|
+
return path.posix.basename(e);
|
|
143
|
+
})).to.have.members(["hoge\n"]);
|
|
144
|
+
rt.splice(0, rt.length);
|
|
145
|
+
|
|
146
|
+
const time = new Date();
|
|
147
|
+
time.setMinutes(time.getMinutes() + 3);
|
|
148
|
+
await fs.utimes(localFiles[1], time, time);
|
|
149
|
+
await send(hostInfo, [localFiles[1]], target);
|
|
150
|
+
await sshExec(hostInfo, `ls ${target}`, output);
|
|
151
|
+
expect(rt.map((e)=>{
|
|
152
|
+
return path.posix.basename(e);
|
|
153
|
+
})).to.have.members(["hoge\n"]);
|
|
154
|
+
|
|
155
|
+
let result;
|
|
156
|
+
await sshExec(hostInfo, `cat ${target}`, (data)=>{
|
|
157
|
+
result = data;
|
|
158
|
+
});
|
|
159
|
+
expect(result).to.equal("ARssh_testLocalDir/bar\n");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe("send directory tree", ()=>{
|
|
163
|
+
it("should accept relative src dirname and relative dst dirname", async ()=>{
|
|
164
|
+
await send(hostInfo, [localRoot], remoteEmptyDir);
|
|
165
|
+
|
|
166
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
|
|
167
|
+
expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
168
|
+
|
|
169
|
+
rt.splice(0, rt.length);
|
|
170
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
|
|
171
|
+
expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
|
|
172
|
+
});
|
|
173
|
+
it("should accept absolute src dirname and relative dst dirname", async ()=>{
|
|
174
|
+
await send(hostInfo, [path.resolve(localRoot)], remoteEmptyDir);
|
|
175
|
+
|
|
176
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
|
|
177
|
+
expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
178
|
+
|
|
179
|
+
rt.splice(0, rt.length);
|
|
180
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
|
|
181
|
+
expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
|
|
182
|
+
});
|
|
183
|
+
it("should accept relative src dirname and absolute dst dirname", async ()=>{
|
|
184
|
+
await send(hostInfo, [localRoot], path.posix.join(remoteHome, remoteEmptyDir));
|
|
185
|
+
|
|
186
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
|
|
187
|
+
expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
188
|
+
|
|
189
|
+
rt.splice(0, rt.length);
|
|
190
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
|
|
191
|
+
expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
|
|
192
|
+
});
|
|
193
|
+
it("should accept absolute src dirname and absolute dst dirname", async ()=>{
|
|
194
|
+
await send(hostInfo, [path.resolve(localRoot)], path.posix.join(remoteHome, remoteEmptyDir));
|
|
195
|
+
|
|
196
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
|
|
197
|
+
expect(formatLsOutput(rt)).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
198
|
+
|
|
199
|
+
rt.splice(0, rt.length);
|
|
200
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
|
|
201
|
+
expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
|
|
202
|
+
});
|
|
203
|
+
//only option need special treatment in rsyncExec. it's hard to set in WHEEL
|
|
204
|
+
it.skip("[not implemented] should send directory tree if only filter matched", async ()=>{
|
|
205
|
+
await send(hostInfo, [localRoot], remoteEmptyDir, [`--include=${localRoot}`, `--include=${localRoot}/ba*`, `--include=${localRoot}/hoge/*`, "--exclude=*"]);
|
|
206
|
+
|
|
207
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
|
|
208
|
+
expect(formatLsOutput(rt)).to.have.members(["hoge", "bar", "baz"]);
|
|
209
|
+
|
|
210
|
+
rt.splice(0, rt.length);
|
|
211
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
|
|
212
|
+
expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo", "poyo"]);
|
|
213
|
+
});
|
|
214
|
+
it("should send directory tree if exclude filter not matched", async ()=>{
|
|
215
|
+
await send(hostInfo, [localRoot], remoteEmptyDir, ["--exclude=ba*", "--exclude=hoge*}"]);
|
|
216
|
+
|
|
217
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
|
|
218
|
+
expect(formatLsOutput(rt)).to.have.members(["foo", "hoge"]);
|
|
219
|
+
});
|
|
220
|
+
it.skip("[not implemented] should send directory tree if only filter matched but exclude filter not matched", async ()=>{
|
|
221
|
+
await send(hostInfo, [localRoot], remoteEmptyDir, "*/{ba*,hoge/*}", "**/poyo");
|
|
222
|
+
|
|
223
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot)}`, output);
|
|
224
|
+
expect(formatLsOutput(rt)).to.have.members(["hoge", "bar", "baz"]);
|
|
225
|
+
|
|
226
|
+
rt.splice(0, rt.length);
|
|
227
|
+
await sshExec(hostInfo, `ls ${path.posix.join(remoteEmptyDir, localRoot, "hoge")}`, output);
|
|
228
|
+
expect(formatLsOutput(rt)).to.have.members(["piyo", "puyo"]);
|
|
229
|
+
});
|
|
230
|
+
it("shoud not send empty directory", async ()=>{
|
|
231
|
+
await send(hostInfo, [localEmptyDir], remoteEmptyDir);
|
|
232
|
+
|
|
233
|
+
await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
|
|
234
|
+
expect(rt).to.have.lengthOf(0);
|
|
235
|
+
});
|
|
236
|
+
it("should send files which match glob pattern", async ()=>{
|
|
237
|
+
await send(hostInfo, [path.join(localRoot, "b*")], remoteEmptyDir);
|
|
238
|
+
|
|
239
|
+
await sshExec(hostInfo, `ls ${remoteEmptyDir}`, output);
|
|
240
|
+
expect(formatLsOutput(rt)).to.have.members(["bar", "baz"]);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe("error case", ()=>{
|
|
244
|
+
it("should not send directory to existing file path", ()=>{
|
|
245
|
+
return expect(send(hostInfo, [localRoot], path.posix.join(remoteRoot, "foo"))).to.be.rejected;
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe("#recv", async ()=>{
|
|
250
|
+
describe("recieve single file", ()=>{
|
|
251
|
+
it("should accept relative src file and relative dst dir name", async ()=>{
|
|
252
|
+
await recv(hostInfo, [remoteFiles[3]], localEmptyDir);
|
|
253
|
+
|
|
254
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
255
|
+
expect(rt2).to.have.members(["piyo"]);
|
|
256
|
+
});
|
|
257
|
+
it("should accept absolute src file and relative dst dir name", async ()=>{
|
|
258
|
+
await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[3])], localEmptyDir);
|
|
259
|
+
|
|
260
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
261
|
+
expect(rt2).to.have.members(["piyo"]);
|
|
262
|
+
});
|
|
263
|
+
it("should accept relative src file and absolute dst dir name", async ()=>{
|
|
264
|
+
await recv(hostInfo, [remoteFiles[3]], path.resolve(localEmptyDir));
|
|
265
|
+
|
|
266
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
267
|
+
expect(rt2).to.have.members(["piyo"]);
|
|
268
|
+
});
|
|
269
|
+
it("should accept absolute src file and absolute dst dir name", async ()=>{
|
|
270
|
+
await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[3])], path.resolve(localEmptyDir));
|
|
271
|
+
|
|
272
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
273
|
+
expect(rt2).to.have.members(["piyo"]);
|
|
274
|
+
});
|
|
275
|
+
it("should accept relative src file and relative dst file name", async ()=>{
|
|
276
|
+
await recv(hostInfo, [remoteFiles[0]], path.join(localEmptyDir, "hoge"));
|
|
277
|
+
|
|
278
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
279
|
+
expect(rt2).to.have.members(["hoge"]);
|
|
280
|
+
});
|
|
281
|
+
it("should accept absolute src file and relative dst file name", async ()=>{
|
|
282
|
+
await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[0])], path.join(localEmptyDir, "hoge"));
|
|
283
|
+
|
|
284
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
285
|
+
expect(rt2).to.have.members(["hoge"]);
|
|
286
|
+
});
|
|
287
|
+
it("should accept relative src file and absolute dst file name", async ()=>{
|
|
288
|
+
await recv(hostInfo, [remoteFiles[0]], path.resolve(localEmptyDir, "hoge"));
|
|
289
|
+
|
|
290
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
291
|
+
expect(rt2).to.have.members(["hoge"]);
|
|
292
|
+
});
|
|
293
|
+
it("should accept absolute src file and absolute dst file name", async ()=>{
|
|
294
|
+
await recv(hostInfo, [path.posix.join(remoteHome, remoteFiles[0])], path.resolve(localEmptyDir, "hoge"));
|
|
295
|
+
|
|
296
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
297
|
+
expect(rt2).to.have.members(["hoge"]);
|
|
298
|
+
});
|
|
299
|
+
it("should accept glob pattern as src", async ()=>{
|
|
300
|
+
await recv(hostInfo, [path.posix.join(remoteRoot, "b*")], path.resolve(localEmptyDir));
|
|
301
|
+
|
|
302
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
303
|
+
expect(rt2).to.have.members(["bar", "baz"]);
|
|
304
|
+
});
|
|
305
|
+
it("should accept glob pattern as src", async ()=>{
|
|
306
|
+
await recv(hostInfo, [path.posix.join(remoteRoot, "*/p[iu]yo")], path.resolve(localEmptyDir));
|
|
307
|
+
|
|
308
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
309
|
+
expect(rt2).to.have.members(["piyo", "puyo"]);
|
|
310
|
+
});
|
|
311
|
+
it("should accept glob pattern as src", async ()=>{
|
|
312
|
+
await recv(hostInfo, [path.posix.join(remoteRoot, "hoge/p[iu]yo"), path.posix.join(remoteRoot, "foo")], path.resolve(localEmptyDir));
|
|
313
|
+
|
|
314
|
+
const rt2 = await fs.readdir(localEmptyDir);
|
|
315
|
+
expect(rt2).to.have.members(["piyo", "puyo", "foo"]);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
describe("recieve directory tree", ()=>{
|
|
319
|
+
it("should get directory tree which match specified glob", async ()=>{
|
|
320
|
+
await recv(hostInfo, [path.posix.join(remoteRoot, "?o*")], localEmptyDir);
|
|
321
|
+
|
|
322
|
+
let rt2 = await fs.readdir(localEmptyDir);
|
|
323
|
+
expect(rt2).to.have.members(["foo", "hoge"]);
|
|
324
|
+
rt2 = await fs.readdir(path.join(localEmptyDir, "hoge"));
|
|
325
|
+
expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
|
|
326
|
+
});
|
|
327
|
+
it("should accept relative src dirname and relative dst dirname", async ()=>{
|
|
328
|
+
await recv(hostInfo, [remoteRoot], localEmptyDir);
|
|
329
|
+
|
|
330
|
+
let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
|
|
331
|
+
expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
332
|
+
rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
|
|
333
|
+
expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
|
|
334
|
+
});
|
|
335
|
+
it("should accept absolute src dirname and relative dst dirname", async ()=>{
|
|
336
|
+
await recv(hostInfo, [path.posix.join(remoteHome, remoteRoot)], localEmptyDir);
|
|
337
|
+
|
|
338
|
+
let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
|
|
339
|
+
expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
340
|
+
rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
|
|
341
|
+
expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
|
|
342
|
+
});
|
|
343
|
+
it("should accept relative src dirname and absolute dst dirname", async ()=>{
|
|
344
|
+
await recv(hostInfo, [remoteRoot], path.resolve(localEmptyDir));
|
|
345
|
+
|
|
346
|
+
let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
|
|
347
|
+
expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
348
|
+
rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
|
|
349
|
+
expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
|
|
350
|
+
});
|
|
351
|
+
it("should accept absolute src dirname and absolute dst dirname", async ()=>{
|
|
352
|
+
await recv(hostInfo, [path.posix.join(remoteHome, remoteRoot)], path.resolve(localEmptyDir));
|
|
353
|
+
|
|
354
|
+
let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
|
|
355
|
+
expect(rt2).to.have.members(["foo", "bar", "baz", "hoge"]);
|
|
356
|
+
rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
|
|
357
|
+
expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
|
|
358
|
+
});
|
|
359
|
+
it.skip("[not implemented] should recv files which matches only filter", async ()=>{
|
|
360
|
+
await recv(hostInfo, [remoteRoot], localEmptyDir, "*/{ba*,hoge/*}");
|
|
361
|
+
|
|
362
|
+
let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
|
|
363
|
+
expect(rt2).to.have.members(["bar", "baz", "hoge"]);
|
|
364
|
+
rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
|
|
365
|
+
expect(rt2).to.have.members(["piyo", "puyo", "poyo"]);
|
|
366
|
+
});
|
|
367
|
+
it("should not recv files which matches exclude filter", async ()=>{
|
|
368
|
+
await recv(hostInfo, [remoteRoot], localEmptyDir, ["--exclude=*/ba*", "--exclude=hoge"]);
|
|
369
|
+
|
|
370
|
+
const rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
|
|
371
|
+
expect(rt2).to.have.members(["foo"]);
|
|
372
|
+
});
|
|
373
|
+
it.skip("[not implemented] should recv files which matches only filter but should not recv which matches exclude filter", async ()=>{
|
|
374
|
+
await recv(hostInfo, [remoteRoot], localEmptyDir, "*/{ba*,hoge/*}", "**/piyo");
|
|
375
|
+
|
|
376
|
+
let rt2 = await fs.readdir(path.posix.join(localEmptyDir, remoteRoot));
|
|
377
|
+
expect(rt2).to.have.members(["bar", "baz", "hoge"]);
|
|
378
|
+
rt2 = await fs.readdir(path.join(localEmptyDir, remoteRoot, "hoge"));
|
|
379
|
+
expect(rt2).to.have.members(["puyo", "poyo"]);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
describe("error case", ()=>{
|
|
383
|
+
it("should not send directory to existing file path", ()=>{
|
|
384
|
+
return expect(recv(hostInfo, [remoteRoot], localFiles[0])).to.be.rejected;
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
package/test/sshExec.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
|
|
4
|
+
Error.traceLimit = 100000;
|
|
5
|
+
|
|
6
|
+
//setup test framework
|
|
7
|
+
const chai = require("chai");
|
|
8
|
+
const { expect } = require("chai");
|
|
9
|
+
const sinon = require("sinon");
|
|
10
|
+
chai.use(require("sinon-chai"));
|
|
11
|
+
chai.use(require("chai-as-promised"));
|
|
12
|
+
|
|
13
|
+
const sshout = sinon.stub();
|
|
14
|
+
|
|
15
|
+
//testee
|
|
16
|
+
const { sshExec, canConnect, disconnect } = require("../lib/sshExec.js");
|
|
17
|
+
|
|
18
|
+
//test helpers
|
|
19
|
+
const hostInfo = require("./util/hostInfo.js");
|
|
20
|
+
const { nonExisting } = require("./util/testFiles.js");
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
describe("test for ssh execution", function() {
|
|
24
|
+
this.timeout(10000);//eslint-disable-line no-invalid-this
|
|
25
|
+
beforeEach(()=>{
|
|
26
|
+
sshout.reset();
|
|
27
|
+
});
|
|
28
|
+
after(async ()=>{
|
|
29
|
+
await disconnect(hostInfo);
|
|
30
|
+
});
|
|
31
|
+
describe("#exec", ()=>{
|
|
32
|
+
const testText = "hoge";
|
|
33
|
+
it("should execute single command with stdout", async ()=>{
|
|
34
|
+
const rt = await sshExec(hostInfo, `echo ${testText}`, sshout);
|
|
35
|
+
expect(rt).to.equal(0);
|
|
36
|
+
expect(sshout).to.be.calledOnce;
|
|
37
|
+
expect(sshout).to.be.calledWith("hoge\n");
|
|
38
|
+
});
|
|
39
|
+
it("should execute single command with stderr", async ()=>{
|
|
40
|
+
const rt = await sshExec(hostInfo, `echo ${testText} >&2`, sshout);
|
|
41
|
+
expect(rt).to.equal(0);
|
|
42
|
+
expect(sshout).to.be.calledOnce;
|
|
43
|
+
expect(sshout).to.be.calledWith("hoge\n");
|
|
44
|
+
});
|
|
45
|
+
it("should execute single command with stdout & stderr", async ()=>{
|
|
46
|
+
const rt = await sshExec(hostInfo, `echo ${testText}; echo ${testText}>&2`, sshout);
|
|
47
|
+
expect(rt).to.equal(0);
|
|
48
|
+
expect(sshout).to.be.calledTwice;
|
|
49
|
+
const call0 = sshout.getCall(0);
|
|
50
|
+
const call1 = sshout.getCall(1);
|
|
51
|
+
expect(call0).to.be.calledWith("hoge\n");
|
|
52
|
+
expect(call1).to.be.calledWith("hoge\n");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
//please note that exec() resolves with non-zero value
|
|
56
|
+
//(126 permisssion deny or 127 file not found)
|
|
57
|
+
//but does not reject in following 2 cases
|
|
58
|
+
it("should not execute command which do not have exec permission", async ()=>{
|
|
59
|
+
await sshExec(hostInfo, "echo echo hoge >hoge");
|
|
60
|
+
await sshExec(hostInfo, "chmod ugo-x hoge");
|
|
61
|
+
const rt = await sshExec(hostInfo, "./hoge");
|
|
62
|
+
expect(rt).to.equal(126);
|
|
63
|
+
});
|
|
64
|
+
it("should resolve with 127 if command is not found", async ()=>{
|
|
65
|
+
const rt = await sshExec(hostInfo, `./${nonExisting}`);
|
|
66
|
+
expect(rt).to.equal(127);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("#canConnect", ()=>{
|
|
70
|
+
let hostInfo2;
|
|
71
|
+
beforeEach(async ()=>{
|
|
72
|
+
hostInfo2 = { ...hostInfo };
|
|
73
|
+
await disconnect(hostInfo);
|
|
74
|
+
});
|
|
75
|
+
it("should be resolved with true", async ()=>{
|
|
76
|
+
expect(await canConnect(hostInfo)).to.be.true;
|
|
77
|
+
});
|
|
78
|
+
it("should be rejected if user does not exist", async ()=>{
|
|
79
|
+
hostInfo2.user = "xxxx";
|
|
80
|
+
return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
|
|
81
|
+
});
|
|
82
|
+
it("should be rejected if password is wrong", async ()=>{
|
|
83
|
+
hostInfo2.password = "xxxx";
|
|
84
|
+
return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
|
|
85
|
+
});
|
|
86
|
+
it("should be rejected if host does not exist", async ()=>{
|
|
87
|
+
hostInfo2.host = "foo.bar.example.com";
|
|
88
|
+
return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
|
|
89
|
+
});
|
|
90
|
+
it("should be rejected if host(ip address) does not exist", async ()=>{
|
|
91
|
+
hostInfo2.host = "192.0.2.1";
|
|
92
|
+
return expect(canConnect(hostInfo2, 5)).to.be.rejectedWith(255);
|
|
93
|
+
});
|
|
94
|
+
it("should be rejected if port number is out of range(-1)", async ()=>{
|
|
95
|
+
hostInfo2.port = -1;
|
|
96
|
+
return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
|
|
97
|
+
});
|
|
98
|
+
it("should be rejected if port number is out of range(65536)", async ()=>{
|
|
99
|
+
hostInfo2.port = 65536;
|
|
100
|
+
return expect(canConnect(hostInfo2)).to.be.rejectedWith(255);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
module.exports = {
|
|
3
|
+
host: process.env.TEST_HOST || "localhost",
|
|
4
|
+
user: process.env.TEST_USER || null,
|
|
5
|
+
port: process.env.TEST_PORT || null,
|
|
6
|
+
password: process.env.TEST_PW,
|
|
7
|
+
passphrase: process.env.TEST_PH,
|
|
8
|
+
keyFile: process.env.TEST_PRIVATE_KEY,
|
|
9
|
+
noStrictHostkeyChecking: true
|
|
10
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const { sshExec } = require("../../lib/sshExec.js");
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* test directory tree
|
|
9
|
+
* ${ROOT}
|
|
10
|
+
* +-- huga/ (empty directory)
|
|
11
|
+
* +-- foo
|
|
12
|
+
* +-- bar
|
|
13
|
+
* +-- baz
|
|
14
|
+
* +-- hoge
|
|
15
|
+
* +-- piyo
|
|
16
|
+
* +-- puyo
|
|
17
|
+
* +-- poyo
|
|
18
|
+
*
|
|
19
|
+
* ${ROOT} is "ARssh_testLocalDir" on local side
|
|
20
|
+
* it is ARssh_testLocalDir on remote side
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const localRoot = "ARssh_testLocalDir";
|
|
25
|
+
const localEmptyDir = path.join(localRoot, "huga");
|
|
26
|
+
const localFiles = [
|
|
27
|
+
path.join(localRoot, "foo"),
|
|
28
|
+
path.join(localRoot, "bar"),
|
|
29
|
+
path.join(localRoot, "baz"),
|
|
30
|
+
path.join(localRoot, "hoge", "piyo"),
|
|
31
|
+
path.join(localRoot, "hoge", "puyo"),
|
|
32
|
+
path.join(localRoot, "hoge", "poyo")
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const remoteRoot = "ARssh_testRemoteDir";
|
|
36
|
+
const remoteEmptyDir = `${remoteRoot}/huga`;
|
|
37
|
+
const remoteFiles = [
|
|
38
|
+
`${remoteRoot}/foo`,
|
|
39
|
+
`${remoteRoot}/bar`,
|
|
40
|
+
`${remoteRoot}/baz`,
|
|
41
|
+
`${remoteRoot}/hoge/piyo`,
|
|
42
|
+
`${remoteRoot}/hoge/puyo`,
|
|
43
|
+
`${remoteRoot}/hoge/poyo`
|
|
44
|
+
];
|
|
45
|
+
const nonExisting = "ARSSH_nonExisting";
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* prepare local files which contain its filename
|
|
49
|
+
*/
|
|
50
|
+
async function createLocalFiles() {
|
|
51
|
+
const localDir2 = path.join(localRoot, "hoge");
|
|
52
|
+
const promises = [];
|
|
53
|
+
await fs.mkdir(localRoot);
|
|
54
|
+
await fs.mkdir(localDir2);
|
|
55
|
+
promises.push(fs.mkdir(localEmptyDir));
|
|
56
|
+
localFiles.forEach((localFile)=>{
|
|
57
|
+
promises.push(fs.writeFile(localFile, `${localFile}\n`));
|
|
58
|
+
});
|
|
59
|
+
return Promise.all(promises);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async function clearLocalTestFiles() {
|
|
64
|
+
return fs.remove(localRoot);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function createRemoteFiles(hostInfo) {
|
|
68
|
+
//create remote files
|
|
69
|
+
await sshExec(hostInfo, `mkdir -p ${remoteRoot}/hoge`);
|
|
70
|
+
await sshExec(hostInfo, `mkdir -p ${remoteEmptyDir}`);
|
|
71
|
+
const script = remoteFiles.reduce((a, remoteFile)=>{
|
|
72
|
+
return `${a}echo ${remoteFile} > ${remoteFile};`;
|
|
73
|
+
}, "");
|
|
74
|
+
return sshExec(hostInfo, `${script}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function clearRemoteTestFiles(hostInfo) {
|
|
78
|
+
return sshExec(hostInfo, `rm -fr ${remoteRoot}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports.createLocalFiles = createLocalFiles;
|
|
82
|
+
module.exports.createRemoteFiles = createRemoteFiles;
|
|
83
|
+
module.exports.clearLocalTestFiles = clearLocalTestFiles;
|
|
84
|
+
module.exports.clearRemoteTestFiles = clearRemoteTestFiles;
|
|
85
|
+
module.exports.localRoot = localRoot;
|
|
86
|
+
module.exports.localEmptyDir = localEmptyDir;
|
|
87
|
+
module.exports.localFiles = localFiles;
|
|
88
|
+
module.exports.nonExisting = nonExisting;
|
|
89
|
+
module.exports.remoteRoot = remoteRoot;
|
|
90
|
+
module.exports.remoteEmptyDir = remoteEmptyDir;
|
|
91
|
+
module.exports.remoteFiles = remoteFiles;
|