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 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
- host: process.env.zH5FtpHost as string,
29
- port: +(process.env.zH5FtpPort || 21),
30
- user: process.env.zH5FtpUser as string,
31
- password: process.env.zH5FtpPassword as string,
32
- uploadPath: `${env.VITE_FTP_DIRNAME}`,
33
- alias: `https://h5.eventnet.cn/`,
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
- host: string;
5
- port?: number;
6
- user: string;
7
- password: string;
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
- host: string;
5
- port?: number;
6
- user: string;
7
- password: string;
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
- if (!host || !user || !password || !uploadPath) {
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
- const client = new import_basic_ftp.Client();
102
- let uploadSpinner;
103
- try {
104
- uploadSpinner = (0, import_ora.default)("\u51C6\u5907\u81EA\u52A8\u4E0A\u4F20\uFF0C\u521B\u5EFA\u8FDE\u63A5\u4E2D...").start();
105
- await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
106
- uploadSpinner.color = "blue";
107
- uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
108
- const fileList = await client.list(uploadPath);
109
- uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${import_chalk.default.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
110
- if (fileList.length) {
111
- if (singleBack) {
112
- await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles);
113
- } else {
114
- const isBackFiles = await (0, import_prompts.select)({
115
- message: "\u662F\u5426\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6",
116
- choices: ["\u5426", "\u662F"],
117
- default: "\u5426"
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
- if (isBackFiles === "\u662F") {
120
- await createBackupFile(client, uploadPath, protocol, baseUrl);
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(`FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
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(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
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.downloadToDir(tempDir.path, dir);
193
- backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${import_chalk.default.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
194
- import_node_fs.default.readdirSync(tempDir.path).forEach((fileName2) => {
195
- if (fileName2.startsWith("backup_") && fileName2.endsWith(".zip")) {
196
- import_node_fs.default.rmSync(import_node_path.default.join(tempDir.path, fileName2));
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
- `\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
291
+ `==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
202
292
  )}`;
203
293
  await client.uploadFrom(zipFilePath, (0, import_vite.normalizePath)(`${dir}/${fileName}`));
204
- backupSpinner.succeed(`\u5907\u4EFD\u6210\u529F ${import_chalk.default.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`)}`);
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(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
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 backupRemotePath = (0, import_vite.normalizePath)(`${dir}/${name}.${timestamp}${suffix}`);
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
- backupSpinner.succeed(`\u5DF2\u5907\u4EFD ${backedUpCount} \u4E2A\u6587\u4EF6\u5230 ${import_chalk.default.green(buildUrl(protocol, baseUrl, dir))}`);
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
- backupSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
387
+ backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
275
388
  }
276
389
  } catch (error) {
277
- backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
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
- if (!host || !user || !password || !uploadPath) {
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
- const client = new Client();
68
- let uploadSpinner;
69
- try {
70
- uploadSpinner = ora("\u51C6\u5907\u81EA\u52A8\u4E0A\u4F20\uFF0C\u521B\u5EFA\u8FDE\u63A5\u4E2D...").start();
71
- await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
72
- uploadSpinner.color = "blue";
73
- uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
74
- const fileList = await client.list(uploadPath);
75
- uploadSpinner.succeed(`\u5DF2\u8FDE\u63A5 ${chalk.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, uploadPath)}`)}`);
76
- if (fileList.length) {
77
- if (singleBack) {
78
- await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles);
79
- } else {
80
- const isBackFiles = await select({
81
- message: "\u662F\u5426\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6",
82
- choices: ["\u5426", "\u662F"],
83
- default: "\u5426"
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
- if (isBackFiles === "\u662F") {
86
- await createBackupFile(client, uploadPath, protocol, baseUrl);
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(`FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
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(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
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.downloadToDir(tempDir.path, dir);
159
- backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
160
- fs.readdirSync(tempDir.path).forEach((fileName2) => {
161
- if (fileName2.startsWith("backup_") && fileName2.endsWith(".zip")) {
162
- fs.rmSync(path.join(tempDir.path, fileName2));
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
- `\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
257
+ `==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`
168
258
  )}`;
169
259
  await client.uploadFrom(zipFilePath, normalizePath(`${dir}/${fileName}`));
170
- backupSpinner.succeed(`\u5907\u4EFD\u6210\u529F ${chalk.green(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir + "/" + fileName)}`)}`);
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(`\u76EE\u5F55: ==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
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 backupRemotePath = normalizePath(`${dir}/${name}.${timestamp}${suffix}`);
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
- backupSpinner.succeed(`\u5DF2\u5907\u4EFD ${backedUpCount} \u4E2A\u6587\u4EF6\u5230 ${chalk.green(buildUrl(protocol, baseUrl, dir))}`);
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
- backupSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
353
+ backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
241
354
  }
242
355
  } catch (error) {
243
- backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-deploy-ftp",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",