@qse/ssh-sftp 1.1.0 → 1.3.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/lib/index.js DELETED
@@ -1,470 +0,0 @@
1
- "use strict";
2
-
3
- const Client = require('ssh2-sftp-client');
4
- const ora = require('ora');
5
- const glob = require('glob');
6
- const fs = require('fs');
7
- const minimatch = require('minimatch');
8
- const inquirer = require('inquirer');
9
- const {
10
- exitWithError,
11
- warn
12
- } = require('./utils');
13
- const chalk = require('chalk');
14
- const {
15
- presets,
16
- servers
17
- } = require('./presets');
18
- const path = require('path');
19
-
20
- /** @type {Options} */
21
- const defualtOpts = {
22
- localPath: 'dist',
23
- noWarn: false,
24
- keepAlive: false,
25
- cleanRemoteFiles: true,
26
- ignore: ['**/*.LICENSE.txt'],
27
- securityLock: true
28
- };
29
-
30
- /**
31
- * get upload files
32
- *
33
- * @param {string} localPath
34
- * @param {string} remotePath
35
- * @param {string[]} ignore
36
- */
37
- function getFilesPath(localPath, remotePath, ignore) {
38
- const files = glob.sync(`${localPath}/**/*`, {
39
- ignore: getSafePattern(ignore, localPath),
40
- dot: true
41
- });
42
- return files.map(localFilePath => {
43
- return {
44
- localPath: localFilePath,
45
- remotePath: localFilePath.replace(localPath, remotePath)
46
- };
47
- });
48
- }
49
-
50
- /**
51
- * get remote ls deep
52
- *
53
- * @typedef {Object} FilesOptions
54
- * @property {true|string[]} [patterns]
55
- *
56
- * @param {Client} sftp
57
- * @param {string} remotePath
58
- * @param {FilesOptions} [options]
59
- * @return {Promise<{isDir:boolean;path:string}[]>}
60
- */
61
- async function getRemoteDeepFiles(sftp, remotePath, options) {
62
- const {
63
- patterns
64
- } = options;
65
- /**
66
- * @param {string} remotePath
67
- * @returns {Promise<string[]|string>}
68
- */
69
- async function getFiles(remotePath, data = []) {
70
- const list = await sftp.list(remotePath);
71
- for (const item of list) {
72
- const path = remotePath + '/' + item.name;
73
- if (item.type === 'd') {
74
- data.push({
75
- isDir: true,
76
- path
77
- });
78
- await getFiles(path, data);
79
- } else {
80
- data.push({
81
- isDir: false,
82
- path
83
- });
84
- }
85
- }
86
- return data;
87
- }
88
- const ls = (await getFiles(remotePath)).filter(o => o.path);
89
- if (patterns.length > 0) {
90
- let tmp = ls;
91
- const safePatterns = getSafePattern(patterns, remotePath);
92
- tmp = tmp.filter(o => safePatterns.some(reg => minimatch(o.path, reg)));
93
- return tmp;
94
- }
95
- return ls;
96
- }
97
- function ensureDiff(local, remote) {
98
- return remote.map(file => {
99
- const isSame = local.some(local => local.remotePath === file.path);
100
- return {
101
- ...file,
102
- isSame
103
- };
104
- });
105
- }
106
-
107
- /**
108
- * @typedef {Object} Options
109
- * @property {string} [localPath]
110
- * @property {string} [remotePath]
111
- * @property {{context?:string;folder?:string;server?:string}} [preset]
112
- * @property {import('ssh2').ConnectConfig} [connectOptions]
113
- * @property {string[]} [ignore]
114
- * @property {boolean|string[]} [cleanRemoteFiles]
115
- * @property {boolean} [securityLock]
116
- * @property {boolean} [keepAlive]
117
- * @property {boolean} [noWarn]
118
- * @property {boolean} [skipPrompt]
119
- * @property {{remotePath:string;connectOptions?:import('ssh2').ConnectConfig}[]} [targets]
120
- * @param {Options} opts
121
- */
122
-
123
- async function sshSftp(opts) {
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) {
140
- const {
141
- deployedURL,
142
- sftpURL
143
- } = getDeployURL(opts);
144
- console.log('部署网址:', chalk.green(deployedURL));
145
- const spinner = ora(`连接服务器 ${sftpURL}`).start();
146
- const sftp = new Client();
147
- try {
148
- await sftp.connect(opts.connectOptions);
149
- spinner.succeed(`已连接 ${sftpURL}`);
150
- if (!(await sftp.exists(opts.remotePath))) {
151
- let confirm = false;
152
- if (opts.skipPrompt) {
153
- confirm = true;
154
- } else {
155
- const ans = await inquirer.prompt({
156
- name: 'confirm',
157
- message: `远程文件夹不存在,是否要创建一个`,
158
- type: 'confirm'
159
- });
160
- confirm = ans.confirm;
161
- }
162
- if (confirm) {
163
- await sftp.mkdir(opts.remotePath, true);
164
- } else {
165
- process.exit();
166
- }
167
- }
168
- let remoteDeletefiles = [];
169
- let localUploadFiles = [];
170
- spinner.start('对比本地/远程的文件数量');
171
- localUploadFiles = getFilesPath(opts.localPath, opts.remotePath, opts.ignore || []);
172
- spinner.succeed(`本地文件数量:${localUploadFiles.length}`);
173
- if (opts.cleanRemoteFiles) {
174
- remoteDeletefiles = await getRemoteDeepFiles(sftp, opts.remotePath, {
175
- patterns: opts.cleanRemoteFiles === true ? [] : opts.cleanRemoteFiles
176
- });
177
- spinner.succeed(`远程文件数量:${remoteDeletefiles.length}`);
178
- const Confirm = {
179
- delete: Symbol(),
180
- skip: Symbol(),
181
- stop: Symbol(),
182
- showDeleteFile: Symbol()
183
- };
184
- let confirm = Confirm.delete;
185
- if (remoteDeletefiles.length > localUploadFiles.length && !opts.skipPrompt) {
186
- const showSelect = async () => {
187
- const {
188
- confirm
189
- } = await inquirer.prompt({
190
- name: 'confirm',
191
- message: `远程需要删除的文件数(${remoteDeletefiles.length})比本地(${localUploadFiles.length})多,确定要删除吗?`,
192
- type: 'list',
193
- choices: [{
194
- name: '删除',
195
- value: Confirm.delete
196
- }, {
197
- name: '不删除,继续部署',
198
- value: Confirm.skip
199
- }, {
200
- name: '中止部署',
201
- value: Confirm.stop
202
- }, {
203
- name: '显示需要删除的文件',
204
- value: Confirm.showDeleteFile
205
- }]
206
- });
207
- return confirm;
208
- };
209
- do {
210
- confirm = await showSelect();
211
- if (confirm === Confirm.stop) {
212
- process.exit();
213
- }
214
- if (confirm === Confirm.showDeleteFile) {
215
- const diffFiles = ensureDiff(localUploadFiles, remoteDeletefiles);
216
- diffFiles.forEach(o => {
217
- const path = o.isSame ? o.path : chalk.red(o.path);
218
- console.log(` - ${path}`);
219
- });
220
- }
221
- } while (confirm === Confirm.showDeleteFile);
222
- }
223
- if (confirm === Confirm.delete) {
224
- spinner.start('开始删除远程文件');
225
- remoteDeletefiles = mergeDelete(remoteDeletefiles);
226
- for (const i in remoteDeletefiles) {
227
- const o = remoteDeletefiles[i];
228
- spinner.text = `[${i + 1}/${remoteDeletefiles.length}] 正在删除 ${o.path}`;
229
- if (o.isDir) await sftp.rmdir(o.path, true);else await sftp.delete(o.path);
230
- }
231
- spinner.succeed(`已删除 ${opts.remotePath}`);
232
- }
233
- }
234
- spinner.start(`开始上传 ${opts.localPath} 到 ${opts.remotePath}`);
235
- if (Array.isArray(opts.ignore) && opts.ignore.length > 0) {
236
- for (const i in localUploadFiles) {
237
- const o = localUploadFiles[i];
238
- spinner.text = `[${i}/${localUploadFiles.length}] 正在上传 ${o.localPath} 到 ${o.remotePath}`;
239
- if (fs.statSync(o.localPath).isDirectory()) {
240
- if (!(await sftp.exists(o.remotePath))) {
241
- await sftp.mkdir(o.remotePath);
242
- }
243
- continue;
244
- }
245
- await sftp.fastPut(o.localPath, o.remotePath);
246
- }
247
- } else {
248
- await sftp.uploadDir(opts.localPath, opts.remotePath);
249
- }
250
- spinner.succeed(`已上传 ${opts.localPath} 到 ${opts.remotePath}`);
251
- return {
252
- sftp,
253
- opts
254
- };
255
- } catch (error) {
256
- spinner.fail('异常中断');
257
- if (error.message.includes('sftpConnect')) {
258
- exitWithError(`登录失败,请检查 connectOptions 配置项\n原始信息:${error.message}`);
259
- } else {
260
- console.error(error);
261
- }
262
- } finally {
263
- if (!opts.keepAlive) {
264
- await sftp.end();
265
- }
266
- }
267
- }
268
-
269
- /**
270
- * @param {Options} opts
271
- * @param {{d:boolean,u:boolean,i:boolean}} lsOpts
272
- */
273
- async function sshSftpLS(opts, lsOpts) {
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) {
283
- const sftp = new Client();
284
- try {
285
- await sftp.connect(opts.connectOptions);
286
- if (lsOpts.d) {
287
- if (opts.cleanRemoteFiles) {
288
- const ls = await getRemoteDeepFiles(sftp, opts.remotePath, {
289
- patterns: opts.cleanRemoteFiles === true ? [] : opts.cleanRemoteFiles
290
- });
291
- console.log(`删除文件 ${opts.remotePath}(${ls.length}):`);
292
- for (const o of ls) {
293
- console.log(` - ${o.path}`);
294
- }
295
- }
296
- }
297
- if (lsOpts.i && Array.isArray(opts.ignore) && opts.ignore.length > 0) {
298
- let ls = glob.sync(`${opts.localPath}/**/*`);
299
- ls = ls.filter(s => getSafePattern(opts.ignore, opts.localPath).some(reg => minimatch(s, reg)));
300
- console.log(`忽略文件 (${ls.length}):`);
301
- for (const s of ls) {
302
- console.log(` - ${s}`);
303
- }
304
- }
305
- if (lsOpts.u) {
306
- if (opts.ignore && opts.ignore.length > 0) {
307
- const ls = getFilesPath(opts.localPath, opts.remotePath, opts.ignore);
308
- console.log(`上传文件 (${ls.length}): `);
309
- for (const o of ls) {
310
- console.log(` + ${o.localPath}`);
311
- }
312
- } else {
313
- console.log(`上传 ${opts.localPath} 全部文件到 ${opts.remotePath}`);
314
- }
315
- }
316
- } catch (error) {
317
- console.error(error);
318
- } finally {
319
- await sftp.end();
320
- }
321
- }
322
-
323
- /**
324
- * @param {{isDir:boolean;path:string}[]} files
325
- */
326
- function mergeDelete(files) {
327
- let dirs = files.filter(o => o.isDir);
328
- dirs.forEach(({
329
- path
330
- }) => {
331
- files = files.filter(o => !(o.path.startsWith(path) && path !== o.path));
332
- });
333
- return files;
334
- }
335
-
336
- /**
337
- * @param {string[]} patterns
338
- * @param {string} prefixPath
339
- */
340
- function getSafePattern(patterns, prefixPath) {
341
- const safePatterns = patterns.map(s => s.replace(/^[\.\/]*/, prefixPath + '/')).reduce((acc, s) => [...acc, s, s + '/**/*'], []);
342
- return safePatterns;
343
- }
344
-
345
- /**
346
- * @param {Options} opts
347
- */
348
- function parseOpts(opts) {
349
- opts = Object.assign({}, defualtOpts, opts);
350
- process.env.__SFTP_NO_WARN = opts.noWarn;
351
- const pkg = require(path.resolve('package.json'));
352
- if (!pkg.name) {
353
- exitWithError('package.json 中的 name 字段不能为空');
354
- }
355
- if (pkg.name.startsWith('@')) {
356
- // 如果包含scope需要无视掉
357
- pkg.name = pkg.name.replace(/@.*\//, '');
358
- }
359
- if (opts.preset && opts.preset.server) {
360
- if (!servers[opts.preset.server]) exitWithError('未知的 preset.server');
361
- const server = {
362
- ...servers[opts.preset.server]
363
- };
364
- opts.connectOptions = server;
365
- }
366
- if (opts.preset && opts.preset.context) {
367
- if (!presets[opts.preset.context]) exitWithError('未知的 preset.context');
368
- const preset = {
369
- ...presets[opts.preset.context]
370
- };
371
- const folder = opts.preset.folder || pkg.name;
372
- preset.remotePath = path.join(preset.remotePath, folder);
373
- opts = Object.assign({}, preset, opts);
374
- }
375
- if (!fs.existsSync(opts.localPath)) {
376
- exitWithError(`localPath 配置错误,未找到需要上传的文件夹(${opts.localPath})`);
377
- }
378
- if (!fs.statSync(opts.localPath).isDirectory()) {
379
- exitWithError('localPath 配置错误,必须是一个文件夹');
380
- }
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');
401
- }
402
- if (typeof opts.securityLock !== 'boolean') {
403
- opts.securityLock = true;
404
- }
405
- if (opts.securityLock === false) {
406
- warn('请确保自己清楚关闭安全锁(securityLock)后的风险');
407
- } else {
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
- }
412
- }
413
- }
414
- if (opts.ignore) {
415
- opts.ignore = [opts.ignore].flat(1).filter(Boolean);
416
- }
417
- if (opts.cleanRemoteFiles === true) {
418
- opts.cleanRemoteFiles = [];
419
- }
420
- if (opts.cleanRemoteFiles) {
421
- opts.cleanRemoteFiles = [opts.cleanRemoteFiles].flat(1).filter(Boolean);
422
- }
423
- return opts;
424
- }
425
- function getDeployURL(target) {
426
- const _r = target.connectOptions;
427
- const sftpURL = `sftp://${_r.username}:${_r.password}@${_r.host}:${_r.port}/${target.remotePath}`;
428
- let deployedURL = '未知';
429
- for (const context in presets) {
430
- const preset = presets[context];
431
- if (!target.remotePath.startsWith(preset.remotePath)) continue;
432
- const fullPath = target.remotePath.replace(preset.remotePath, '').slice(1);
433
- deployedURL = ['http://www.zhidianbao.cn:8088', context, fullPath, ''].join('/');
434
- break;
435
- }
436
- return {
437
- sftpURL,
438
- deployedURL
439
- };
440
- }
441
-
442
- /**
443
- * @param {Options} opts
444
- */
445
- function sshSftpShowUrl(opts) {
446
- opts = parseOpts(opts);
447
- for (const target of opts.targets) {
448
- const {
449
- deployedURL
450
- } = getDeployURL(target);
451
- console.log('部署网址:', chalk.green(deployedURL));
452
- }
453
- }
454
- function sshSftpShowConfig(opts) {
455
- opts = parseOpts(opts);
456
- console.log(JSON.stringify(opts, null, 2));
457
- sshSftpShowUrl(opts);
458
- sshSftpLS(opts, {
459
- u: true,
460
- d: true,
461
- i: true
462
- });
463
- }
464
- module.exports = sshSftp;
465
- module.exports.sshSftp = sshSftp;
466
- module.exports.sshSftpLS = sshSftpLS;
467
- module.exports.sshSftpShowUrl = sshSftpShowUrl;
468
- module.exports.sshSftpShowConfig = sshSftpShowConfig;
469
- module.exports.parseOpts = parseOpts;
470
- module.exports.Client = Client;
package/lib/utils.js DELETED
@@ -1,32 +0,0 @@
1
- "use strict";
2
-
3
- const chalk = require('chalk');
4
-
5
- /**
6
- * @param {string} message
7
- * @return {never}
8
- */
9
- function exitWithError(message) {
10
- console.log(`${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(message)}`);
11
- process.exit();
12
- }
13
- function warn(message) {
14
- if (process.env.__SFTP_NO_WARN) return;
15
- console.log(`${chalk.bgYellow.white.bold(' WARN ')} ${chalk.yellow.bold(message)}`);
16
- }
17
- function splitOnFirst(string, separator) {
18
- if (!(typeof string === 'string' && typeof separator === 'string')) {
19
- throw new TypeError('Expected the arguments to be of type `string`');
20
- }
21
- if (string === '' || separator === '') {
22
- return [];
23
- }
24
- const separatorIndex = string.indexOf(separator);
25
- if (separatorIndex === -1) {
26
- return [];
27
- }
28
- return [string.slice(0, separatorIndex), string.slice(separatorIndex + separator.length)];
29
- }
30
- module.exports.splitOnFirst = splitOnFirst;
31
- module.exports.exitWithError = exitWithError;
32
- module.exports.warn = warn;