@qse/ssh-sftp 1.2.0 → 1.3.1
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 +8 -0
- package/eslint.config.mjs +3 -0
- package/package.json +8 -5
- package/src/cli.js +6 -14
- package/src/index.js +77 -52
- 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.1",
|
|
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
|
},
|
|
@@ -26,14 +26,17 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@inquirer/prompts": "^8.3.0",
|
|
29
30
|
"chalk": "^5.6.2",
|
|
30
31
|
"glob": "^13.0.0",
|
|
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
|
+
"@qse/eslint-config": "^1.0.0",
|
|
40
|
+
"eslint": "^10.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
|
@@ -3,11 +3,14 @@ import ora from 'ora'
|
|
|
3
3
|
import * as glob from 'glob'
|
|
4
4
|
import fs from 'fs'
|
|
5
5
|
import * as minimatch from 'minimatch'
|
|
6
|
-
import inquirer from 'inquirer'
|
|
7
|
-
import { exitWithError, warn } from './utils.js'
|
|
6
|
+
import * as inquirer from '@inquirer/prompts'
|
|
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}`)
|
|
@@ -133,12 +145,7 @@ async function _sshSftp(opts) {
|
|
|
133
145
|
if (opts.skipPrompt) {
|
|
134
146
|
confirm = true
|
|
135
147
|
} else {
|
|
136
|
-
|
|
137
|
-
name: 'confirm',
|
|
138
|
-
message: `远程文件夹不存在,是否要创建一个`,
|
|
139
|
-
type: 'confirm',
|
|
140
|
-
})
|
|
141
|
-
confirm = ans.confirm
|
|
148
|
+
confirm = await inquirer.confirm({ message: `远程文件夹不存在,是否要创建一个` })
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
if (confirm) {
|
|
@@ -172,10 +179,8 @@ async function _sshSftp(opts) {
|
|
|
172
179
|
let confirm = Confirm.delete
|
|
173
180
|
if (remoteDeletefiles.length > localUploadFiles.length && !opts.skipPrompt) {
|
|
174
181
|
const showSelect = async () => {
|
|
175
|
-
|
|
176
|
-
name: 'confirm',
|
|
182
|
+
return await inquirer.select({
|
|
177
183
|
message: `远程需要删除的文件数(${remoteDeletefiles.length})比本地(${localUploadFiles.length})多,确定要删除吗?`,
|
|
178
|
-
type: 'list',
|
|
179
184
|
choices: [
|
|
180
185
|
{ name: '删除', value: Confirm.delete },
|
|
181
186
|
{ name: '不删除,继续部署', value: Confirm.skip },
|
|
@@ -183,7 +188,6 @@ async function _sshSftp(opts) {
|
|
|
183
188
|
{ name: '显示需要删除的文件', value: Confirm.showDeleteFile },
|
|
184
189
|
],
|
|
185
190
|
})
|
|
186
|
-
return confirm
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
do {
|
|
@@ -205,12 +209,19 @@ async function _sshSftp(opts) {
|
|
|
205
209
|
if (confirm === Confirm.delete) {
|
|
206
210
|
spinner.start('开始删除远程文件')
|
|
207
211
|
remoteDeletefiles = mergeDelete(remoteDeletefiles)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
await Promise.all(
|
|
213
|
+
remoteDeletefiles.map((o, i) =>
|
|
214
|
+
limit(async () => {
|
|
215
|
+
spinner.text = `[${i + 1}/${remoteDeletefiles.length}] 正在删除 ${o.path}`
|
|
216
|
+
try {
|
|
217
|
+
if (o.isDir) await sftp.rmdir(o.path, true)
|
|
218
|
+
else await sftp.delete(o.path)
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.error(`删除失败: ${o.path}`, e.message)
|
|
221
|
+
}
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
)
|
|
214
225
|
spinner.succeed(`已删除 ${opts.remotePath}`)
|
|
215
226
|
}
|
|
216
227
|
}
|
|
@@ -218,17 +229,31 @@ async function _sshSftp(opts) {
|
|
|
218
229
|
spinner.start(`开始上传 ${opts.localPath} 到 ${opts.remotePath}`)
|
|
219
230
|
|
|
220
231
|
if (Array.isArray(opts.ignore) && opts.ignore.length > 0) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
+
const dirs = localUploadFiles.filter((o) => fs.statSync(o.localPath).isDirectory())
|
|
233
|
+
const files = localUploadFiles.filter((o) => !fs.statSync(o.localPath).isDirectory())
|
|
234
|
+
|
|
235
|
+
// 先并发创建目录
|
|
236
|
+
await Promise.all(
|
|
237
|
+
dirs.map((o) =>
|
|
238
|
+
limit(async () => {
|
|
239
|
+
try {
|
|
240
|
+
if (!(await sftp.exists(o.remotePath))) {
|
|
241
|
+
await sftp.mkdir(o.remotePath)
|
|
242
|
+
}
|
|
243
|
+
} catch {}
|
|
244
|
+
}),
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// 再并发上传文件
|
|
249
|
+
await Promise.all(
|
|
250
|
+
files.map((o, i) =>
|
|
251
|
+
limit(async () => {
|
|
252
|
+
spinner.text = `[${i + 1}/${files.length}] 正在上传 ${o.localPath} 到 ${o.remotePath}`
|
|
253
|
+
await sftp.fastPut(o.localPath, o.remotePath)
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
256
|
+
)
|
|
232
257
|
} else {
|
|
233
258
|
await sftp.uploadDir(opts.localPath, opts.remotePath)
|
|
234
259
|
}
|
|
@@ -281,7 +306,7 @@ async function _sshSftpLS(opts, lsOpts) {
|
|
|
281
306
|
if (lsOpts.i && Array.isArray(opts.ignore) && opts.ignore.length > 0) {
|
|
282
307
|
let ls = glob.sync(`${opts.localPath}/**/*`)
|
|
283
308
|
ls = ls.filter((s) =>
|
|
284
|
-
getSafePattern(opts.ignore, opts.localPath).some((reg) => minimatch(s, reg))
|
|
309
|
+
getSafePattern(opts.ignore, opts.localPath).some((reg) => minimatch(s, reg)),
|
|
285
310
|
)
|
|
286
311
|
|
|
287
312
|
console.log(`忽略文件 (${ls.length}):`)
|
|
@@ -325,7 +350,7 @@ function mergeDelete(files) {
|
|
|
325
350
|
*/
|
|
326
351
|
function getSafePattern(patterns, prefixPath) {
|
|
327
352
|
const safePatterns = patterns
|
|
328
|
-
.map((s) => s.replace(/^[
|
|
353
|
+
.map((s) => s.replace(/^[./]*/, prefixPath + '/'))
|
|
329
354
|
.reduce((acc, s) => [...acc, s, s + '/**/*'], [])
|
|
330
355
|
return safePatterns
|
|
331
356
|
}
|
|
@@ -407,7 +432,7 @@ function parseOpts(opts) {
|
|
|
407
432
|
`remotePath:${target.remotePath}`,
|
|
408
433
|
`项目名称:${pkg.name} // 源自 package.json 中的 name 字段,忽略scope字段`,
|
|
409
434
|
`\n你可以设置 "securityLock": false 来关闭这个验证`,
|
|
410
|
-
].join('\n')
|
|
435
|
+
].join('\n'),
|
|
411
436
|
)
|
|
412
437
|
}
|
|
413
438
|
}
|
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 }
|