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