vite-plugin-deploy-ftp 0.1.0 → 1.0.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/README.md +160 -11
- package/dist/index.d.mts +28 -7
- package/dist/index.d.ts +28 -7
- package/dist/index.js +170 -53
- package/dist/index.mjs +171 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# vite-plugin-deploy-ftp
|
|
2
2
|
|
|
3
|
-
将 dist 目录上传到 FTP
|
|
3
|
+
将 dist 目录上传到 FTP 服务器,支持单个或多个 FTP 服务器配置
|
|
4
4
|
|
|
5
5
|
## 介绍
|
|
6
6
|
|
|
7
|
-
`vite-plugin-deploy-ftp` 是一个 Vite 插件,用于将打包后的文件上传到 FTP
|
|
7
|
+
`vite-plugin-deploy-ftp` 是一个 Vite 插件,用于将打包后的文件上传到 FTP 服务器。插件支持:
|
|
8
|
+
|
|
9
|
+
- 单个 FTP 服务器配置
|
|
10
|
+
- 多个 FTP 服务器配置(可多选上传目标)
|
|
11
|
+
- 自动备份远程文件
|
|
12
|
+
- 连接重试机制
|
|
13
|
+
- 选择性文件备份
|
|
8
14
|
|
|
9
15
|
## 安装
|
|
10
16
|
|
|
@@ -14,23 +20,166 @@ pnpm add vite-plugin-deploy-ftp -D
|
|
|
14
20
|
|
|
15
21
|
## 使用
|
|
16
22
|
|
|
23
|
+
### 单个 FTP 配置
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// vite.config.ts
|
|
27
|
+
import vitePluginDeployFtp from 'vite-plugin-deploy-ftp'
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
plugins: [
|
|
31
|
+
vitePluginDeployFtp({
|
|
32
|
+
open: true,
|
|
33
|
+
host: 'ftp.example.com',
|
|
34
|
+
port: 21,
|
|
35
|
+
user: 'username',
|
|
36
|
+
password: 'password',
|
|
37
|
+
uploadPath: '/public_html',
|
|
38
|
+
alias: 'https://example.com',
|
|
39
|
+
singleBack: false,
|
|
40
|
+
singleBackFiles: ['index.html'],
|
|
41
|
+
maxRetries: 3,
|
|
42
|
+
retryDelay: 1000,
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 多个 FTP 配置
|
|
49
|
+
|
|
17
50
|
```ts
|
|
18
51
|
// vite.config.ts
|
|
19
52
|
import vitePluginDeployFtp from 'vite-plugin-deploy-ftp'
|
|
20
53
|
|
|
21
|
-
// ...existing code...
|
|
22
54
|
export default {
|
|
23
|
-
// ...existing code...
|
|
24
55
|
plugins: [
|
|
25
|
-
// 在最后一个插件中使用
|
|
26
56
|
vitePluginDeployFtp({
|
|
27
57
|
open: true,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
58
|
+
uploadPath: '/public_html',
|
|
59
|
+
singleBack: false,
|
|
60
|
+
singleBackFiles: ['index.html'],
|
|
61
|
+
maxRetries: 3,
|
|
62
|
+
retryDelay: 1000,
|
|
63
|
+
ftps: [
|
|
64
|
+
{
|
|
65
|
+
name: '生产环境',
|
|
66
|
+
host: 'ftp.production.com',
|
|
67
|
+
port: 21,
|
|
68
|
+
user: 'prod_user',
|
|
69
|
+
password: 'prod_password',
|
|
70
|
+
alias: 'https://production.com',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: '测试环境',
|
|
74
|
+
host: 'ftp.test.com',
|
|
75
|
+
port: 21,
|
|
76
|
+
user: 'test_user',
|
|
77
|
+
password: 'test_password',
|
|
78
|
+
alias: 'https://test.com',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: '开发环境',
|
|
82
|
+
host: 'ftp.dev.com',
|
|
83
|
+
port: 21,
|
|
84
|
+
user: 'dev_user',
|
|
85
|
+
password: 'dev_password',
|
|
86
|
+
alias: 'https://dev.com',
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 配置参数
|
|
95
|
+
|
|
96
|
+
### 通用参数
|
|
97
|
+
|
|
98
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
99
|
+
| ----------------- | ---------- | ---------------- | -------------------------------- |
|
|
100
|
+
| `open` | `boolean` | `true` | 是否启用插件 |
|
|
101
|
+
| `uploadPath` | `string` | - | FTP 服务器上的上传路径 |
|
|
102
|
+
| `singleBack` | `boolean` | `false` | 是否使用单文件备份模式 |
|
|
103
|
+
| `singleBackFiles` | `string[]` | `['index.html']` | 单文件备份模式下要备份的文件列表 |
|
|
104
|
+
| `maxRetries` | `number` | `3` | 连接失败时的最大重试次数 |
|
|
105
|
+
| `retryDelay` | `number` | `1000` | 重试延迟时间(毫秒) |
|
|
106
|
+
|
|
107
|
+
### 单个 FTP 配置参数
|
|
108
|
+
|
|
109
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
110
|
+
| ---------- | -------- | ------ | -------------------------- |
|
|
111
|
+
| `host` | `string` | - | FTP 服务器地址 |
|
|
112
|
+
| `port` | `number` | `21` | FTP 服务器端口 |
|
|
113
|
+
| `user` | `string` | - | FTP 用户名 |
|
|
114
|
+
| `password` | `string` | - | FTP 密码 |
|
|
115
|
+
| `alias` | `string` | `''` | 网站别名,用于生成完整 URL |
|
|
116
|
+
|
|
117
|
+
### 多个 FTP 配置参数
|
|
118
|
+
|
|
119
|
+
| 参数 | 类型 | 说明 |
|
|
120
|
+
| ------ | ------------- | ------------------ |
|
|
121
|
+
| `ftps` | `FtpConfig[]` | FTP 服务器配置数组 |
|
|
122
|
+
|
|
123
|
+
#### FtpConfig 对象
|
|
124
|
+
|
|
125
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
126
|
+
| ---------- | -------- | ------ | ---------------------------------- |
|
|
127
|
+
| `name` | `string` | - | FTP 服务器名称(用于选择界面显示) |
|
|
128
|
+
| `host` | `string` | - | FTP 服务器地址 |
|
|
129
|
+
| `port` | `number` | `21` | FTP 服务器端口 |
|
|
130
|
+
| `user` | `string` | - | FTP 用户名 |
|
|
131
|
+
| `password` | `string` | - | FTP 密码 |
|
|
132
|
+
| `alias` | `string` | `''` | 网站别名,用于生成完整 URL |
|
|
133
|
+
|
|
134
|
+
## 功能特性
|
|
135
|
+
|
|
136
|
+
### 多服务器选择
|
|
137
|
+
|
|
138
|
+
当使用多个 FTP 配置时,插件会显示一个多选界面,让您选择要上传到哪些服务器:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
? 选择要上传的FTP服务器(可多选)
|
|
142
|
+
❯ ◯ 生产环境
|
|
143
|
+
◯ 测试环境
|
|
144
|
+
◯ 开发环境
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 备份功能
|
|
148
|
+
|
|
149
|
+
插件提供两种备份模式:
|
|
150
|
+
|
|
151
|
+
1. **完整备份**: 将远程目录下的所有文件打包备份
|
|
152
|
+
2. **选择性备份**: 只备份指定的文件(通过 `singleBackFiles` 配置)
|
|
153
|
+
|
|
154
|
+
### 连接重试
|
|
155
|
+
|
|
156
|
+
当 FTP 连接失败时,插件会自动重试,您可以通过 `maxRetries` 和 `retryDelay` 参数控制重试行为。
|
|
157
|
+
|
|
158
|
+
## 环境变量示例
|
|
159
|
+
|
|
160
|
+
建议将敏感信息(如用户名和密码)放在环境变量中:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# .env
|
|
164
|
+
VITE_FTP_HOST=ftp.example.com
|
|
165
|
+
VITE_FTP_PORT=21
|
|
166
|
+
VITE_FTP_USER=username
|
|
167
|
+
VITE_FTP_PASSWORD=password
|
|
168
|
+
VITE_FTP_PATH=/public_html
|
|
169
|
+
VITE_FTP_ALIAS=https://example.com
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// vite.config.ts
|
|
174
|
+
export default {
|
|
175
|
+
plugins: [
|
|
176
|
+
vitePluginDeployFtp({
|
|
177
|
+
host: process.env.VITE_FTP_HOST,
|
|
178
|
+
port: +(process.env.VITE_FTP_PORT || 21),
|
|
179
|
+
user: process.env.VITE_FTP_USER,
|
|
180
|
+
password: process.env.VITE_FTP_PASSWORD,
|
|
181
|
+
uploadPath: process.env.VITE_FTP_PATH,
|
|
182
|
+
alias: process.env.VITE_FTP_ALIAS,
|
|
34
183
|
}),
|
|
35
184
|
],
|
|
36
185
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
|
-
type vitePluginDeployFtpOption = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
type vitePluginDeployFtpOption = ({
|
|
4
|
+
uploadPath: string;
|
|
5
|
+
singleBackFiles?: string[];
|
|
6
|
+
singleBack?: boolean;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
maxRetries?: number;
|
|
9
|
+
retryDelay?: number;
|
|
10
|
+
showBackFile?: boolean;
|
|
11
|
+
} & {
|
|
12
|
+
ftps: {
|
|
13
|
+
name: string;
|
|
14
|
+
host?: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
user?: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
alias?: string;
|
|
19
|
+
}[];
|
|
20
|
+
defaultFtp?: string;
|
|
21
|
+
}) | ({
|
|
8
22
|
uploadPath: string;
|
|
9
23
|
singleBackFiles?: string[];
|
|
10
24
|
singleBack?: boolean;
|
|
11
|
-
alias?: string;
|
|
12
25
|
open?: boolean;
|
|
13
26
|
maxRetries?: number;
|
|
14
27
|
retryDelay?: number;
|
|
15
|
-
|
|
28
|
+
showBackFile?: boolean;
|
|
29
|
+
} & {
|
|
30
|
+
name?: string;
|
|
31
|
+
host?: string;
|
|
32
|
+
port?: number;
|
|
33
|
+
user?: string;
|
|
34
|
+
password?: string;
|
|
35
|
+
alias?: string;
|
|
36
|
+
});
|
|
16
37
|
declare function vitePluginDeployFtp(option: vitePluginDeployFtpOption): Plugin;
|
|
17
38
|
|
|
18
39
|
export { vitePluginDeployFtp as default, type vitePluginDeployFtpOption };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
|
-
type vitePluginDeployFtpOption = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
type vitePluginDeployFtpOption = ({
|
|
4
|
+
uploadPath: string;
|
|
5
|
+
singleBackFiles?: string[];
|
|
6
|
+
singleBack?: boolean;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
maxRetries?: number;
|
|
9
|
+
retryDelay?: number;
|
|
10
|
+
showBackFile?: boolean;
|
|
11
|
+
} & {
|
|
12
|
+
ftps: {
|
|
13
|
+
name: string;
|
|
14
|
+
host?: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
user?: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
alias?: string;
|
|
19
|
+
}[];
|
|
20
|
+
defaultFtp?: string;
|
|
21
|
+
}) | ({
|
|
8
22
|
uploadPath: string;
|
|
9
23
|
singleBackFiles?: string[];
|
|
10
24
|
singleBack?: boolean;
|
|
11
|
-
alias?: string;
|
|
12
25
|
open?: boolean;
|
|
13
26
|
maxRetries?: number;
|
|
14
27
|
retryDelay?: number;
|
|
15
|
-
|
|
28
|
+
showBackFile?: boolean;
|
|
29
|
+
} & {
|
|
30
|
+
name?: string;
|
|
31
|
+
host?: string;
|
|
32
|
+
port?: number;
|
|
33
|
+
user?: string;
|
|
34
|
+
password?: string;
|
|
35
|
+
alias?: string;
|
|
36
|
+
});
|
|
16
37
|
declare function vitePluginDeployFtp(option: vitePluginDeployFtpOption): Plugin;
|
|
17
38
|
|
|
18
39
|
export { vitePluginDeployFtp as default, type vitePluginDeployFtpOption };
|
package/dist/index.js
CHANGED
|
@@ -46,18 +46,17 @@ var import_vite = require("vite");
|
|
|
46
46
|
function vitePluginDeployFtp(option) {
|
|
47
47
|
const {
|
|
48
48
|
open = true,
|
|
49
|
-
host,
|
|
50
|
-
port = 21,
|
|
51
|
-
user,
|
|
52
|
-
password,
|
|
53
49
|
uploadPath,
|
|
54
|
-
alias = "",
|
|
55
50
|
singleBack = false,
|
|
56
51
|
singleBackFiles = ["index.html"],
|
|
52
|
+
showBackFile = false,
|
|
57
53
|
maxRetries = 3,
|
|
58
54
|
retryDelay = 1e3
|
|
59
55
|
} = option || {};
|
|
60
|
-
|
|
56
|
+
const isMultiFtp = "ftps" in option;
|
|
57
|
+
const ftpConfigs = isMultiFtp ? option.ftps : [{ ...option, name: option.name || option.alias || option.host }];
|
|
58
|
+
const defaultFtp = isMultiFtp ? option.defaultFtp : void 0;
|
|
59
|
+
if (!uploadPath || isMultiFtp && (!option.ftps || option.ftps.length === 0)) {
|
|
61
60
|
return {
|
|
62
61
|
name: "vite-plugin-deploy-ftp",
|
|
63
62
|
apply: "build",
|
|
@@ -69,10 +68,14 @@ function vitePluginDeployFtp(option) {
|
|
|
69
68
|
};
|
|
70
69
|
}
|
|
71
70
|
let outDir = "dist";
|
|
71
|
+
let buildFailed = false;
|
|
72
72
|
return {
|
|
73
73
|
name: "vite-plugin-deploy-ftp",
|
|
74
74
|
apply: "build",
|
|
75
75
|
enforce: "post",
|
|
76
|
+
buildEnd(error) {
|
|
77
|
+
if (error) buildFailed = true;
|
|
78
|
+
},
|
|
76
79
|
configResolved(config) {
|
|
77
80
|
outDir = config.build?.outDir || "dist";
|
|
78
81
|
},
|
|
@@ -80,55 +83,134 @@ function vitePluginDeployFtp(option) {
|
|
|
80
83
|
sequential: true,
|
|
81
84
|
order: "post",
|
|
82
85
|
async handler() {
|
|
83
|
-
if (!open) return;
|
|
86
|
+
if (!open || buildFailed) return;
|
|
84
87
|
try {
|
|
85
88
|
await deployToFtp();
|
|
86
89
|
} catch (error) {
|
|
87
|
-
console.error(import_chalk.default.red("FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
|
|
90
|
+
console.error(import_chalk.default.red("\u274C FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
|
|
88
91
|
throw error;
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
};
|
|
93
96
|
async function deployToFtp() {
|
|
94
|
-
const { protocol, baseUrl } = parseAlias(alias);
|
|
95
97
|
const ftpUploadChoice = await (0, import_prompts.select)({
|
|
96
98
|
message: "\u662F\u5426\u4E0A\u4F20FTP",
|
|
97
99
|
choices: ["\u662F", "\u5426"],
|
|
98
100
|
default: "\u662F"
|
|
99
101
|
});
|
|
100
102
|
if (ftpUploadChoice === "\u5426") return;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
103
|
+
let selectedConfigs = [];
|
|
104
|
+
if (isMultiFtp) {
|
|
105
|
+
if (defaultFtp) {
|
|
106
|
+
const defaultConfig = ftpConfigs.find((ftp) => ftp.name === defaultFtp);
|
|
107
|
+
if (defaultConfig) {
|
|
108
|
+
if (validateFtpConfig(defaultConfig)) {
|
|
109
|
+
console.log(import_chalk.default.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
|
|
110
|
+
selectedConfigs = [defaultConfig];
|
|
111
|
+
} else {
|
|
112
|
+
console.log(import_chalk.default.yellow(`\u26A0\uFE0F \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (selectedConfigs.length === 0) {
|
|
117
|
+
const validConfigs = ftpConfigs.filter(validateFtpConfig);
|
|
118
|
+
const invalidConfigs = ftpConfigs.filter((config) => !validateFtpConfig(config));
|
|
119
|
+
if (invalidConfigs.length > 0) {
|
|
120
|
+
console.log(import_chalk.default.yellow("\n \u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
|
|
121
|
+
invalidConfigs.forEach((config) => {
|
|
122
|
+
const missing = [];
|
|
123
|
+
if (!config.host) missing.push("host");
|
|
124
|
+
if (!config.user) missing.push("user");
|
|
125
|
+
if (!config.password) missing.push("password");
|
|
126
|
+
console.log(import_chalk.default.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
|
|
118
127
|
});
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
console.log();
|
|
129
|
+
}
|
|
130
|
+
if (validConfigs.length === 0) {
|
|
131
|
+
console.error(import_chalk.default.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const choices = validConfigs.map((ftp) => ({
|
|
135
|
+
name: ftp.name,
|
|
136
|
+
value: ftp
|
|
137
|
+
}));
|
|
138
|
+
selectedConfigs = await (0, import_prompts.checkbox)({
|
|
139
|
+
message: "\u9009\u62E9\u8981\u4E0A\u4F20\u7684FTP\u670D\u52A1\u5668\uFF08\u53EF\u591A\u9009\uFF09",
|
|
140
|
+
choices,
|
|
141
|
+
required: true
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const singleConfig = ftpConfigs[0];
|
|
146
|
+
if (validateFtpConfig(singleConfig)) {
|
|
147
|
+
selectedConfigs = [{ ...singleConfig, name: singleConfig.name || singleConfig.host }];
|
|
148
|
+
} else {
|
|
149
|
+
const missing = [];
|
|
150
|
+
if (!singleConfig.host) missing.push("host");
|
|
151
|
+
if (!singleConfig.user) missing.push("user");
|
|
152
|
+
if (!singleConfig.password) missing.push("password");
|
|
153
|
+
console.error(import_chalk.default.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const ftpConfig of selectedConfigs) {
|
|
158
|
+
const { host, port = 21, user, password, alias = "", name } = ftpConfig;
|
|
159
|
+
if (!host || !user || !password) {
|
|
160
|
+
console.error(import_chalk.default.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
|
|
161
|
+
if (!host) console.error(import_chalk.default.red(" - \u7F3A\u5C11 host"));
|
|
162
|
+
if (!user) console.error(import_chalk.default.red(" - \u7F3A\u5C11 user"));
|
|
163
|
+
if (!password) console.error(import_chalk.default.red(" - \u7F3A\u5C11 password"));
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const { protocol, baseUrl } = parseAlias(alias);
|
|
167
|
+
const displayName = name || host;
|
|
168
|
+
console.log(import_chalk.default.blue(`
|
|
169
|
+
\u{1F680} \u5F00\u59CB\u4E0A\u4F20\u5230: ${displayName}`));
|
|
170
|
+
const client = new import_basic_ftp.Client();
|
|
171
|
+
let uploadSpinner;
|
|
172
|
+
try {
|
|
173
|
+
uploadSpinner = (0, import_ora.default)(`\u8FDE\u63A5\u5230 ${displayName} \u4E2D...`).start();
|
|
174
|
+
await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
|
|
175
|
+
uploadSpinner.color = "green";
|
|
176
|
+
uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
|
|
177
|
+
const fileList = await client.list(uploadPath);
|
|
178
|
+
uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${import_chalk.default.green(`${displayName} ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
|
|
179
|
+
if (fileList.length) {
|
|
180
|
+
if (singleBack) {
|
|
181
|
+
await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles, showBackFile);
|
|
182
|
+
} else {
|
|
183
|
+
const isBackFiles = await (0, import_prompts.select)({
|
|
184
|
+
message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
|
|
185
|
+
choices: ["\u5426", "\u662F"],
|
|
186
|
+
default: "\u5426"
|
|
187
|
+
});
|
|
188
|
+
if (isBackFiles === "\u662F") {
|
|
189
|
+
await createBackupFile(client, uploadPath, protocol, baseUrl, showBackFile);
|
|
190
|
+
}
|
|
121
191
|
}
|
|
122
192
|
}
|
|
193
|
+
const uploadFileSpinner = (0, import_ora.default)(`\u4E0A\u4F20\u5230 ${displayName} \u4E2D...`).start();
|
|
194
|
+
await client.uploadFromDir(outDir, uploadPath);
|
|
195
|
+
uploadFileSpinner.succeed(
|
|
196
|
+
`\u{1F389} \u4E0A\u4F20\u5230 ${displayName} \u6210\u529F! \u8BBF\u95EE\u5730\u5740: ` + import_chalk.default.green(buildUrl(protocol, baseUrl, uploadPath))
|
|
197
|
+
);
|
|
198
|
+
console.log();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (uploadSpinner) {
|
|
201
|
+
uploadSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
|
|
202
|
+
}
|
|
203
|
+
console.error(import_chalk.default.red(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
204
|
+
throw error;
|
|
205
|
+
} finally {
|
|
206
|
+
client.close();
|
|
123
207
|
}
|
|
124
|
-
const uploadFileSpinner = (0, import_ora.default)("\u4E0A\u4F20\u4E2D...").start();
|
|
125
|
-
await client.uploadFromDir(outDir, uploadPath);
|
|
126
|
-
uploadFileSpinner.succeed("\u4E0A\u4F20\u6210\u529F url:" + import_chalk.default.green(buildUrl(protocol, baseUrl, uploadPath)));
|
|
127
|
-
} finally {
|
|
128
|
-
client.close();
|
|
129
208
|
}
|
|
130
209
|
}
|
|
131
210
|
}
|
|
211
|
+
function validateFtpConfig(config) {
|
|
212
|
+
return !!(config.host && config.user && config.password);
|
|
213
|
+
}
|
|
132
214
|
function parseAlias(alias = "") {
|
|
133
215
|
const [protocol = "", baseUrl = ""] = alias.split("://");
|
|
134
216
|
return {
|
|
@@ -153,12 +235,12 @@ async function connectWithRetry(client, config, maxRetries, retryDelay) {
|
|
|
153
235
|
} catch (error) {
|
|
154
236
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
155
237
|
if (attempt < maxRetries) {
|
|
156
|
-
console.log(import_chalk.default.yellow(`\u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
238
|
+
console.log(import_chalk.default.yellow(`\u26A0\uFE0F \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
157
239
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
158
240
|
}
|
|
159
241
|
}
|
|
160
242
|
}
|
|
161
|
-
throw new Error(
|
|
243
|
+
throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
|
|
162
244
|
}
|
|
163
245
|
function createTempDir(basePath) {
|
|
164
246
|
const tempBaseDir = import_node_os.default.tmpdir();
|
|
@@ -174,13 +256,13 @@ function createTempDir(basePath) {
|
|
|
174
256
|
import_node_fs.default.rmSync(tempPath, { recursive: true, force: true });
|
|
175
257
|
}
|
|
176
258
|
} catch (error) {
|
|
177
|
-
console.warn(import_chalk.default.yellow(`\u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
259
|
+
console.warn(import_chalk.default.yellow(`\u26A0\uFE0F \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
178
260
|
}
|
|
179
261
|
}
|
|
180
262
|
};
|
|
181
263
|
}
|
|
182
|
-
async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
183
|
-
const backupSpinner = (0, import_ora.default)(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(
|
|
264
|
+
async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
|
|
265
|
+
const backupSpinner = (0, import_ora.default)(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
184
266
|
const fileName = `backup_${(0, import_dayjs.default)().format("YYYYMMDD_HHmmss")}.zip`;
|
|
185
267
|
const tempDir = createTempDir("backup-zip");
|
|
186
268
|
const zipFilePath = import_node_path.default.join(import_node_os.default.tmpdir(), "vite-plugin-deploy-ftp", fileName);
|
|
@@ -189,19 +271,31 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
189
271
|
if (!import_node_fs.default.existsSync(zipDir)) {
|
|
190
272
|
import_node_fs.default.mkdirSync(zipDir, { recursive: true });
|
|
191
273
|
}
|
|
192
|
-
await client.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
274
|
+
const remoteFiles = await client.list(dir);
|
|
275
|
+
const filteredFiles = remoteFiles.filter((file) => !file.name.startsWith("backup_") || !file.name.endsWith(".zip"));
|
|
276
|
+
if (showBackFile) {
|
|
277
|
+
console.log(import_chalk.default.cyan(`
|
|
278
|
+
\u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${filteredFiles.length} \u4E2A\u6587\u4EF6:`));
|
|
279
|
+
filteredFiles.forEach((file) => {
|
|
280
|
+
console.log(import_chalk.default.gray(` - ${file.name} (${file.size} bytes)`));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
for (const file of filteredFiles) {
|
|
284
|
+
if (file.type === 1) {
|
|
285
|
+
await client.downloadTo(import_node_path.default.join(tempDir.path, file.name), (0, import_vite.normalizePath)(`${dir}/${file.name}`));
|
|
197
286
|
}
|
|
198
|
-
}
|
|
287
|
+
}
|
|
288
|
+
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
|
|
199
289
|
await createZipFile(tempDir.path, zipFilePath);
|
|
200
290
|
backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${import_chalk.default.yellow(
|
|
201
|
-
|
|
291
|
+
`==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
|
|
202
292
|
)}`;
|
|
203
293
|
await client.uploadFrom(zipFilePath, (0, import_vite.normalizePath)(`${dir}/${fileName}`));
|
|
204
|
-
|
|
294
|
+
const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
|
|
295
|
+
backupSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
|
|
296
|
+
console.log(import_chalk.default.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
297
|
+
console.log(import_chalk.default.green(`\u{1F517} ${backupUrl}`));
|
|
298
|
+
console.log();
|
|
205
299
|
} catch (error) {
|
|
206
300
|
backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
|
|
207
301
|
throw error;
|
|
@@ -212,7 +306,7 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
212
306
|
import_node_fs.default.rmSync(zipFilePath);
|
|
213
307
|
}
|
|
214
308
|
} catch (error) {
|
|
215
|
-
console.warn(import_chalk.default.yellow("\u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
309
|
+
console.warn(import_chalk.default.yellow("\u26A0\uFE0F \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
216
310
|
}
|
|
217
311
|
}
|
|
218
312
|
}
|
|
@@ -233,10 +327,11 @@ async function createZipFile(sourceDir, outputPath) {
|
|
|
233
327
|
archive.finalize();
|
|
234
328
|
});
|
|
235
329
|
}
|
|
236
|
-
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles) {
|
|
330
|
+
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
|
|
237
331
|
const timestamp = (0, import_dayjs.default)().format("YYYYMMDD_HHmmss");
|
|
238
|
-
const backupSpinner = (0, import_ora.default)(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(
|
|
332
|
+
const backupSpinner = (0, import_ora.default)(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
239
333
|
const tempDir = createTempDir("single-backup");
|
|
334
|
+
let backupProgressSpinner;
|
|
240
335
|
try {
|
|
241
336
|
const remoteFiles = await client.list(dir);
|
|
242
337
|
const backupTasks = singleBackFiles.map((fileName) => {
|
|
@@ -247,8 +342,18 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
247
342
|
backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
|
|
248
343
|
return;
|
|
249
344
|
}
|
|
345
|
+
backupSpinner.stop();
|
|
346
|
+
if (showBackFile) {
|
|
347
|
+
console.log(import_chalk.default.cyan(`
|
|
348
|
+
\u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
|
|
349
|
+
backupTasks.forEach((task) => {
|
|
350
|
+
console.log(import_chalk.default.gray(` - ${task.fileName}`));
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
backupProgressSpinner = (0, import_ora.default)("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
|
|
250
354
|
const concurrencyLimit = 3;
|
|
251
355
|
let backedUpCount = 0;
|
|
356
|
+
const backedUpFiles = [];
|
|
252
357
|
for (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
|
|
253
358
|
const batch = backupTasks.slice(i, i + concurrencyLimit);
|
|
254
359
|
const promises = batch.map(async ({ fileName }) => {
|
|
@@ -256,9 +361,12 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
256
361
|
const localTempPath = import_node_path.default.join(tempDir.path, fileName);
|
|
257
362
|
const [name, ext] = fileName.split(".");
|
|
258
363
|
const suffix = ext ? `.${ext}` : "";
|
|
259
|
-
const
|
|
364
|
+
const backupFileName = `${name}.${timestamp}${suffix}`;
|
|
365
|
+
const backupRemotePath = (0, import_vite.normalizePath)(`${dir}/${backupFileName}`);
|
|
260
366
|
await client.downloadTo(localTempPath, (0, import_vite.normalizePath)(`${dir}/${fileName}`));
|
|
261
367
|
await client.uploadFrom(localTempPath, backupRemotePath);
|
|
368
|
+
const backupUrl = buildUrl(protocol, baseUrl, backupRemotePath);
|
|
369
|
+
backedUpFiles.push(backupUrl);
|
|
262
370
|
return true;
|
|
263
371
|
} catch (error) {
|
|
264
372
|
console.warn(import_chalk.default.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
@@ -269,12 +377,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
269
377
|
backedUpCount += results.filter(Boolean).length;
|
|
270
378
|
}
|
|
271
379
|
if (backedUpCount > 0) {
|
|
272
|
-
|
|
380
|
+
backupProgressSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
|
|
381
|
+
console.log(import_chalk.default.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
382
|
+
backedUpFiles.forEach((url) => {
|
|
383
|
+
console.log(import_chalk.default.green(`\u{1F517} ${url}`));
|
|
384
|
+
});
|
|
385
|
+
console.log();
|
|
273
386
|
} else {
|
|
274
|
-
|
|
387
|
+
backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
|
|
275
388
|
}
|
|
276
389
|
} catch (error) {
|
|
277
|
-
|
|
390
|
+
if (backupProgressSpinner) {
|
|
391
|
+
backupProgressSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
392
|
+
} else {
|
|
393
|
+
backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
394
|
+
}
|
|
278
395
|
throw error;
|
|
279
396
|
} finally {
|
|
280
397
|
tempDir.cleanup();
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { select } from "@inquirer/prompts";
|
|
2
|
+
import { checkbox, select } from "@inquirer/prompts";
|
|
3
3
|
import archiver from "archiver";
|
|
4
4
|
import { Client } from "basic-ftp";
|
|
5
5
|
import chalk from "chalk";
|
|
@@ -12,18 +12,17 @@ import { normalizePath } from "vite";
|
|
|
12
12
|
function vitePluginDeployFtp(option) {
|
|
13
13
|
const {
|
|
14
14
|
open = true,
|
|
15
|
-
host,
|
|
16
|
-
port = 21,
|
|
17
|
-
user,
|
|
18
|
-
password,
|
|
19
15
|
uploadPath,
|
|
20
|
-
alias = "",
|
|
21
16
|
singleBack = false,
|
|
22
17
|
singleBackFiles = ["index.html"],
|
|
18
|
+
showBackFile = false,
|
|
23
19
|
maxRetries = 3,
|
|
24
20
|
retryDelay = 1e3
|
|
25
21
|
} = option || {};
|
|
26
|
-
|
|
22
|
+
const isMultiFtp = "ftps" in option;
|
|
23
|
+
const ftpConfigs = isMultiFtp ? option.ftps : [{ ...option, name: option.name || option.alias || option.host }];
|
|
24
|
+
const defaultFtp = isMultiFtp ? option.defaultFtp : void 0;
|
|
25
|
+
if (!uploadPath || isMultiFtp && (!option.ftps || option.ftps.length === 0)) {
|
|
27
26
|
return {
|
|
28
27
|
name: "vite-plugin-deploy-ftp",
|
|
29
28
|
apply: "build",
|
|
@@ -35,10 +34,14 @@ function vitePluginDeployFtp(option) {
|
|
|
35
34
|
};
|
|
36
35
|
}
|
|
37
36
|
let outDir = "dist";
|
|
37
|
+
let buildFailed = false;
|
|
38
38
|
return {
|
|
39
39
|
name: "vite-plugin-deploy-ftp",
|
|
40
40
|
apply: "build",
|
|
41
41
|
enforce: "post",
|
|
42
|
+
buildEnd(error) {
|
|
43
|
+
if (error) buildFailed = true;
|
|
44
|
+
},
|
|
42
45
|
configResolved(config) {
|
|
43
46
|
outDir = config.build?.outDir || "dist";
|
|
44
47
|
},
|
|
@@ -46,55 +49,134 @@ function vitePluginDeployFtp(option) {
|
|
|
46
49
|
sequential: true,
|
|
47
50
|
order: "post",
|
|
48
51
|
async handler() {
|
|
49
|
-
if (!open) return;
|
|
52
|
+
if (!open || buildFailed) return;
|
|
50
53
|
try {
|
|
51
54
|
await deployToFtp();
|
|
52
55
|
} catch (error) {
|
|
53
|
-
console.error(chalk.red("FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
|
|
56
|
+
console.error(chalk.red("\u274C FTP \u90E8\u7F72\u5931\u8D25:"), error instanceof Error ? error.message : error);
|
|
54
57
|
throw error;
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
};
|
|
59
62
|
async function deployToFtp() {
|
|
60
|
-
const { protocol, baseUrl } = parseAlias(alias);
|
|
61
63
|
const ftpUploadChoice = await select({
|
|
62
64
|
message: "\u662F\u5426\u4E0A\u4F20FTP",
|
|
63
65
|
choices: ["\u662F", "\u5426"],
|
|
64
66
|
default: "\u662F"
|
|
65
67
|
});
|
|
66
68
|
if (ftpUploadChoice === "\u5426") return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
let selectedConfigs = [];
|
|
70
|
+
if (isMultiFtp) {
|
|
71
|
+
if (defaultFtp) {
|
|
72
|
+
const defaultConfig = ftpConfigs.find((ftp) => ftp.name === defaultFtp);
|
|
73
|
+
if (defaultConfig) {
|
|
74
|
+
if (validateFtpConfig(defaultConfig)) {
|
|
75
|
+
console.log(chalk.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
|
|
76
|
+
selectedConfigs = [defaultConfig];
|
|
77
|
+
} else {
|
|
78
|
+
console.log(chalk.yellow(`\u26A0\uFE0F \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (selectedConfigs.length === 0) {
|
|
83
|
+
const validConfigs = ftpConfigs.filter(validateFtpConfig);
|
|
84
|
+
const invalidConfigs = ftpConfigs.filter((config) => !validateFtpConfig(config));
|
|
85
|
+
if (invalidConfigs.length > 0) {
|
|
86
|
+
console.log(chalk.yellow("\n \u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
|
|
87
|
+
invalidConfigs.forEach((config) => {
|
|
88
|
+
const missing = [];
|
|
89
|
+
if (!config.host) missing.push("host");
|
|
90
|
+
if (!config.user) missing.push("user");
|
|
91
|
+
if (!config.password) missing.push("password");
|
|
92
|
+
console.log(chalk.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
|
|
84
93
|
});
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
|
96
|
+
if (validConfigs.length === 0) {
|
|
97
|
+
console.error(chalk.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const choices = validConfigs.map((ftp) => ({
|
|
101
|
+
name: ftp.name,
|
|
102
|
+
value: ftp
|
|
103
|
+
}));
|
|
104
|
+
selectedConfigs = await checkbox({
|
|
105
|
+
message: "\u9009\u62E9\u8981\u4E0A\u4F20\u7684FTP\u670D\u52A1\u5668\uFF08\u53EF\u591A\u9009\uFF09",
|
|
106
|
+
choices,
|
|
107
|
+
required: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
const singleConfig = ftpConfigs[0];
|
|
112
|
+
if (validateFtpConfig(singleConfig)) {
|
|
113
|
+
selectedConfigs = [{ ...singleConfig, name: singleConfig.name || singleConfig.host }];
|
|
114
|
+
} else {
|
|
115
|
+
const missing = [];
|
|
116
|
+
if (!singleConfig.host) missing.push("host");
|
|
117
|
+
if (!singleConfig.user) missing.push("user");
|
|
118
|
+
if (!singleConfig.password) missing.push("password");
|
|
119
|
+
console.error(chalk.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const ftpConfig of selectedConfigs) {
|
|
124
|
+
const { host, port = 21, user, password, alias = "", name } = ftpConfig;
|
|
125
|
+
if (!host || !user || !password) {
|
|
126
|
+
console.error(chalk.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
|
|
127
|
+
if (!host) console.error(chalk.red(" - \u7F3A\u5C11 host"));
|
|
128
|
+
if (!user) console.error(chalk.red(" - \u7F3A\u5C11 user"));
|
|
129
|
+
if (!password) console.error(chalk.red(" - \u7F3A\u5C11 password"));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const { protocol, baseUrl } = parseAlias(alias);
|
|
133
|
+
const displayName = name || host;
|
|
134
|
+
console.log(chalk.blue(`
|
|
135
|
+
\u{1F680} \u5F00\u59CB\u4E0A\u4F20\u5230: ${displayName}`));
|
|
136
|
+
const client = new Client();
|
|
137
|
+
let uploadSpinner;
|
|
138
|
+
try {
|
|
139
|
+
uploadSpinner = ora(`\u8FDE\u63A5\u5230 ${displayName} \u4E2D...`).start();
|
|
140
|
+
await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
|
|
141
|
+
uploadSpinner.color = "green";
|
|
142
|
+
uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
|
|
143
|
+
const fileList = await client.list(uploadPath);
|
|
144
|
+
uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${chalk.green(`${displayName} ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
|
|
145
|
+
if (fileList.length) {
|
|
146
|
+
if (singleBack) {
|
|
147
|
+
await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles, showBackFile);
|
|
148
|
+
} else {
|
|
149
|
+
const isBackFiles = await select({
|
|
150
|
+
message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
|
|
151
|
+
choices: ["\u5426", "\u662F"],
|
|
152
|
+
default: "\u5426"
|
|
153
|
+
});
|
|
154
|
+
if (isBackFiles === "\u662F") {
|
|
155
|
+
await createBackupFile(client, uploadPath, protocol, baseUrl, showBackFile);
|
|
156
|
+
}
|
|
87
157
|
}
|
|
88
158
|
}
|
|
159
|
+
const uploadFileSpinner = ora(`\u4E0A\u4F20\u5230 ${displayName} \u4E2D...`).start();
|
|
160
|
+
await client.uploadFromDir(outDir, uploadPath);
|
|
161
|
+
uploadFileSpinner.succeed(
|
|
162
|
+
`\u{1F389} \u4E0A\u4F20\u5230 ${displayName} \u6210\u529F! \u8BBF\u95EE\u5730\u5740: ` + chalk.green(buildUrl(protocol, baseUrl, uploadPath))
|
|
163
|
+
);
|
|
164
|
+
console.log();
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (uploadSpinner) {
|
|
167
|
+
uploadSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
|
|
168
|
+
}
|
|
169
|
+
console.error(chalk.red(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
170
|
+
throw error;
|
|
171
|
+
} finally {
|
|
172
|
+
client.close();
|
|
89
173
|
}
|
|
90
|
-
const uploadFileSpinner = ora("\u4E0A\u4F20\u4E2D...").start();
|
|
91
|
-
await client.uploadFromDir(outDir, uploadPath);
|
|
92
|
-
uploadFileSpinner.succeed("\u4E0A\u4F20\u6210\u529F url:" + chalk.green(buildUrl(protocol, baseUrl, uploadPath)));
|
|
93
|
-
} finally {
|
|
94
|
-
client.close();
|
|
95
174
|
}
|
|
96
175
|
}
|
|
97
176
|
}
|
|
177
|
+
function validateFtpConfig(config) {
|
|
178
|
+
return !!(config.host && config.user && config.password);
|
|
179
|
+
}
|
|
98
180
|
function parseAlias(alias = "") {
|
|
99
181
|
const [protocol = "", baseUrl = ""] = alias.split("://");
|
|
100
182
|
return {
|
|
@@ -119,12 +201,12 @@ async function connectWithRetry(client, config, maxRetries, retryDelay) {
|
|
|
119
201
|
} catch (error) {
|
|
120
202
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
121
203
|
if (attempt < maxRetries) {
|
|
122
|
-
console.log(chalk.yellow(`\u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
204
|
+
console.log(chalk.yellow(`\u26A0\uFE0F \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
123
205
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
124
206
|
}
|
|
125
207
|
}
|
|
126
208
|
}
|
|
127
|
-
throw new Error(
|
|
209
|
+
throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
|
|
128
210
|
}
|
|
129
211
|
function createTempDir(basePath) {
|
|
130
212
|
const tempBaseDir = os.tmpdir();
|
|
@@ -140,13 +222,13 @@ function createTempDir(basePath) {
|
|
|
140
222
|
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
141
223
|
}
|
|
142
224
|
} catch (error) {
|
|
143
|
-
console.warn(chalk.yellow(`\u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
225
|
+
console.warn(chalk.yellow(`\u26A0\uFE0F \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
144
226
|
}
|
|
145
227
|
}
|
|
146
228
|
};
|
|
147
229
|
}
|
|
148
|
-
async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
149
|
-
const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(
|
|
230
|
+
async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
|
|
231
|
+
const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
150
232
|
const fileName = `backup_${dayjs().format("YYYYMMDD_HHmmss")}.zip`;
|
|
151
233
|
const tempDir = createTempDir("backup-zip");
|
|
152
234
|
const zipFilePath = path.join(os.tmpdir(), "vite-plugin-deploy-ftp", fileName);
|
|
@@ -155,19 +237,31 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
155
237
|
if (!fs.existsSync(zipDir)) {
|
|
156
238
|
fs.mkdirSync(zipDir, { recursive: true });
|
|
157
239
|
}
|
|
158
|
-
await client.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
240
|
+
const remoteFiles = await client.list(dir);
|
|
241
|
+
const filteredFiles = remoteFiles.filter((file) => !file.name.startsWith("backup_") || !file.name.endsWith(".zip"));
|
|
242
|
+
if (showBackFile) {
|
|
243
|
+
console.log(chalk.cyan(`
|
|
244
|
+
\u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${filteredFiles.length} \u4E2A\u6587\u4EF6:`));
|
|
245
|
+
filteredFiles.forEach((file) => {
|
|
246
|
+
console.log(chalk.gray(` - ${file.name} (${file.size} bytes)`));
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const file of filteredFiles) {
|
|
250
|
+
if (file.type === 1) {
|
|
251
|
+
await client.downloadTo(path.join(tempDir.path, file.name), normalizePath(`${dir}/${file.name}`));
|
|
163
252
|
}
|
|
164
|
-
}
|
|
253
|
+
}
|
|
254
|
+
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
|
|
165
255
|
await createZipFile(tempDir.path, zipFilePath);
|
|
166
256
|
backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk.yellow(
|
|
167
|
-
|
|
257
|
+
`==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
|
|
168
258
|
)}`;
|
|
169
259
|
await client.uploadFrom(zipFilePath, normalizePath(`${dir}/${fileName}`));
|
|
170
|
-
|
|
260
|
+
const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
|
|
261
|
+
backupSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
|
|
262
|
+
console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
263
|
+
console.log(chalk.green(`\u{1F517} ${backupUrl}`));
|
|
264
|
+
console.log();
|
|
171
265
|
} catch (error) {
|
|
172
266
|
backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
|
|
173
267
|
throw error;
|
|
@@ -178,7 +272,7 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
178
272
|
fs.rmSync(zipFilePath);
|
|
179
273
|
}
|
|
180
274
|
} catch (error) {
|
|
181
|
-
console.warn(chalk.yellow("\u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
275
|
+
console.warn(chalk.yellow("\u26A0\uFE0F \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
182
276
|
}
|
|
183
277
|
}
|
|
184
278
|
}
|
|
@@ -199,10 +293,11 @@ async function createZipFile(sourceDir, outputPath) {
|
|
|
199
293
|
archive.finalize();
|
|
200
294
|
});
|
|
201
295
|
}
|
|
202
|
-
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles) {
|
|
296
|
+
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
|
|
203
297
|
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
|
204
|
-
const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(
|
|
298
|
+
const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
205
299
|
const tempDir = createTempDir("single-backup");
|
|
300
|
+
let backupProgressSpinner;
|
|
206
301
|
try {
|
|
207
302
|
const remoteFiles = await client.list(dir);
|
|
208
303
|
const backupTasks = singleBackFiles.map((fileName) => {
|
|
@@ -213,8 +308,18 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
213
308
|
backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
|
|
214
309
|
return;
|
|
215
310
|
}
|
|
311
|
+
backupSpinner.stop();
|
|
312
|
+
if (showBackFile) {
|
|
313
|
+
console.log(chalk.cyan(`
|
|
314
|
+
\u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
|
|
315
|
+
backupTasks.forEach((task) => {
|
|
316
|
+
console.log(chalk.gray(` - ${task.fileName}`));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
|
|
216
320
|
const concurrencyLimit = 3;
|
|
217
321
|
let backedUpCount = 0;
|
|
322
|
+
const backedUpFiles = [];
|
|
218
323
|
for (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
|
|
219
324
|
const batch = backupTasks.slice(i, i + concurrencyLimit);
|
|
220
325
|
const promises = batch.map(async ({ fileName }) => {
|
|
@@ -222,9 +327,12 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
222
327
|
const localTempPath = path.join(tempDir.path, fileName);
|
|
223
328
|
const [name, ext] = fileName.split(".");
|
|
224
329
|
const suffix = ext ? `.${ext}` : "";
|
|
225
|
-
const
|
|
330
|
+
const backupFileName = `${name}.${timestamp}${suffix}`;
|
|
331
|
+
const backupRemotePath = normalizePath(`${dir}/${backupFileName}`);
|
|
226
332
|
await client.downloadTo(localTempPath, normalizePath(`${dir}/${fileName}`));
|
|
227
333
|
await client.uploadFrom(localTempPath, backupRemotePath);
|
|
334
|
+
const backupUrl = buildUrl(protocol, baseUrl, backupRemotePath);
|
|
335
|
+
backedUpFiles.push(backupUrl);
|
|
228
336
|
return true;
|
|
229
337
|
} catch (error) {
|
|
230
338
|
console.warn(chalk.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
@@ -235,12 +343,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
235
343
|
backedUpCount += results.filter(Boolean).length;
|
|
236
344
|
}
|
|
237
345
|
if (backedUpCount > 0) {
|
|
238
|
-
|
|
346
|
+
backupProgressSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
|
|
347
|
+
console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
348
|
+
backedUpFiles.forEach((url) => {
|
|
349
|
+
console.log(chalk.green(`\u{1F517} ${url}`));
|
|
350
|
+
});
|
|
351
|
+
console.log();
|
|
239
352
|
} else {
|
|
240
|
-
|
|
353
|
+
backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
|
|
241
354
|
}
|
|
242
355
|
} catch (error) {
|
|
243
|
-
|
|
356
|
+
if (backupProgressSpinner) {
|
|
357
|
+
backupProgressSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
358
|
+
} else {
|
|
359
|
+
backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
360
|
+
}
|
|
244
361
|
throw error;
|
|
245
362
|
} finally {
|
|
246
363
|
tempDir.cleanup();
|