@qse/ssh-sftp 1.0.0 → 1.1.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 +2 -0
- package/lib/cli.js +0 -23
- package/lib/index.js +76 -97
- package/lib/utils.js +1 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ npm i @qse/ssh-sftp -D
|
|
|
36
36
|
|
|
37
37
|
### `.sftprc.json`
|
|
38
38
|
|
|
39
|
+
schema定义 http://www.zhidianbao.cn:8088/qsxxwapdev/edu-ssh-sftp/sftprc.schema.json
|
|
40
|
+
|
|
39
41
|
| 字段名 | 类型 | 描述 | 默认值 |
|
|
40
42
|
| ---------------- | ---------------------------------------------------- | ---------------------------------------------- | ---------------------- |
|
|
41
43
|
| localPath | `string` | | `'dist'` |
|
package/lib/cli.js
CHANGED
|
@@ -7,21 +7,14 @@ const {
|
|
|
7
7
|
sshSftpShowUrl,
|
|
8
8
|
sshSftpShowConfig
|
|
9
9
|
} = require('./index');
|
|
10
|
-
|
|
11
10
|
const fs = require('fs');
|
|
12
|
-
|
|
13
11
|
const ora = require('ora');
|
|
14
|
-
|
|
15
12
|
const {
|
|
16
13
|
exitWithError
|
|
17
14
|
} = require('./utils');
|
|
18
|
-
|
|
19
15
|
const updateNotifier = require('update-notifier');
|
|
20
|
-
|
|
21
16
|
const pkg = require('../package.json');
|
|
22
|
-
|
|
23
17
|
const presets = require('./presets');
|
|
24
|
-
|
|
25
18
|
updateNotifier({
|
|
26
19
|
pkg,
|
|
27
20
|
updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
|
|
@@ -43,48 +36,37 @@ require('yargs').usage('使用: $0 [command] \n\n代码:svn://192.168.10.168/e
|
|
|
43
36
|
v: 'version',
|
|
44
37
|
h: 'help'
|
|
45
38
|
}).argv;
|
|
46
|
-
|
|
47
39
|
function getOpts() {
|
|
48
40
|
isRoot();
|
|
49
|
-
|
|
50
41
|
if (!fs.existsSync('.sftprc.json')) {
|
|
51
42
|
return exitWithError('没找到 .sftprc.json 文件,请先执行 ssh-sftp init');
|
|
52
43
|
}
|
|
53
|
-
|
|
54
44
|
const opts = fs.readFileSync('.sftprc.json', 'utf-8');
|
|
55
45
|
return JSON.parse(opts);
|
|
56
46
|
}
|
|
57
|
-
|
|
58
47
|
function isRoot() {
|
|
59
48
|
if (!fs.existsSync('package.json')) {
|
|
60
49
|
exitWithError('请在项目的根目录运行(package.json所在的目录)');
|
|
61
50
|
}
|
|
62
51
|
}
|
|
63
|
-
|
|
64
52
|
function upload({
|
|
65
53
|
clear,
|
|
66
54
|
yes
|
|
67
55
|
}) {
|
|
68
56
|
const opts = getOpts();
|
|
69
|
-
|
|
70
57
|
if (clear === false) {
|
|
71
58
|
opts.cleanRemoteFiles = false;
|
|
72
59
|
}
|
|
73
|
-
|
|
74
60
|
if (yes) {
|
|
75
61
|
opts.skipPrompt = true;
|
|
76
62
|
}
|
|
77
|
-
|
|
78
63
|
sshSftp(opts);
|
|
79
64
|
}
|
|
80
|
-
|
|
81
65
|
function generateDefaultConfigJSON() {
|
|
82
66
|
isRoot();
|
|
83
|
-
|
|
84
67
|
if (fs.existsSync('.sftprc.json')) {
|
|
85
68
|
return exitWithError('已存在 .sftprc.json 文件,请勿重复生成');
|
|
86
69
|
}
|
|
87
|
-
|
|
88
70
|
fs.writeFileSync('.sftprc.json', JSON.stringify({
|
|
89
71
|
$schema: 'http://www.zhidianbao.cn:8088/qsxxwapdev/edu-ssh-sftp/sftprc.schema.json',
|
|
90
72
|
localPath: '/path/to/localDir',
|
|
@@ -102,25 +84,20 @@ function generateDefaultConfigJSON() {
|
|
|
102
84
|
});
|
|
103
85
|
ora().succeed('.sftprc.json 生成在项目根目录');
|
|
104
86
|
}
|
|
105
|
-
|
|
106
87
|
function ls(argv) {
|
|
107
88
|
if (!argv.u && !argv.d && !argv.i) {
|
|
108
89
|
argv.u = true;
|
|
109
90
|
argv.d = true;
|
|
110
91
|
argv.i = true;
|
|
111
92
|
}
|
|
112
|
-
|
|
113
93
|
sshSftpLS(getOpts(), argv);
|
|
114
94
|
}
|
|
115
|
-
|
|
116
95
|
function showUrl() {
|
|
117
96
|
sshSftpShowUrl(getOpts());
|
|
118
97
|
}
|
|
119
|
-
|
|
120
98
|
function showConfig() {
|
|
121
99
|
sshSftpShowConfig(getOpts());
|
|
122
100
|
}
|
|
123
|
-
|
|
124
101
|
function showPresets() {
|
|
125
102
|
console.log(JSON.stringify(presets, null, 2));
|
|
126
103
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const Client = require('ssh2-sftp-client');
|
|
4
|
-
|
|
5
4
|
const ora = require('ora');
|
|
6
|
-
|
|
7
5
|
const glob = require('glob');
|
|
8
|
-
|
|
9
6
|
const fs = require('fs');
|
|
10
|
-
|
|
11
7
|
const minimatch = require('minimatch');
|
|
12
|
-
|
|
13
8
|
const inquirer = require('inquirer');
|
|
14
|
-
|
|
15
9
|
const {
|
|
16
10
|
exitWithError,
|
|
17
11
|
warn
|
|
18
12
|
} = require('./utils');
|
|
19
|
-
|
|
20
13
|
const chalk = require('chalk');
|
|
21
|
-
|
|
22
14
|
const {
|
|
23
15
|
presets,
|
|
24
16
|
servers
|
|
25
17
|
} = require('./presets');
|
|
26
|
-
|
|
27
18
|
const path = require('path');
|
|
28
|
-
/** @type {Options} */
|
|
29
|
-
|
|
30
19
|
|
|
20
|
+
/** @type {Options} */
|
|
31
21
|
const defualtOpts = {
|
|
32
22
|
localPath: 'dist',
|
|
33
23
|
noWarn: false,
|
|
@@ -36,6 +26,7 @@ const defualtOpts = {
|
|
|
36
26
|
ignore: ['**/*.LICENSE.txt'],
|
|
37
27
|
securityLock: true
|
|
38
28
|
};
|
|
29
|
+
|
|
39
30
|
/**
|
|
40
31
|
* get upload files
|
|
41
32
|
*
|
|
@@ -43,7 +34,6 @@ const defualtOpts = {
|
|
|
43
34
|
* @param {string} remotePath
|
|
44
35
|
* @param {string[]} ignore
|
|
45
36
|
*/
|
|
46
|
-
|
|
47
37
|
function getFilesPath(localPath, remotePath, ignore) {
|
|
48
38
|
const files = glob.sync(`${localPath}/**/*`, {
|
|
49
39
|
ignore: getSafePattern(ignore, localPath),
|
|
@@ -56,6 +46,7 @@ function getFilesPath(localPath, remotePath, ignore) {
|
|
|
56
46
|
};
|
|
57
47
|
});
|
|
58
48
|
}
|
|
49
|
+
|
|
59
50
|
/**
|
|
60
51
|
* get remote ls deep
|
|
61
52
|
*
|
|
@@ -67,8 +58,6 @@ function getFilesPath(localPath, remotePath, ignore) {
|
|
|
67
58
|
* @param {FilesOptions} [options]
|
|
68
59
|
* @return {Promise<{isDir:boolean;path:string}[]>}
|
|
69
60
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
61
|
async function getRemoteDeepFiles(sftp, remotePath, options) {
|
|
73
62
|
const {
|
|
74
63
|
patterns
|
|
@@ -77,13 +66,10 @@ async function getRemoteDeepFiles(sftp, remotePath, options) {
|
|
|
77
66
|
* @param {string} remotePath
|
|
78
67
|
* @returns {Promise<string[]|string>}
|
|
79
68
|
*/
|
|
80
|
-
|
|
81
69
|
async function getFiles(remotePath, data = []) {
|
|
82
70
|
const list = await sftp.list(remotePath);
|
|
83
|
-
|
|
84
71
|
for (const item of list) {
|
|
85
72
|
const path = remotePath + '/' + item.name;
|
|
86
|
-
|
|
87
73
|
if (item.type === 'd') {
|
|
88
74
|
data.push({
|
|
89
75
|
isDir: true,
|
|
@@ -97,30 +83,27 @@ async function getRemoteDeepFiles(sftp, remotePath, options) {
|
|
|
97
83
|
});
|
|
98
84
|
}
|
|
99
85
|
}
|
|
100
|
-
|
|
101
86
|
return data;
|
|
102
87
|
}
|
|
103
|
-
|
|
104
88
|
const ls = (await getFiles(remotePath)).filter(o => o.path);
|
|
105
|
-
|
|
106
89
|
if (patterns.length > 0) {
|
|
107
90
|
let tmp = ls;
|
|
108
91
|
const safePatterns = getSafePattern(patterns, remotePath);
|
|
109
92
|
tmp = tmp.filter(o => safePatterns.some(reg => minimatch(o.path, reg)));
|
|
110
93
|
return tmp;
|
|
111
94
|
}
|
|
112
|
-
|
|
113
95
|
return ls;
|
|
114
96
|
}
|
|
115
|
-
|
|
116
97
|
function ensureDiff(local, remote) {
|
|
117
98
|
return remote.map(file => {
|
|
118
99
|
const isSame = local.some(local => local.remotePath === file.path);
|
|
119
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
...file,
|
|
120
102
|
isSame
|
|
121
103
|
};
|
|
122
104
|
});
|
|
123
105
|
}
|
|
106
|
+
|
|
124
107
|
/**
|
|
125
108
|
* @typedef {Object} Options
|
|
126
109
|
* @property {string} [localPath]
|
|
@@ -133,12 +116,27 @@ function ensureDiff(local, remote) {
|
|
|
133
116
|
* @property {boolean} [keepAlive]
|
|
134
117
|
* @property {boolean} [noWarn]
|
|
135
118
|
* @property {boolean} [skipPrompt]
|
|
119
|
+
* @property {{remotePath:string;connectOptions?:import('ssh2').ConnectConfig}[]} [targets]
|
|
136
120
|
* @param {Options} opts
|
|
137
121
|
*/
|
|
138
122
|
|
|
139
|
-
|
|
140
123
|
async function sshSftp(opts) {
|
|
141
124
|
opts = parseOpts(opts);
|
|
125
|
+
const results = [];
|
|
126
|
+
// 这里只返回第一个目标的结果,是为了兼容以前的代码
|
|
127
|
+
for (const target of opts.targets) {
|
|
128
|
+
results.push(await _sshSftp({
|
|
129
|
+
...opts,
|
|
130
|
+
...target
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
return results[0];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {Options} opts
|
|
138
|
+
*/
|
|
139
|
+
async function _sshSftp(opts) {
|
|
142
140
|
const {
|
|
143
141
|
deployedURL,
|
|
144
142
|
sftpURL
|
|
@@ -146,14 +144,11 @@ async function sshSftp(opts) {
|
|
|
146
144
|
console.log('部署网址:', chalk.green(deployedURL));
|
|
147
145
|
const spinner = ora(`连接服务器 ${sftpURL}`).start();
|
|
148
146
|
const sftp = new Client();
|
|
149
|
-
|
|
150
147
|
try {
|
|
151
148
|
await sftp.connect(opts.connectOptions);
|
|
152
149
|
spinner.succeed(`已连接 ${sftpURL}`);
|
|
153
|
-
|
|
154
150
|
if (!(await sftp.exists(opts.remotePath))) {
|
|
155
151
|
let confirm = false;
|
|
156
|
-
|
|
157
152
|
if (opts.skipPrompt) {
|
|
158
153
|
confirm = true;
|
|
159
154
|
} else {
|
|
@@ -164,20 +159,17 @@ async function sshSftp(opts) {
|
|
|
164
159
|
});
|
|
165
160
|
confirm = ans.confirm;
|
|
166
161
|
}
|
|
167
|
-
|
|
168
162
|
if (confirm) {
|
|
169
163
|
await sftp.mkdir(opts.remotePath, true);
|
|
170
164
|
} else {
|
|
171
165
|
process.exit();
|
|
172
166
|
}
|
|
173
167
|
}
|
|
174
|
-
|
|
175
168
|
let remoteDeletefiles = [];
|
|
176
169
|
let localUploadFiles = [];
|
|
177
170
|
spinner.start('对比本地/远程的文件数量');
|
|
178
171
|
localUploadFiles = getFilesPath(opts.localPath, opts.remotePath, opts.ignore || []);
|
|
179
172
|
spinner.succeed(`本地文件数量:${localUploadFiles.length}`);
|
|
180
|
-
|
|
181
173
|
if (opts.cleanRemoteFiles) {
|
|
182
174
|
remoteDeletefiles = await getRemoteDeepFiles(sftp, opts.remotePath, {
|
|
183
175
|
patterns: opts.cleanRemoteFiles === true ? [] : opts.cleanRemoteFiles
|
|
@@ -190,7 +182,6 @@ async function sshSftp(opts) {
|
|
|
190
182
|
showDeleteFile: Symbol()
|
|
191
183
|
};
|
|
192
184
|
let confirm = Confirm.delete;
|
|
193
|
-
|
|
194
185
|
if (remoteDeletefiles.length > localUploadFiles.length && !opts.skipPrompt) {
|
|
195
186
|
const showSelect = async () => {
|
|
196
187
|
const {
|
|
@@ -215,14 +206,11 @@ async function sshSftp(opts) {
|
|
|
215
206
|
});
|
|
216
207
|
return confirm;
|
|
217
208
|
};
|
|
218
|
-
|
|
219
209
|
do {
|
|
220
210
|
confirm = await showSelect();
|
|
221
|
-
|
|
222
211
|
if (confirm === Confirm.stop) {
|
|
223
212
|
process.exit();
|
|
224
213
|
}
|
|
225
|
-
|
|
226
214
|
if (confirm === Confirm.showDeleteFile) {
|
|
227
215
|
const diffFiles = ensureDiff(localUploadFiles, remoteDeletefiles);
|
|
228
216
|
diffFiles.forEach(o => {
|
|
@@ -232,42 +220,33 @@ async function sshSftp(opts) {
|
|
|
232
220
|
}
|
|
233
221
|
} while (confirm === Confirm.showDeleteFile);
|
|
234
222
|
}
|
|
235
|
-
|
|
236
223
|
if (confirm === Confirm.delete) {
|
|
237
224
|
spinner.start('开始删除远程文件');
|
|
238
225
|
remoteDeletefiles = mergeDelete(remoteDeletefiles);
|
|
239
|
-
|
|
240
226
|
for (const i in remoteDeletefiles) {
|
|
241
227
|
const o = remoteDeletefiles[i];
|
|
242
228
|
spinner.text = `[${i + 1}/${remoteDeletefiles.length}] 正在删除 ${o.path}`;
|
|
243
229
|
if (o.isDir) await sftp.rmdir(o.path, true);else await sftp.delete(o.path);
|
|
244
230
|
}
|
|
245
|
-
|
|
246
231
|
spinner.succeed(`已删除 ${opts.remotePath}`);
|
|
247
232
|
}
|
|
248
233
|
}
|
|
249
|
-
|
|
250
234
|
spinner.start(`开始上传 ${opts.localPath} 到 ${opts.remotePath}`);
|
|
251
|
-
|
|
252
235
|
if (Array.isArray(opts.ignore) && opts.ignore.length > 0) {
|
|
253
236
|
for (const i in localUploadFiles) {
|
|
254
237
|
const o = localUploadFiles[i];
|
|
255
238
|
spinner.text = `[${i}/${localUploadFiles.length}] 正在上传 ${o.localPath} 到 ${o.remotePath}`;
|
|
256
|
-
|
|
257
239
|
if (fs.statSync(o.localPath).isDirectory()) {
|
|
258
240
|
if (!(await sftp.exists(o.remotePath))) {
|
|
259
241
|
await sftp.mkdir(o.remotePath);
|
|
260
242
|
}
|
|
261
|
-
|
|
262
243
|
continue;
|
|
263
244
|
}
|
|
264
|
-
|
|
265
245
|
await sftp.fastPut(o.localPath, o.remotePath);
|
|
266
246
|
}
|
|
267
247
|
} else {
|
|
268
248
|
await sftp.uploadDir(opts.localPath, opts.remotePath);
|
|
269
249
|
}
|
|
270
|
-
|
|
271
250
|
spinner.succeed(`已上传 ${opts.localPath} 到 ${opts.remotePath}`);
|
|
272
251
|
return {
|
|
273
252
|
sftp,
|
|
@@ -275,7 +254,6 @@ async function sshSftp(opts) {
|
|
|
275
254
|
};
|
|
276
255
|
} catch (error) {
|
|
277
256
|
spinner.fail('异常中断');
|
|
278
|
-
|
|
279
257
|
if (error.message.includes('sftpConnect')) {
|
|
280
258
|
exitWithError(`登录失败,请检查 connectOptions 配置项\n原始信息:${error.message}`);
|
|
281
259
|
} else {
|
|
@@ -283,51 +261,51 @@ async function sshSftp(opts) {
|
|
|
283
261
|
}
|
|
284
262
|
} finally {
|
|
285
263
|
if (!opts.keepAlive) {
|
|
286
|
-
sftp.end();
|
|
264
|
+
await sftp.end();
|
|
287
265
|
}
|
|
288
266
|
}
|
|
289
267
|
}
|
|
268
|
+
|
|
290
269
|
/**
|
|
291
270
|
* @param {Options} opts
|
|
292
271
|
* @param {{d:boolean,u:boolean,i:boolean}} lsOpts
|
|
293
272
|
*/
|
|
294
|
-
|
|
295
|
-
|
|
296
273
|
async function sshSftpLS(opts, lsOpts) {
|
|
297
274
|
opts = parseOpts(opts);
|
|
275
|
+
for (const target of opts.targets) {
|
|
276
|
+
await _sshSftpLS({
|
|
277
|
+
...opts,
|
|
278
|
+
...target
|
|
279
|
+
}, lsOpts);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function _sshSftpLS(opts, lsOpts) {
|
|
298
283
|
const sftp = new Client();
|
|
299
|
-
|
|
300
284
|
try {
|
|
301
285
|
await sftp.connect(opts.connectOptions);
|
|
302
|
-
|
|
303
286
|
if (lsOpts.d) {
|
|
304
287
|
if (opts.cleanRemoteFiles) {
|
|
305
288
|
const ls = await getRemoteDeepFiles(sftp, opts.remotePath, {
|
|
306
289
|
patterns: opts.cleanRemoteFiles === true ? [] : opts.cleanRemoteFiles
|
|
307
290
|
});
|
|
308
291
|
console.log(`删除文件 ${opts.remotePath}(${ls.length}):`);
|
|
309
|
-
|
|
310
292
|
for (const o of ls) {
|
|
311
293
|
console.log(` - ${o.path}`);
|
|
312
294
|
}
|
|
313
295
|
}
|
|
314
296
|
}
|
|
315
|
-
|
|
316
297
|
if (lsOpts.i && Array.isArray(opts.ignore) && opts.ignore.length > 0) {
|
|
317
298
|
let ls = glob.sync(`${opts.localPath}/**/*`);
|
|
318
299
|
ls = ls.filter(s => getSafePattern(opts.ignore, opts.localPath).some(reg => minimatch(s, reg)));
|
|
319
300
|
console.log(`忽略文件 (${ls.length}):`);
|
|
320
|
-
|
|
321
301
|
for (const s of ls) {
|
|
322
302
|
console.log(` - ${s}`);
|
|
323
303
|
}
|
|
324
304
|
}
|
|
325
|
-
|
|
326
305
|
if (lsOpts.u) {
|
|
327
306
|
if (opts.ignore && opts.ignore.length > 0) {
|
|
328
307
|
const ls = getFilesPath(opts.localPath, opts.remotePath, opts.ignore);
|
|
329
308
|
console.log(`上传文件 (${ls.length}): `);
|
|
330
|
-
|
|
331
309
|
for (const o of ls) {
|
|
332
310
|
console.log(` + ${o.localPath}`);
|
|
333
311
|
}
|
|
@@ -338,14 +316,13 @@ async function sshSftpLS(opts, lsOpts) {
|
|
|
338
316
|
} catch (error) {
|
|
339
317
|
console.error(error);
|
|
340
318
|
} finally {
|
|
341
|
-
sftp.end();
|
|
319
|
+
await sftp.end();
|
|
342
320
|
}
|
|
343
321
|
}
|
|
322
|
+
|
|
344
323
|
/**
|
|
345
324
|
* @param {{isDir:boolean;path:string}[]} files
|
|
346
325
|
*/
|
|
347
|
-
|
|
348
|
-
|
|
349
326
|
function mergeDelete(files) {
|
|
350
327
|
let dirs = files.filter(o => o.isDir);
|
|
351
328
|
dirs.forEach(({
|
|
@@ -355,122 +332,125 @@ function mergeDelete(files) {
|
|
|
355
332
|
});
|
|
356
333
|
return files;
|
|
357
334
|
}
|
|
335
|
+
|
|
358
336
|
/**
|
|
359
337
|
* @param {string[]} patterns
|
|
360
338
|
* @param {string} prefixPath
|
|
361
339
|
*/
|
|
362
|
-
|
|
363
|
-
|
|
364
340
|
function getSafePattern(patterns, prefixPath) {
|
|
365
341
|
const safePatterns = patterns.map(s => s.replace(/^[\.\/]*/, prefixPath + '/')).reduce((acc, s) => [...acc, s, s + '/**/*'], []);
|
|
366
342
|
return safePatterns;
|
|
367
343
|
}
|
|
344
|
+
|
|
368
345
|
/**
|
|
369
346
|
* @param {Options} opts
|
|
370
347
|
*/
|
|
371
|
-
|
|
372
|
-
|
|
373
348
|
function parseOpts(opts) {
|
|
374
349
|
opts = Object.assign({}, defualtOpts, opts);
|
|
375
350
|
process.env.__SFTP_NO_WARN = opts.noWarn;
|
|
376
|
-
|
|
377
351
|
const pkg = require(path.resolve('package.json'));
|
|
378
|
-
|
|
379
352
|
if (!pkg.name) {
|
|
380
353
|
exitWithError('package.json 中的 name 字段不能为空');
|
|
381
354
|
}
|
|
382
|
-
|
|
383
355
|
if (pkg.name.startsWith('@')) {
|
|
384
356
|
// 如果包含scope需要无视掉
|
|
385
357
|
pkg.name = pkg.name.replace(/@.*\//, '');
|
|
386
358
|
}
|
|
387
|
-
|
|
388
359
|
if (opts.preset && opts.preset.server) {
|
|
389
360
|
if (!servers[opts.preset.server]) exitWithError('未知的 preset.server');
|
|
390
|
-
const server = {
|
|
361
|
+
const server = {
|
|
362
|
+
...servers[opts.preset.server]
|
|
391
363
|
};
|
|
392
364
|
opts.connectOptions = server;
|
|
393
365
|
}
|
|
394
|
-
|
|
395
366
|
if (opts.preset && opts.preset.context) {
|
|
396
367
|
if (!presets[opts.preset.context]) exitWithError('未知的 preset.context');
|
|
397
|
-
const preset = {
|
|
368
|
+
const preset = {
|
|
369
|
+
...presets[opts.preset.context]
|
|
398
370
|
};
|
|
399
371
|
const folder = opts.preset.folder || pkg.name;
|
|
400
372
|
preset.remotePath = path.join(preset.remotePath, folder);
|
|
401
373
|
opts = Object.assign({}, preset, opts);
|
|
402
374
|
}
|
|
403
|
-
|
|
404
375
|
if (!fs.existsSync(opts.localPath)) {
|
|
405
376
|
exitWithError(`localPath 配置错误,未找到需要上传的文件夹(${opts.localPath})`);
|
|
406
377
|
}
|
|
407
|
-
|
|
408
378
|
if (!fs.statSync(opts.localPath).isDirectory()) {
|
|
409
379
|
exitWithError('localPath 配置错误,必须是一个文件夹');
|
|
410
380
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
381
|
+
opts.connectOptions = {
|
|
382
|
+
...opts.connectOptions,
|
|
383
|
+
readyTimeout: 2 * 60 * 1000
|
|
384
|
+
};
|
|
385
|
+
opts.targets = opts.targets || [];
|
|
386
|
+
if (opts.targets.length === 0 && !opts.remotePath) {
|
|
387
|
+
exitWithError('remotePath 或 targets 未配置');
|
|
388
|
+
}
|
|
389
|
+
if (opts.targets.length === 0) {
|
|
390
|
+
opts.targets.push({
|
|
391
|
+
remotePath: opts.remotePath,
|
|
392
|
+
connectOptions: opts.connectOptions
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
opts.targets.forEach(target => {
|
|
396
|
+
target.connectOptions = target.connectOptions || opts.connectOptions;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (opts.targets.length > 1 && opts.keepAlive) {
|
|
400
|
+
exitWithError('keepAlive 选项在多目标上传时不支持,请设置为 false');
|
|
414
401
|
}
|
|
415
|
-
|
|
416
402
|
if (typeof opts.securityLock !== 'boolean') {
|
|
417
403
|
opts.securityLock = true;
|
|
418
404
|
}
|
|
419
|
-
|
|
420
405
|
if (opts.securityLock === false) {
|
|
421
406
|
warn('请确保自己清楚关闭安全锁(securityLock)后的风险');
|
|
422
407
|
} else {
|
|
423
|
-
|
|
424
|
-
|
|
408
|
+
for (const target of opts.targets) {
|
|
409
|
+
if (!target.remotePath.includes(pkg.name)) {
|
|
410
|
+
exitWithError([`remotePath 中不包含项目名称`, `为防止错误上传/删除和保证服务器目录可读性,你必须让remotePath中包含你的项目名称`, `remotePath:${target.remotePath}`, `项目名称:${pkg.name} // 源自 package.json 中的 name 字段,忽略scope字段`, `\n你可以设置 "securityLock": false 来关闭这个验证`].join('\n'));
|
|
411
|
+
}
|
|
425
412
|
}
|
|
426
413
|
}
|
|
427
|
-
|
|
428
414
|
if (opts.ignore) {
|
|
429
415
|
opts.ignore = [opts.ignore].flat(1).filter(Boolean);
|
|
430
416
|
}
|
|
431
|
-
|
|
432
417
|
if (opts.cleanRemoteFiles === true) {
|
|
433
418
|
opts.cleanRemoteFiles = [];
|
|
434
419
|
}
|
|
435
|
-
|
|
436
420
|
if (opts.cleanRemoteFiles) {
|
|
437
421
|
opts.cleanRemoteFiles = [opts.cleanRemoteFiles].flat(1).filter(Boolean);
|
|
438
422
|
}
|
|
439
|
-
|
|
440
423
|
return opts;
|
|
441
424
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
const sftpURL = `sftp://${_r.username}:${_r.password}@${_r.host}:${_r.port}/${opts.remotePath}`;
|
|
425
|
+
function getDeployURL(target) {
|
|
426
|
+
const _r = target.connectOptions;
|
|
427
|
+
const sftpURL = `sftp://${_r.username}:${_r.password}@${_r.host}:${_r.port}/${target.remotePath}`;
|
|
446
428
|
let deployedURL = '未知';
|
|
447
|
-
|
|
448
429
|
for (const context in presets) {
|
|
449
430
|
const preset = presets[context];
|
|
450
|
-
if (!
|
|
451
|
-
const fullPath =
|
|
431
|
+
if (!target.remotePath.startsWith(preset.remotePath)) continue;
|
|
432
|
+
const fullPath = target.remotePath.replace(preset.remotePath, '').slice(1);
|
|
452
433
|
deployedURL = ['http://www.zhidianbao.cn:8088', context, fullPath, ''].join('/');
|
|
453
434
|
break;
|
|
454
435
|
}
|
|
455
|
-
|
|
456
436
|
return {
|
|
457
437
|
sftpURL,
|
|
458
438
|
deployedURL
|
|
459
439
|
};
|
|
460
440
|
}
|
|
441
|
+
|
|
461
442
|
/**
|
|
462
443
|
* @param {Options} opts
|
|
463
444
|
*/
|
|
464
|
-
|
|
465
|
-
|
|
466
445
|
function sshSftpShowUrl(opts) {
|
|
467
446
|
opts = parseOpts(opts);
|
|
468
|
-
const {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
447
|
+
for (const target of opts.targets) {
|
|
448
|
+
const {
|
|
449
|
+
deployedURL
|
|
450
|
+
} = getDeployURL(target);
|
|
451
|
+
console.log('部署网址:', chalk.green(deployedURL));
|
|
452
|
+
}
|
|
472
453
|
}
|
|
473
|
-
|
|
474
454
|
function sshSftpShowConfig(opts) {
|
|
475
455
|
opts = parseOpts(opts);
|
|
476
456
|
console.log(JSON.stringify(opts, null, 2));
|
|
@@ -481,7 +461,6 @@ function sshSftpShowConfig(opts) {
|
|
|
481
461
|
i: true
|
|
482
462
|
});
|
|
483
463
|
}
|
|
484
|
-
|
|
485
464
|
module.exports = sshSftp;
|
|
486
465
|
module.exports.sshSftp = sshSftp;
|
|
487
466
|
module.exports.sshSftpLS = sshSftpLS;
|
package/lib/utils.js
CHANGED
|
@@ -1,40 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
+
|
|
4
5
|
/**
|
|
5
6
|
* @param {string} message
|
|
6
7
|
* @return {never}
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
9
|
function exitWithError(message) {
|
|
11
10
|
console.log(`${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(message)}`);
|
|
12
11
|
process.exit();
|
|
13
12
|
}
|
|
14
|
-
|
|
15
13
|
function warn(message) {
|
|
16
14
|
if (process.env.__SFTP_NO_WARN) return;
|
|
17
15
|
console.log(`${chalk.bgYellow.white.bold(' WARN ')} ${chalk.yellow.bold(message)}`);
|
|
18
16
|
}
|
|
19
|
-
|
|
20
17
|
function splitOnFirst(string, separator) {
|
|
21
18
|
if (!(typeof string === 'string' && typeof separator === 'string')) {
|
|
22
19
|
throw new TypeError('Expected the arguments to be of type `string`');
|
|
23
20
|
}
|
|
24
|
-
|
|
25
21
|
if (string === '' || separator === '') {
|
|
26
22
|
return [];
|
|
27
23
|
}
|
|
28
|
-
|
|
29
24
|
const separatorIndex = string.indexOf(separator);
|
|
30
|
-
|
|
31
25
|
if (separatorIndex === -1) {
|
|
32
26
|
return [];
|
|
33
27
|
}
|
|
34
|
-
|
|
35
28
|
return [string.slice(0, separatorIndex), string.slice(separatorIndex + separator.length)];
|
|
36
29
|
}
|
|
37
|
-
|
|
38
30
|
module.exports.splitOnFirst = splitOnFirst;
|
|
39
31
|
module.exports.exitWithError = exitWithError;
|
|
40
32
|
module.exports.warn = warn;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qse/ssh-sftp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "教育代码部署工具",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"author": "Ironkinoko <kinoko_main@outlook.com>",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"scripts": {
|
|
9
9
|
"start": "dumi dev",
|
|
10
10
|
"docs:build": "dumi build && cp sftprc.schema.json docs-dist",
|
|
11
|
-
"docs:deploy": "
|
|
11
|
+
"docs:deploy": "node ./src/cli.js",
|
|
12
12
|
"build": "father-build",
|
|
13
13
|
"deploy": "yarn docs:build && yarn docs:deploy && rm -rf docs-dist",
|
|
14
14
|
"release": "yarn build && npm publish && rm -rf lib",
|