vite-plugin-deploy-ftp 0.0.10 → 1.0.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/README.md +160 -11
- package/dist/index.d.mts +28 -7
- package/dist/index.d.ts +28 -7
- package/dist/index.js +175 -59
- package/dist/index.mjs +176 -60
- package/package.json +8 -8
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
|
@@ -28,11 +28,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
30
|
// src/index.ts
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
33
|
default: () => vitePluginDeployFtp
|
|
34
34
|
});
|
|
35
|
-
module.exports = __toCommonJS(
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
var import_prompts = require("@inquirer/prompts");
|
|
37
37
|
var import_archiver = __toESM(require("archiver"));
|
|
38
38
|
var import_basic_ftp = require("basic-ftp");
|
|
@@ -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,133 @@ 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\u26A0\uFE0F \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
|
+
} catch (error) {
|
|
199
|
+
if (uploadSpinner) {
|
|
200
|
+
uploadSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
|
|
201
|
+
}
|
|
202
|
+
console.error(import_chalk.default.red(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
203
|
+
throw error;
|
|
204
|
+
} finally {
|
|
205
|
+
client.close();
|
|
123
206
|
}
|
|
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
207
|
}
|
|
130
208
|
}
|
|
131
209
|
}
|
|
210
|
+
function validateFtpConfig(config) {
|
|
211
|
+
return !!(config.host && config.user && config.password);
|
|
212
|
+
}
|
|
132
213
|
function parseAlias(alias = "") {
|
|
133
214
|
const [protocol = "", baseUrl = ""] = alias.split("://");
|
|
134
215
|
return {
|
|
@@ -153,12 +234,12 @@ async function connectWithRetry(client, config, maxRetries, retryDelay) {
|
|
|
153
234
|
} catch (error) {
|
|
154
235
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
155
236
|
if (attempt < maxRetries) {
|
|
156
|
-
console.log(import_chalk.default.yellow(`\u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
237
|
+
console.log(import_chalk.default.yellow(`\u26A0\uFE0F \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
157
238
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
158
239
|
}
|
|
159
240
|
}
|
|
160
241
|
}
|
|
161
|
-
throw new Error(
|
|
242
|
+
throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
|
|
162
243
|
}
|
|
163
244
|
function createTempDir(basePath) {
|
|
164
245
|
const tempBaseDir = import_node_os.default.tmpdir();
|
|
@@ -174,13 +255,13 @@ function createTempDir(basePath) {
|
|
|
174
255
|
import_node_fs.default.rmSync(tempPath, { recursive: true, force: true });
|
|
175
256
|
}
|
|
176
257
|
} catch (error) {
|
|
177
|
-
console.warn(import_chalk.default.yellow(`\u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
258
|
+
console.warn(import_chalk.default.yellow(`\u26A0\uFE0F \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
178
259
|
}
|
|
179
260
|
}
|
|
180
261
|
};
|
|
181
262
|
}
|
|
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(
|
|
263
|
+
async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
|
|
264
|
+
const backupSpinner = (0, import_ora.default)(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
184
265
|
const fileName = `backup_${(0, import_dayjs.default)().format("YYYYMMDD_HHmmss")}.zip`;
|
|
185
266
|
const tempDir = createTempDir("backup-zip");
|
|
186
267
|
const zipFilePath = import_node_path.default.join(import_node_os.default.tmpdir(), "vite-plugin-deploy-ftp", fileName);
|
|
@@ -189,21 +270,33 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
189
270
|
if (!import_node_fs.default.existsSync(zipDir)) {
|
|
190
271
|
import_node_fs.default.mkdirSync(zipDir, { recursive: true });
|
|
191
272
|
}
|
|
192
|
-
await client.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
273
|
+
const remoteFiles = await client.list(dir);
|
|
274
|
+
const filteredFiles = remoteFiles.filter((file) => !file.name.startsWith("backup_") || !file.name.endsWith(".zip"));
|
|
275
|
+
if (showBackFile) {
|
|
276
|
+
console.log(import_chalk.default.cyan(`
|
|
277
|
+
\u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${filteredFiles.length} \u4E2A\u6587\u4EF6:`));
|
|
278
|
+
filteredFiles.forEach((file) => {
|
|
279
|
+
console.log(import_chalk.default.gray(` - ${file.name} (${file.size} bytes)`));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
for (const file of filteredFiles) {
|
|
283
|
+
if (file.type === 1) {
|
|
284
|
+
await client.downloadTo(import_node_path.default.join(tempDir.path, file.name), (0, import_vite.normalizePath)(`${dir}/${file.name}`));
|
|
197
285
|
}
|
|
198
|
-
}
|
|
286
|
+
}
|
|
287
|
+
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
|
|
199
288
|
await createZipFile(tempDir.path, zipFilePath);
|
|
200
289
|
backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${import_chalk.default.yellow(
|
|
201
|
-
|
|
290
|
+
`==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
|
|
202
291
|
)}`;
|
|
203
292
|
await client.uploadFrom(zipFilePath, (0, import_vite.normalizePath)(`${dir}/${fileName}`));
|
|
204
|
-
|
|
293
|
+
const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
|
|
294
|
+
backupSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
|
|
295
|
+
console.log(import_chalk.default.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
296
|
+
console.log(import_chalk.default.green(` ${backupUrl}`));
|
|
297
|
+
console.log();
|
|
205
298
|
} catch (error) {
|
|
206
|
-
backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
|
|
299
|
+
backupSpinner.fail("\u274C \u5907\u4EFD\u5931\u8D25");
|
|
207
300
|
throw error;
|
|
208
301
|
} finally {
|
|
209
302
|
tempDir.cleanup();
|
|
@@ -212,7 +305,7 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
212
305
|
import_node_fs.default.rmSync(zipFilePath);
|
|
213
306
|
}
|
|
214
307
|
} catch (error) {
|
|
215
|
-
console.warn(import_chalk.default.yellow("\u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
308
|
+
console.warn(import_chalk.default.yellow("\u26A0\uFE0F \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
216
309
|
}
|
|
217
310
|
}
|
|
218
311
|
}
|
|
@@ -233,10 +326,11 @@ async function createZipFile(sourceDir, outputPath) {
|
|
|
233
326
|
archive.finalize();
|
|
234
327
|
});
|
|
235
328
|
}
|
|
236
|
-
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles) {
|
|
329
|
+
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
|
|
237
330
|
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(
|
|
331
|
+
const backupSpinner = (0, import_ora.default)(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${import_chalk.default.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
239
332
|
const tempDir = createTempDir("single-backup");
|
|
333
|
+
let backupProgressSpinner;
|
|
240
334
|
try {
|
|
241
335
|
const remoteFiles = await client.list(dir);
|
|
242
336
|
const backupTasks = singleBackFiles.map((fileName) => {
|
|
@@ -244,11 +338,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
244
338
|
return remoteFile ? { fileName, exists: true } : { fileName, exists: false };
|
|
245
339
|
}).filter((task) => task.exists);
|
|
246
340
|
if (backupTasks.length === 0) {
|
|
247
|
-
backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
|
|
341
|
+
backupSpinner.warn("\u26A0\uFE0F \u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
|
|
248
342
|
return;
|
|
249
343
|
}
|
|
344
|
+
backupSpinner.stop();
|
|
345
|
+
if (showBackFile) {
|
|
346
|
+
console.log(import_chalk.default.cyan(`
|
|
347
|
+
\u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
|
|
348
|
+
backupTasks.forEach((task) => {
|
|
349
|
+
console.log(import_chalk.default.gray(` - ${task.fileName}`));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
backupProgressSpinner = (0, import_ora.default)("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
|
|
250
353
|
const concurrencyLimit = 3;
|
|
251
354
|
let backedUpCount = 0;
|
|
355
|
+
const backedUpFiles = [];
|
|
252
356
|
for (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
|
|
253
357
|
const batch = backupTasks.slice(i, i + concurrencyLimit);
|
|
254
358
|
const promises = batch.map(async ({ fileName }) => {
|
|
@@ -256,12 +360,15 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
256
360
|
const localTempPath = import_node_path.default.join(tempDir.path, fileName);
|
|
257
361
|
const [name, ext] = fileName.split(".");
|
|
258
362
|
const suffix = ext ? `.${ext}` : "";
|
|
259
|
-
const
|
|
363
|
+
const backupFileName = `${name}.${timestamp}${suffix}`;
|
|
364
|
+
const backupRemotePath = (0, import_vite.normalizePath)(`${dir}/${backupFileName}`);
|
|
260
365
|
await client.downloadTo(localTempPath, (0, import_vite.normalizePath)(`${dir}/${fileName}`));
|
|
261
366
|
await client.uploadFrom(localTempPath, backupRemotePath);
|
|
367
|
+
const backupUrl = buildUrl(protocol, baseUrl, backupRemotePath);
|
|
368
|
+
backedUpFiles.push(backupUrl);
|
|
262
369
|
return true;
|
|
263
370
|
} catch (error) {
|
|
264
|
-
console.warn(import_chalk.default.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
371
|
+
console.warn(import_chalk.default.yellow(`\u26A0\uFE0F \u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
265
372
|
return false;
|
|
266
373
|
}
|
|
267
374
|
});
|
|
@@ -269,12 +376,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
269
376
|
backedUpCount += results.filter(Boolean).length;
|
|
270
377
|
}
|
|
271
378
|
if (backedUpCount > 0) {
|
|
272
|
-
|
|
379
|
+
backupProgressSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
|
|
380
|
+
console.log(import_chalk.default.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
381
|
+
backedUpFiles.forEach((url) => {
|
|
382
|
+
console.log(import_chalk.default.green(` ${url}`));
|
|
383
|
+
});
|
|
384
|
+
console.log();
|
|
273
385
|
} else {
|
|
274
|
-
|
|
386
|
+
backupProgressSpinner.fail("\u274C \u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
|
|
275
387
|
}
|
|
276
388
|
} catch (error) {
|
|
277
|
-
|
|
389
|
+
if (backupProgressSpinner) {
|
|
390
|
+
backupProgressSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
391
|
+
} else {
|
|
392
|
+
backupSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
393
|
+
}
|
|
278
394
|
throw error;
|
|
279
395
|
} finally {
|
|
280
396
|
tempDir.cleanup();
|
package/dist/index.mjs
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
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";
|
|
6
6
|
import dayjs from "dayjs";
|
|
7
|
-
import fs from "
|
|
8
|
-
import os from "
|
|
9
|
-
import path from "
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import os from "os";
|
|
9
|
+
import path from "path";
|
|
10
10
|
import ora from "ora";
|
|
11
11
|
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,133 @@ 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\u26A0\uFE0F \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
|
+
} catch (error) {
|
|
165
|
+
if (uploadSpinner) {
|
|
166
|
+
uploadSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
|
|
167
|
+
}
|
|
168
|
+
console.error(chalk.red(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
169
|
+
throw error;
|
|
170
|
+
} finally {
|
|
171
|
+
client.close();
|
|
89
172
|
}
|
|
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
173
|
}
|
|
96
174
|
}
|
|
97
175
|
}
|
|
176
|
+
function validateFtpConfig(config) {
|
|
177
|
+
return !!(config.host && config.user && config.password);
|
|
178
|
+
}
|
|
98
179
|
function parseAlias(alias = "") {
|
|
99
180
|
const [protocol = "", baseUrl = ""] = alias.split("://");
|
|
100
181
|
return {
|
|
@@ -119,12 +200,12 @@ async function connectWithRetry(client, config, maxRetries, retryDelay) {
|
|
|
119
200
|
} catch (error) {
|
|
120
201
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
121
202
|
if (attempt < maxRetries) {
|
|
122
|
-
console.log(chalk.yellow(`\u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
203
|
+
console.log(chalk.yellow(`\u26A0\uFE0F \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
123
204
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
124
205
|
}
|
|
125
206
|
}
|
|
126
207
|
}
|
|
127
|
-
throw new Error(
|
|
208
|
+
throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
|
|
128
209
|
}
|
|
129
210
|
function createTempDir(basePath) {
|
|
130
211
|
const tempBaseDir = os.tmpdir();
|
|
@@ -140,13 +221,13 @@ function createTempDir(basePath) {
|
|
|
140
221
|
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
141
222
|
}
|
|
142
223
|
} catch (error) {
|
|
143
|
-
console.warn(chalk.yellow(`\u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
224
|
+
console.warn(chalk.yellow(`\u26A0\uFE0F \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
144
225
|
}
|
|
145
226
|
}
|
|
146
227
|
};
|
|
147
228
|
}
|
|
148
|
-
async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
149
|
-
const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(
|
|
229
|
+
async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
|
|
230
|
+
const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
150
231
|
const fileName = `backup_${dayjs().format("YYYYMMDD_HHmmss")}.zip`;
|
|
151
232
|
const tempDir = createTempDir("backup-zip");
|
|
152
233
|
const zipFilePath = path.join(os.tmpdir(), "vite-plugin-deploy-ftp", fileName);
|
|
@@ -155,21 +236,33 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
155
236
|
if (!fs.existsSync(zipDir)) {
|
|
156
237
|
fs.mkdirSync(zipDir, { recursive: true });
|
|
157
238
|
}
|
|
158
|
-
await client.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
239
|
+
const remoteFiles = await client.list(dir);
|
|
240
|
+
const filteredFiles = remoteFiles.filter((file) => !file.name.startsWith("backup_") || !file.name.endsWith(".zip"));
|
|
241
|
+
if (showBackFile) {
|
|
242
|
+
console.log(chalk.cyan(`
|
|
243
|
+
\u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${filteredFiles.length} \u4E2A\u6587\u4EF6:`));
|
|
244
|
+
filteredFiles.forEach((file) => {
|
|
245
|
+
console.log(chalk.gray(` - ${file.name} (${file.size} bytes)`));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
for (const file of filteredFiles) {
|
|
249
|
+
if (file.type === 1) {
|
|
250
|
+
await client.downloadTo(path.join(tempDir.path, file.name), normalizePath(`${dir}/${file.name}`));
|
|
163
251
|
}
|
|
164
|
-
}
|
|
252
|
+
}
|
|
253
|
+
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
|
|
165
254
|
await createZipFile(tempDir.path, zipFilePath);
|
|
166
255
|
backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk.yellow(
|
|
167
|
-
|
|
256
|
+
`==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
|
|
168
257
|
)}`;
|
|
169
258
|
await client.uploadFrom(zipFilePath, normalizePath(`${dir}/${fileName}`));
|
|
170
|
-
|
|
259
|
+
const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
|
|
260
|
+
backupSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
|
|
261
|
+
console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
262
|
+
console.log(chalk.green(` ${backupUrl}`));
|
|
263
|
+
console.log();
|
|
171
264
|
} catch (error) {
|
|
172
|
-
backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
|
|
265
|
+
backupSpinner.fail("\u274C \u5907\u4EFD\u5931\u8D25");
|
|
173
266
|
throw error;
|
|
174
267
|
} finally {
|
|
175
268
|
tempDir.cleanup();
|
|
@@ -178,7 +271,7 @@ async function createBackupFile(client, dir, protocol, baseUrl) {
|
|
|
178
271
|
fs.rmSync(zipFilePath);
|
|
179
272
|
}
|
|
180
273
|
} catch (error) {
|
|
181
|
-
console.warn(chalk.yellow("\u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
274
|
+
console.warn(chalk.yellow("\u26A0\uFE0F \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
182
275
|
}
|
|
183
276
|
}
|
|
184
277
|
}
|
|
@@ -199,10 +292,11 @@ async function createZipFile(sourceDir, outputPath) {
|
|
|
199
292
|
archive.finalize();
|
|
200
293
|
});
|
|
201
294
|
}
|
|
202
|
-
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles) {
|
|
295
|
+
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
|
|
203
296
|
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
|
204
|
-
const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(
|
|
297
|
+
const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
205
298
|
const tempDir = createTempDir("single-backup");
|
|
299
|
+
let backupProgressSpinner;
|
|
206
300
|
try {
|
|
207
301
|
const remoteFiles = await client.list(dir);
|
|
208
302
|
const backupTasks = singleBackFiles.map((fileName) => {
|
|
@@ -210,11 +304,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
210
304
|
return remoteFile ? { fileName, exists: true } : { fileName, exists: false };
|
|
211
305
|
}).filter((task) => task.exists);
|
|
212
306
|
if (backupTasks.length === 0) {
|
|
213
|
-
backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
|
|
307
|
+
backupSpinner.warn("\u26A0\uFE0F \u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
|
|
214
308
|
return;
|
|
215
309
|
}
|
|
310
|
+
backupSpinner.stop();
|
|
311
|
+
if (showBackFile) {
|
|
312
|
+
console.log(chalk.cyan(`
|
|
313
|
+
\u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
|
|
314
|
+
backupTasks.forEach((task) => {
|
|
315
|
+
console.log(chalk.gray(` - ${task.fileName}`));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
|
|
216
319
|
const concurrencyLimit = 3;
|
|
217
320
|
let backedUpCount = 0;
|
|
321
|
+
const backedUpFiles = [];
|
|
218
322
|
for (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
|
|
219
323
|
const batch = backupTasks.slice(i, i + concurrencyLimit);
|
|
220
324
|
const promises = batch.map(async ({ fileName }) => {
|
|
@@ -222,12 +326,15 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
222
326
|
const localTempPath = path.join(tempDir.path, fileName);
|
|
223
327
|
const [name, ext] = fileName.split(".");
|
|
224
328
|
const suffix = ext ? `.${ext}` : "";
|
|
225
|
-
const
|
|
329
|
+
const backupFileName = `${name}.${timestamp}${suffix}`;
|
|
330
|
+
const backupRemotePath = normalizePath(`${dir}/${backupFileName}`);
|
|
226
331
|
await client.downloadTo(localTempPath, normalizePath(`${dir}/${fileName}`));
|
|
227
332
|
await client.uploadFrom(localTempPath, backupRemotePath);
|
|
333
|
+
const backupUrl = buildUrl(protocol, baseUrl, backupRemotePath);
|
|
334
|
+
backedUpFiles.push(backupUrl);
|
|
228
335
|
return true;
|
|
229
336
|
} catch (error) {
|
|
230
|
-
console.warn(chalk.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
337
|
+
console.warn(chalk.yellow(`\u26A0\uFE0F \u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
231
338
|
return false;
|
|
232
339
|
}
|
|
233
340
|
});
|
|
@@ -235,12 +342,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
235
342
|
backedUpCount += results.filter(Boolean).length;
|
|
236
343
|
}
|
|
237
344
|
if (backedUpCount > 0) {
|
|
238
|
-
|
|
345
|
+
backupProgressSpinner.succeed("\u2705 \u5907\u4EFD\u5B8C\u6210");
|
|
346
|
+
console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
347
|
+
backedUpFiles.forEach((url) => {
|
|
348
|
+
console.log(chalk.green(` ${url}`));
|
|
349
|
+
});
|
|
350
|
+
console.log();
|
|
239
351
|
} else {
|
|
240
|
-
|
|
352
|
+
backupProgressSpinner.fail("\u274C \u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
|
|
241
353
|
}
|
|
242
354
|
} catch (error) {
|
|
243
|
-
|
|
355
|
+
if (backupProgressSpinner) {
|
|
356
|
+
backupProgressSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
357
|
+
} else {
|
|
358
|
+
backupSpinner.fail("\u274C \u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
359
|
+
}
|
|
244
360
|
throw error;
|
|
245
361
|
} finally {
|
|
246
362
|
tempDir.cleanup();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-deploy-ftp",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,21 +25,21 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"description": "将dist目录下的文件上传到ftp服务器",
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^22.
|
|
29
|
-
"tsup": "^8.
|
|
30
|
-
"typescript": "^5.
|
|
28
|
+
"@types/node": "^22.15.32",
|
|
29
|
+
"tsup": "^8.5.0",
|
|
30
|
+
"typescript": "^5.8.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"vite": "^6.0.3"
|
|
33
|
+
"vite": "^6.0.3 || ^7"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@inquirer/prompts": "^7.
|
|
36
|
+
"@inquirer/prompts": "^7.5.3",
|
|
37
37
|
"@types/archiver": "^6.0.3",
|
|
38
38
|
"archiver": "^7.0.1",
|
|
39
39
|
"basic-ftp": "^5.0.5",
|
|
40
|
-
"chalk": "^5.
|
|
40
|
+
"chalk": "^5.4.1",
|
|
41
41
|
"dayjs": "^1.11.13",
|
|
42
|
-
"ora": "^8.
|
|
42
|
+
"ora": "^8.2.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup",
|