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