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