@qse/ssh-sftp 1.2.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/CHANGELOG.md +4 -0
- package/eslint.config.mjs +3 -0
- package/package.json +7 -4
- package/src/cli.js +6 -14
- package/src/index.js +74 -41
- package/src/utils.js +23 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qse/ssh-sftp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "教育代码部署工具",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"author": "Ironkinoko <kinoko_main@outlook.com>",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"docs:build": "mkdir -p docs-dist && cp sftprc.schema.json docs-dist",
|
|
11
11
|
"docs:deploy": "node ./src/cli.js",
|
|
12
12
|
"deploy": "npm run docs:build && npm run docs:deploy && rm -rf docs-dist",
|
|
13
|
-
"release": "npm publish
|
|
13
|
+
"release": "npm publish",
|
|
14
14
|
"prettier": "prettier -c -w \"src/**/*.{js,jsx,tsx,ts,less,md,json}\"",
|
|
15
15
|
"postversion": "npm run release"
|
|
16
16
|
},
|
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
"inquirer": "^13.1.0",
|
|
32
32
|
"minimatch": "^10.1.1",
|
|
33
33
|
"ora": "^9.0.0",
|
|
34
|
+
"p-limit": "^7.3.0",
|
|
34
35
|
"ssh2-sftp-client": "^12.0.1",
|
|
35
|
-
"update-notifier": "^7.3.1",
|
|
36
36
|
"yargs": "^18.0.0"
|
|
37
37
|
},
|
|
38
|
-
"devDependencies": {
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"eslint": "^9.39.3",
|
|
40
|
+
"@qse/eslint-config": "^1.0.0"
|
|
41
|
+
}
|
|
39
42
|
}
|
package/src/cli.js
CHANGED
|
@@ -4,21 +4,13 @@ import { sshSftp, sshSftpLS, sshSftpShowUrl, sshSftpShowConfig } from './index.j
|
|
|
4
4
|
import fs from 'fs'
|
|
5
5
|
import ora from 'ora'
|
|
6
6
|
import { exitWithError } from './utils.js'
|
|
7
|
-
import updateNotifier from 'update-notifier'
|
|
8
7
|
import { presets } from './presets.js'
|
|
9
8
|
import Yargs from 'yargs'
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
updateNotifier({
|
|
14
|
-
pkg,
|
|
15
|
-
updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
|
|
16
|
-
shouldNotifyInNpmScript: true,
|
|
17
|
-
}).notify()
|
|
18
|
-
|
|
10
|
+
// eslint-disable-next-line no-unused-expressions
|
|
19
11
|
Yargs(process.argv.slice(2))
|
|
20
12
|
.usage(
|
|
21
|
-
'使用: $0 [command] \n\n代码:svn://192.168.10.168/edu/code/A0.New-system/0A2.front-end-component/ssh-sftp/trunk'
|
|
13
|
+
'使用: $0 [command] \n\n代码:svn://192.168.10.168/edu/code/A0.New-system/0A2.front-end-component/ssh-sftp/trunk',
|
|
22
14
|
)
|
|
23
15
|
.command(
|
|
24
16
|
'*',
|
|
@@ -29,7 +21,7 @@ Yargs(process.argv.slice(2))
|
|
|
29
21
|
desc: '不存在的目录不再询问,直接创建',
|
|
30
22
|
type: 'boolean',
|
|
31
23
|
}),
|
|
32
|
-
upload
|
|
24
|
+
upload,
|
|
33
25
|
)
|
|
34
26
|
.command('init', '生成 .sftprc.json 配置文件', {}, generateDefaultConfigJSON)
|
|
35
27
|
.command(
|
|
@@ -40,7 +32,7 @@ Yargs(process.argv.slice(2))
|
|
|
40
32
|
.option('u', { desc: '列出需要上传的文件' })
|
|
41
33
|
.option('d', { desc: '列出需要删除的文件' })
|
|
42
34
|
.option('i', { desc: '列出忽略的文件' }),
|
|
43
|
-
ls
|
|
35
|
+
ls,
|
|
44
36
|
)
|
|
45
37
|
.command(['show-config', 'sc'], '显示部署的完整信息', {}, showConfig)
|
|
46
38
|
.command(['show-presets', 'sp'], '显示预设配置', {}, showPresets)
|
|
@@ -97,9 +89,9 @@ function generateDefaultConfigJSON() {
|
|
|
97
89
|
cleanRemoteFiles: false,
|
|
98
90
|
},
|
|
99
91
|
null,
|
|
100
|
-
2
|
|
92
|
+
2,
|
|
101
93
|
),
|
|
102
|
-
{ encoding: 'utf-8' }
|
|
94
|
+
{ encoding: 'utf-8' },
|
|
103
95
|
)
|
|
104
96
|
ora().succeed('.sftprc.json 生成在项目根目录')
|
|
105
97
|
}
|
package/src/index.js
CHANGED
|
@@ -4,10 +4,13 @@ import * as glob from 'glob'
|
|
|
4
4
|
import fs from 'fs'
|
|
5
5
|
import * as minimatch from 'minimatch'
|
|
6
6
|
import inquirer from 'inquirer'
|
|
7
|
-
import { exitWithError, warn } from './utils.js'
|
|
7
|
+
import { exitWithError, injectTiming, warn } from './utils.js'
|
|
8
8
|
import chalk from 'chalk'
|
|
9
9
|
import { presets, servers } from './presets.js'
|
|
10
10
|
import path from 'path'
|
|
11
|
+
import pLimit from 'p-limit'
|
|
12
|
+
|
|
13
|
+
const limit = pLimit(6)
|
|
11
14
|
|
|
12
15
|
/** @type {Options} */
|
|
13
16
|
const defualtOpts = {
|
|
@@ -52,30 +55,40 @@ function getFilesPath(localPath, remotePath, ignore) {
|
|
|
52
55
|
*/
|
|
53
56
|
async function getRemoteDeepFiles(sftp, remotePath, options) {
|
|
54
57
|
const { patterns } = options
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
const data = []
|
|
59
|
+
const queue = [remotePath]
|
|
60
|
+
const LIST_CONCURRENCY = 6
|
|
61
|
+
|
|
62
|
+
while (queue.length > 0) {
|
|
63
|
+
const batch = queue.splice(0, LIST_CONCURRENCY)
|
|
64
|
+
const results = await Promise.all(
|
|
65
|
+
batch.map(async (base) => {
|
|
66
|
+
try {
|
|
67
|
+
return { base, list: await sftp.list(base) }
|
|
68
|
+
} catch {
|
|
69
|
+
return { base, list: [] }
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
for (const { base, list } of results) {
|
|
75
|
+
for (const item of list) {
|
|
76
|
+
const p = `${base}/${item.name}`
|
|
77
|
+
if (item.type === 'd') {
|
|
78
|
+
data.push({ isDir: true, path: p })
|
|
79
|
+
queue.push(p)
|
|
80
|
+
} else {
|
|
81
|
+
data.push({ isDir: false, path: p })
|
|
82
|
+
}
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
|
-
return data
|
|
71
85
|
}
|
|
72
|
-
|
|
86
|
+
|
|
87
|
+
const ls = data.filter((o) => o.path)
|
|
73
88
|
|
|
74
89
|
if (patterns.length > 0) {
|
|
75
|
-
let tmp = ls
|
|
76
90
|
const safePatterns = getSafePattern(patterns, remotePath)
|
|
77
|
-
|
|
78
|
-
return tmp
|
|
91
|
+
return ls.filter((o) => safePatterns.some((reg) => minimatch(o.path, reg)))
|
|
79
92
|
}
|
|
80
93
|
return ls
|
|
81
94
|
}
|
|
@@ -121,9 +134,8 @@ async function _sshSftp(opts) {
|
|
|
121
134
|
const { deployedURL, sftpURL } = getDeployURL(opts)
|
|
122
135
|
|
|
123
136
|
console.log('部署网址:', chalk.green(deployedURL))
|
|
124
|
-
const spinner = ora(`连接服务器 ${sftpURL}`).start()
|
|
137
|
+
const spinner = injectTiming(ora(`连接服务器 ${sftpURL}`)).start()
|
|
125
138
|
const sftp = new Client()
|
|
126
|
-
|
|
127
139
|
try {
|
|
128
140
|
await sftp.connect(opts.connectOptions)
|
|
129
141
|
spinner.succeed(`已连接 ${sftpURL}`)
|
|
@@ -205,12 +217,19 @@ async function _sshSftp(opts) {
|
|
|
205
217
|
if (confirm === Confirm.delete) {
|
|
206
218
|
spinner.start('开始删除远程文件')
|
|
207
219
|
remoteDeletefiles = mergeDelete(remoteDeletefiles)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
await Promise.all(
|
|
221
|
+
remoteDeletefiles.map((o, i) =>
|
|
222
|
+
limit(async () => {
|
|
223
|
+
spinner.text = `[${i + 1}/${remoteDeletefiles.length}] 正在删除 ${o.path}`
|
|
224
|
+
try {
|
|
225
|
+
if (o.isDir) await sftp.rmdir(o.path, true)
|
|
226
|
+
else await sftp.delete(o.path)
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.error(`删除失败: ${o.path}`, e.message)
|
|
229
|
+
}
|
|
230
|
+
}),
|
|
231
|
+
),
|
|
232
|
+
)
|
|
214
233
|
spinner.succeed(`已删除 ${opts.remotePath}`)
|
|
215
234
|
}
|
|
216
235
|
}
|
|
@@ -218,17 +237,31 @@ async function _sshSftp(opts) {
|
|
|
218
237
|
spinner.start(`开始上传 ${opts.localPath} 到 ${opts.remotePath}`)
|
|
219
238
|
|
|
220
239
|
if (Array.isArray(opts.ignore) && opts.ignore.length > 0) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
240
|
+
const dirs = localUploadFiles.filter((o) => fs.statSync(o.localPath).isDirectory())
|
|
241
|
+
const files = localUploadFiles.filter((o) => !fs.statSync(o.localPath).isDirectory())
|
|
242
|
+
|
|
243
|
+
// 先并发创建目录
|
|
244
|
+
await Promise.all(
|
|
245
|
+
dirs.map((o) =>
|
|
246
|
+
limit(async () => {
|
|
247
|
+
try {
|
|
248
|
+
if (!(await sftp.exists(o.remotePath))) {
|
|
249
|
+
await sftp.mkdir(o.remotePath)
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// 再并发上传文件
|
|
257
|
+
await Promise.all(
|
|
258
|
+
files.map((o, i) =>
|
|
259
|
+
limit(async () => {
|
|
260
|
+
spinner.text = `[${i + 1}/${files.length}] 正在上传 ${o.localPath} 到 ${o.remotePath}`
|
|
261
|
+
await sftp.fastPut(o.localPath, o.remotePath)
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
)
|
|
232
265
|
} else {
|
|
233
266
|
await sftp.uploadDir(opts.localPath, opts.remotePath)
|
|
234
267
|
}
|
|
@@ -281,7 +314,7 @@ async function _sshSftpLS(opts, lsOpts) {
|
|
|
281
314
|
if (lsOpts.i && Array.isArray(opts.ignore) && opts.ignore.length > 0) {
|
|
282
315
|
let ls = glob.sync(`${opts.localPath}/**/*`)
|
|
283
316
|
ls = ls.filter((s) =>
|
|
284
|
-
getSafePattern(opts.ignore, opts.localPath).some((reg) => minimatch(s, reg))
|
|
317
|
+
getSafePattern(opts.ignore, opts.localPath).some((reg) => minimatch(s, reg)),
|
|
285
318
|
)
|
|
286
319
|
|
|
287
320
|
console.log(`忽略文件 (${ls.length}):`)
|
|
@@ -325,7 +358,7 @@ function mergeDelete(files) {
|
|
|
325
358
|
*/
|
|
326
359
|
function getSafePattern(patterns, prefixPath) {
|
|
327
360
|
const safePatterns = patterns
|
|
328
|
-
.map((s) => s.replace(/^[
|
|
361
|
+
.map((s) => s.replace(/^[./]*/, prefixPath + '/'))
|
|
329
362
|
.reduce((acc, s) => [...acc, s, s + '/**/*'], [])
|
|
330
363
|
return safePatterns
|
|
331
364
|
}
|
|
@@ -407,7 +440,7 @@ function parseOpts(opts) {
|
|
|
407
440
|
`remotePath:${target.remotePath}`,
|
|
408
441
|
`项目名称:${pkg.name} // 源自 package.json 中的 name 字段,忽略scope字段`,
|
|
409
442
|
`\n你可以设置 "securityLock": false 来关闭这个验证`,
|
|
410
|
-
].join('\n')
|
|
443
|
+
].join('\n'),
|
|
411
444
|
)
|
|
412
445
|
}
|
|
413
446
|
}
|
package/src/utils.js
CHANGED
|
@@ -32,4 +32,26 @@ function splitOnFirst(string, separator) {
|
|
|
32
32
|
return [string.slice(0, separatorIndex), string.slice(separatorIndex + separator.length)]
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param {import('ora').Ora} ora
|
|
38
|
+
*/
|
|
39
|
+
function injectTiming(ora) {
|
|
40
|
+
const oldStart = ora.start
|
|
41
|
+
|
|
42
|
+
let current = 0
|
|
43
|
+
ora.start = (message) => {
|
|
44
|
+
current = performance.now()
|
|
45
|
+
return oldStart.call(ora, message)
|
|
46
|
+
}
|
|
47
|
+
const oldSucceed = ora.succeed
|
|
48
|
+
ora.succeed = (message) => {
|
|
49
|
+
const now = performance.now()
|
|
50
|
+
const diff = (now - current) / 1000
|
|
51
|
+
return oldSucceed.call(ora, `${message} ${chalk.dim(`(${diff.toFixed(2)}s)`)}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return ora
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { splitOnFirst, exitWithError, warn, injectTiming }
|