vite-plugin-deploy-ftp 1.2.2 → 3.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 +1 -0
- package/dist/index.d.ts +10 -21
- package/dist/index.js +516 -256
- package/package.json +8 -6
- package/dist/index.d.mts +0 -41
- package/dist/index.mjs +0 -435
package/dist/index.js
CHANGED
|
@@ -1,147 +1,459 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { checkbox, select } from "@inquirer/prompts";
|
|
3
|
+
import archiver from "archiver";
|
|
4
|
+
import { Client } from "basic-ftp";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import dayjs from "dayjs";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import { stat } from "fs/promises";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
import { normalizePath } from "vite";
|
|
13
|
+
var formatBytes = (bytes) => {
|
|
14
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
15
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
16
|
+
let value = bytes;
|
|
17
|
+
let unitIndex = 0;
|
|
18
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
19
|
+
value /= 1024;
|
|
20
|
+
unitIndex++;
|
|
21
|
+
}
|
|
22
|
+
const digits = value >= 100 || unitIndex === 0 ? 0 : 1;
|
|
23
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
24
|
+
};
|
|
25
|
+
var formatDuration = (seconds) => {
|
|
26
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "--";
|
|
27
|
+
const rounded = Math.round(seconds);
|
|
28
|
+
const mins = Math.floor(rounded / 60);
|
|
29
|
+
const secs = rounded % 60;
|
|
30
|
+
if (mins === 0) return `${secs}s`;
|
|
31
|
+
return `${mins}m${String(secs).padStart(2, "0")}s`;
|
|
32
|
+
};
|
|
33
|
+
var trimMiddle = (text, maxLength) => {
|
|
34
|
+
if (text.length <= maxLength) return text;
|
|
35
|
+
if (maxLength <= 10) return text.slice(0, maxLength);
|
|
36
|
+
const leftLength = Math.floor((maxLength - 3) / 2);
|
|
37
|
+
const rightLength = maxLength - 3 - leftLength;
|
|
38
|
+
return `${text.slice(0, leftLength)}...${text.slice(-rightLength)}`;
|
|
11
39
|
};
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
40
|
+
var buildCapsuleBar = (ratio, width = 30) => {
|
|
41
|
+
const safeRatio = Math.max(0, Math.min(1, ratio));
|
|
42
|
+
if (width <= 0) return "";
|
|
43
|
+
if (safeRatio >= 1) {
|
|
44
|
+
return chalk.green("\u2588".repeat(width));
|
|
17
45
|
}
|
|
18
|
-
|
|
46
|
+
const pointerIndex = Math.min(width - 1, Math.floor(width * safeRatio));
|
|
47
|
+
const done = pointerIndex > 0 ? chalk.green("\u2588".repeat(pointerIndex)) : "";
|
|
48
|
+
const pointer = chalk.cyanBright("\u25B8");
|
|
49
|
+
const pending = pointerIndex < width - 1 ? chalk.gray("\u2591".repeat(width - pointerIndex - 1)) : "";
|
|
50
|
+
return `${done}${pointer}${pending}`;
|
|
51
|
+
};
|
|
52
|
+
var normalizeRemotePath = (targetDir, relativeFilePath) => {
|
|
53
|
+
const joined = normalizePath(`${targetDir}/${relativeFilePath}`).replace(/\/{2,}/g, "/");
|
|
54
|
+
if (targetDir.startsWith("/")) return joined.startsWith("/") ? joined : `/${joined}`;
|
|
55
|
+
return joined.replace(/^\/+/, "");
|
|
56
|
+
};
|
|
57
|
+
var sleep = async (ms) => {
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
59
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/index.ts
|
|
31
|
-
var index_exports = {};
|
|
32
|
-
__export(index_exports, {
|
|
33
|
-
default: () => vitePluginDeployFtp
|
|
34
|
-
});
|
|
35
|
-
module.exports = __toCommonJS(index_exports);
|
|
36
|
-
var import_prompts = require("@inquirer/prompts");
|
|
37
|
-
var import_archiver = __toESM(require("archiver"));
|
|
38
|
-
var import_basic_ftp = require("basic-ftp");
|
|
39
|
-
var import_chalk = __toESM(require("chalk"));
|
|
40
|
-
var import_dayjs = __toESM(require("dayjs"));
|
|
41
|
-
var import_node_fs = __toESM(require("fs"));
|
|
42
|
-
var import_node_os = __toESM(require("os"));
|
|
43
|
-
var import_node_path = __toESM(require("path"));
|
|
44
|
-
var import_ora = __toESM(require("ora"));
|
|
45
|
-
var import_vite = require("vite");
|
|
46
60
|
function vitePluginDeployFtp(option) {
|
|
61
|
+
const safeOption = option || {};
|
|
47
62
|
const {
|
|
48
63
|
open = true,
|
|
49
|
-
uploadPath,
|
|
64
|
+
uploadPath = "",
|
|
50
65
|
singleBack = false,
|
|
51
66
|
singleBackFiles = ["index.html"],
|
|
52
67
|
showBackFile = false,
|
|
53
68
|
maxRetries = 3,
|
|
54
69
|
retryDelay = 1e3,
|
|
55
|
-
autoUpload = false
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
autoUpload = false,
|
|
71
|
+
fancy = true,
|
|
72
|
+
failOnError = true,
|
|
73
|
+
concurrency = 1
|
|
74
|
+
} = safeOption;
|
|
75
|
+
const isMultiFtp = "ftps" in safeOption;
|
|
76
|
+
const ftpConfigs = isMultiFtp ? safeOption.ftps || [] : [{ ...safeOption, name: safeOption.name || safeOption.alias || safeOption.host }];
|
|
77
|
+
const defaultFtp = isMultiFtp ? safeOption.defaultFtp : void 0;
|
|
78
|
+
let outDir = normalizePath(path.resolve("dist"));
|
|
79
|
+
let upload = false;
|
|
80
|
+
let buildFailed = false;
|
|
81
|
+
let resolvedConfig = null;
|
|
82
|
+
const useInteractiveOutput = fancy && Boolean(process.stdout?.isTTY) && Boolean(process.stderr?.isTTY) && !process.env.CI;
|
|
83
|
+
const clearScreen = () => {
|
|
84
|
+
if (!useInteractiveOutput) return;
|
|
85
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
86
|
+
};
|
|
87
|
+
const validateOptions = () => {
|
|
88
|
+
const errors = [];
|
|
89
|
+
if (!uploadPath) errors.push("uploadPath is required");
|
|
90
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 1) errors.push("maxRetries must be >= 1");
|
|
91
|
+
if (!Number.isFinite(retryDelay) || retryDelay < 0) errors.push("retryDelay must be >= 0");
|
|
92
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) errors.push("concurrency must be >= 1");
|
|
93
|
+
if (isMultiFtp) {
|
|
94
|
+
if (!ftpConfigs.length) {
|
|
95
|
+
errors.push("ftps is required and must not be empty");
|
|
96
|
+
}
|
|
97
|
+
if (defaultFtp && !ftpConfigs.some((ftp) => ftp.name === defaultFtp)) {
|
|
98
|
+
errors.push(`defaultFtp "${defaultFtp}" does not match any ftp.name`);
|
|
99
|
+
}
|
|
100
|
+
const validConfigCount = ftpConfigs.filter(validateFtpConfig).length;
|
|
101
|
+
if (validConfigCount === 0) {
|
|
102
|
+
errors.push("at least one ftp config requires host, user and password");
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
const singleConfig = ftpConfigs[0];
|
|
106
|
+
if (!singleConfig?.host) errors.push("host is required");
|
|
107
|
+
if (!singleConfig?.user) errors.push("user is required");
|
|
108
|
+
if (!singleConfig?.password) errors.push("password is required");
|
|
109
|
+
}
|
|
110
|
+
return errors;
|
|
111
|
+
};
|
|
112
|
+
const uploadFileWithRetry = async (task, context) => {
|
|
113
|
+
for (let attempt = 1; attempt <= context.maxRetries; attempt++) {
|
|
114
|
+
try {
|
|
115
|
+
await context.ensureConnected();
|
|
116
|
+
const remoteDir = normalizePath(path.posix.dirname(task.remotePath));
|
|
117
|
+
if (remoteDir && remoteDir !== "." && !context.ensuredDirs.has(remoteDir)) {
|
|
118
|
+
await context.client.ensureDir(remoteDir);
|
|
119
|
+
context.ensuredDirs.add(remoteDir);
|
|
120
|
+
}
|
|
121
|
+
await context.client.uploadFrom(task.filePath, path.posix.basename(task.remotePath));
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
file: task.filePath,
|
|
125
|
+
name: task.remotePath,
|
|
126
|
+
size: task.size,
|
|
127
|
+
retries: attempt - 1
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
context.markDisconnected();
|
|
131
|
+
try {
|
|
132
|
+
context.client.close();
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
if (attempt === context.maxRetries) {
|
|
136
|
+
if (!context.silentLogs) {
|
|
137
|
+
console.log(
|
|
138
|
+
`${chalk.red("\u2717")} ${task.filePath} => ${error instanceof Error ? error.message : String(error)}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
file: task.filePath,
|
|
144
|
+
name: task.remotePath,
|
|
145
|
+
size: task.size,
|
|
146
|
+
retries: attempt - 1,
|
|
147
|
+
error
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (!context.silentLogs) {
|
|
151
|
+
console.log(
|
|
152
|
+
`${chalk.yellow("\u26A0")} ${task.filePath} \u4E0A\u4F20\u5931\u8D25\uFF0C\u6B63\u5728\u91CD\u8BD5 (${attempt}/${context.maxRetries})...`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
await sleep(context.retryDelay * attempt);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
61
158
|
return {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} }
|
|
159
|
+
success: false,
|
|
160
|
+
file: task.filePath,
|
|
161
|
+
name: task.remotePath,
|
|
162
|
+
size: task.size,
|
|
163
|
+
retries: context.maxRetries,
|
|
164
|
+
error: new Error("Max retries exceeded")
|
|
69
165
|
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
closeBundle: {
|
|
84
|
-
sequential: true,
|
|
85
|
-
order: "post",
|
|
86
|
-
async handler() {
|
|
87
|
-
if (!open || buildFailed) return;
|
|
166
|
+
};
|
|
167
|
+
const uploadFilesInBatches = async (connectConfig, files, targetDir, windowSize = concurrency) => {
|
|
168
|
+
const results = [];
|
|
169
|
+
const totalFiles = files.length;
|
|
170
|
+
const tasks = [];
|
|
171
|
+
let completed = 0;
|
|
172
|
+
let failed = 0;
|
|
173
|
+
let uploadedBytes = 0;
|
|
174
|
+
let retries = 0;
|
|
175
|
+
const taskCandidates = await Promise.all(
|
|
176
|
+
files.map(async (relativeFilePath) => {
|
|
177
|
+
const filePath = normalizePath(path.resolve(outDir, relativeFilePath));
|
|
178
|
+
const remotePath = normalizeRemotePath(targetDir, relativeFilePath);
|
|
88
179
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
180
|
+
const fileStats = await stat(filePath);
|
|
181
|
+
return { task: { filePath, remotePath, size: fileStats.size } };
|
|
91
182
|
} catch (error) {
|
|
92
|
-
|
|
93
|
-
|
|
183
|
+
return { task: null, error, filePath, remotePath };
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
for (const candidate of taskCandidates) {
|
|
188
|
+
if (candidate.task) {
|
|
189
|
+
tasks.push(candidate.task);
|
|
190
|
+
} else {
|
|
191
|
+
failed++;
|
|
192
|
+
completed++;
|
|
193
|
+
results.push({
|
|
194
|
+
success: false,
|
|
195
|
+
file: candidate.filePath,
|
|
196
|
+
name: candidate.remotePath,
|
|
197
|
+
size: 0,
|
|
198
|
+
retries: 0,
|
|
199
|
+
error: candidate.error
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const totalBytes = tasks.reduce((sum, task) => sum + task.size, 0);
|
|
204
|
+
const startAt = Date.now();
|
|
205
|
+
const safeWindowSize = Math.max(1, Math.min(windowSize, tasks.length || 1));
|
|
206
|
+
const activeFiles = /* @__PURE__ */ new Set();
|
|
207
|
+
const silentLogs = Boolean(useInteractiveOutput);
|
|
208
|
+
const spinner = useInteractiveOutput ? ora({ text: "\u51C6\u5907\u4E0A\u4F20...", spinner: "dots12" }).start() : null;
|
|
209
|
+
const reportEvery = Math.max(1, Math.ceil(totalFiles / 10));
|
|
210
|
+
let lastReportedCompleted = -1;
|
|
211
|
+
const updateProgress = () => {
|
|
212
|
+
const progressRatio = totalFiles > 0 ? completed / totalFiles : 1;
|
|
213
|
+
const percentage = Math.round(progressRatio * 100);
|
|
214
|
+
const elapsedSeconds = (Date.now() - startAt) / 1e3;
|
|
215
|
+
const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
|
|
216
|
+
const etaSeconds = speed > 0 ? Math.max(0, (totalBytes - uploadedBytes) / speed) : 0;
|
|
217
|
+
const activeList = Array.from(activeFiles);
|
|
218
|
+
const currentFile = activeList.length > 0 ? trimMiddle(activeList[activeList.length - 1], 86) : "-";
|
|
219
|
+
if (!spinner) {
|
|
220
|
+
if (completed === lastReportedCompleted) return;
|
|
221
|
+
if (completed === totalFiles || completed % reportEvery === 0) {
|
|
222
|
+
console.log(
|
|
223
|
+
`${chalk.gray("\u8FDB\u5EA6:")} ${completed}/${totalFiles} (${percentage}%) | ${chalk.gray("\u6570\u636E:")} ${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)} | ${chalk.gray("\u901F\u5EA6:")} ${formatBytes(speed)}/s`
|
|
224
|
+
);
|
|
225
|
+
lastReportedCompleted = completed;
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const bar = buildCapsuleBar(progressRatio);
|
|
230
|
+
const warnLine = retries > 0 || failed > 0 ? `
|
|
231
|
+
${chalk.yellow("\u91CD\u8BD5")}: ${retries} ${chalk.yellow("\u5931\u8D25")}: ${failed}` : "";
|
|
232
|
+
spinner.text = [
|
|
233
|
+
`${chalk.cyan("\u6B63\u5728\u4E0A\u4F20:")} ${chalk.white(currentFile)}`,
|
|
234
|
+
`${bar} ${chalk.bold(`${percentage}%`)} ${chalk.gray(`(${completed}/${totalFiles})`)} ${chalk.gray("|")} ${chalk.blue(formatBytes(uploadedBytes))}/${chalk.blue(formatBytes(totalBytes))} ${chalk.gray("|")} ${chalk.magenta(`${formatBytes(speed)}/s`)} ${chalk.gray("|")} \u9884\u8BA1 ${chalk.yellow(formatDuration(etaSeconds))}`
|
|
235
|
+
].join("\n");
|
|
236
|
+
spinner.text += warnLine;
|
|
237
|
+
};
|
|
238
|
+
const refreshTimer = spinner ? setInterval(updateProgress, 120) : null;
|
|
239
|
+
let currentIndex = 0;
|
|
240
|
+
const worker = async () => {
|
|
241
|
+
const client = new Client();
|
|
242
|
+
const ensuredDirs = /* @__PURE__ */ new Set();
|
|
243
|
+
let connected = false;
|
|
244
|
+
const ensureConnected = async () => {
|
|
245
|
+
if (connected) return;
|
|
246
|
+
await connectWithRetry(client, connectConfig, maxRetries, retryDelay, true);
|
|
247
|
+
connected = true;
|
|
248
|
+
};
|
|
249
|
+
const markDisconnected = () => {
|
|
250
|
+
connected = false;
|
|
251
|
+
ensuredDirs.clear();
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
while (true) {
|
|
255
|
+
const index = currentIndex++;
|
|
256
|
+
if (index >= tasks.length) return;
|
|
257
|
+
const task = tasks[index];
|
|
258
|
+
activeFiles.add(task.remotePath);
|
|
259
|
+
updateProgress();
|
|
260
|
+
const result = await uploadFileWithRetry(task, {
|
|
261
|
+
client,
|
|
262
|
+
ensuredDirs,
|
|
263
|
+
ensureConnected,
|
|
264
|
+
markDisconnected,
|
|
265
|
+
silentLogs,
|
|
266
|
+
maxRetries,
|
|
267
|
+
retryDelay
|
|
268
|
+
});
|
|
269
|
+
completed++;
|
|
270
|
+
retries += result.retries;
|
|
271
|
+
if (result.success) {
|
|
272
|
+
uploadedBytes += result.size;
|
|
273
|
+
} else {
|
|
274
|
+
failed++;
|
|
275
|
+
}
|
|
276
|
+
results.push(result);
|
|
277
|
+
activeFiles.delete(task.remotePath);
|
|
278
|
+
updateProgress();
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
client.close();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
updateProgress();
|
|
285
|
+
try {
|
|
286
|
+
await Promise.all(Array.from({ length: safeWindowSize }, () => worker()));
|
|
287
|
+
} finally {
|
|
288
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
289
|
+
}
|
|
290
|
+
if (spinner) {
|
|
291
|
+
const elapsedSeconds = (Date.now() - startAt) / 1e3;
|
|
292
|
+
const successCount = results.filter((item) => item.success).length;
|
|
293
|
+
const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
|
|
294
|
+
spinner.succeed(
|
|
295
|
+
`${chalk.green("\u4E0A\u4F20\u6210\u529F")} ${successCount} \u4E2A\u6587\u4EF6\u3002
|
|
296
|
+
${buildCapsuleBar(1)} 100% (${totalFiles}/${totalFiles}) ${chalk.gray("|")} \u901F\u5EA6 ${chalk.magenta(`${formatBytes(speed)}/s`)} ${chalk.gray("|")} \u8017\u65F6 ${chalk.yellow(formatDuration(elapsedSeconds))}`
|
|
297
|
+
);
|
|
298
|
+
} else {
|
|
299
|
+
console.log(`${chalk.green("\u2714")} \u6240\u6709\u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
|
|
300
|
+
}
|
|
301
|
+
return results;
|
|
302
|
+
};
|
|
303
|
+
const deploySingleTarget = async (ftpConfig) => {
|
|
304
|
+
const { host, port = 21, user, password, alias = "", name } = ftpConfig;
|
|
305
|
+
if (!host || !user || !password) {
|
|
306
|
+
console.error(chalk.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
|
|
307
|
+
if (!host) console.error(chalk.red(" - \u7F3A\u5C11 host"));
|
|
308
|
+
if (!user) console.error(chalk.red(" - \u7F3A\u5C11 user"));
|
|
309
|
+
if (!password) console.error(chalk.red(" - \u7F3A\u5C11 password"));
|
|
310
|
+
return { name: name || host || "unknown", totalFiles: 0, failedCount: 1 };
|
|
311
|
+
}
|
|
312
|
+
const allFiles = getAllFiles(outDir);
|
|
313
|
+
const totalFiles = allFiles.length;
|
|
314
|
+
const { protocol, baseUrl } = parseAlias(alias);
|
|
315
|
+
const displayName = name || host;
|
|
316
|
+
const startTime = Date.now();
|
|
317
|
+
if (allFiles.length === 0) {
|
|
318
|
+
console.log(`${chalk.yellow("\u26A0 \u6CA1\u6709\u627E\u5230\u9700\u8981\u4E0A\u4F20\u7684\u6587\u4EF6")}`);
|
|
319
|
+
return { name: displayName, totalFiles: 0, failedCount: 0 };
|
|
320
|
+
}
|
|
321
|
+
clearScreen();
|
|
322
|
+
console.log(chalk.cyan(`
|
|
323
|
+
\u{1F680} FTP \u90E8\u7F72\u5F00\u59CB
|
|
324
|
+
`));
|
|
325
|
+
console.log(`${chalk.gray("Server:")} ${chalk.green(displayName)}`);
|
|
326
|
+
console.log(`${chalk.gray("Host:")} ${chalk.green(host)}`);
|
|
327
|
+
console.log(`${chalk.gray("Source:")} ${chalk.yellow(outDir)}`);
|
|
328
|
+
console.log(`${chalk.gray("Target:")} ${chalk.yellow(uploadPath)}`);
|
|
329
|
+
if (alias) console.log(`${chalk.gray("Alias:")} ${chalk.green(alias)}`);
|
|
330
|
+
console.log(`${chalk.gray("Files:")} ${chalk.blue(totalFiles)}
|
|
331
|
+
`);
|
|
332
|
+
const connectConfig = { host, port, user, password };
|
|
333
|
+
const preflightClient = new Client();
|
|
334
|
+
const preflightSpinner = useInteractiveOutput ? ora(`\u8FDE\u63A5\u5230 ${displayName}...`).start() : null;
|
|
335
|
+
try {
|
|
336
|
+
await connectWithRetry(preflightClient, connectConfig, maxRetries, retryDelay, Boolean(preflightSpinner));
|
|
337
|
+
if (preflightSpinner) preflightSpinner.succeed("\u8FDE\u63A5\u6210\u529F");
|
|
338
|
+
await preflightClient.ensureDir(uploadPath);
|
|
339
|
+
const fileList = await preflightClient.list(uploadPath);
|
|
340
|
+
if (fileList.length) {
|
|
341
|
+
if (singleBack) {
|
|
342
|
+
await createSingleBackup(preflightClient, uploadPath, protocol, baseUrl, singleBackFiles, showBackFile);
|
|
343
|
+
} else {
|
|
344
|
+
const shouldBackup = await select({
|
|
345
|
+
message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
|
|
346
|
+
choices: ["\u5426", "\u662F"],
|
|
347
|
+
default: "\u5426"
|
|
348
|
+
});
|
|
349
|
+
if (shouldBackup === "\u662F") {
|
|
350
|
+
await createBackupFile(preflightClient, uploadPath, protocol, baseUrl, showBackFile);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const results = await uploadFilesInBatches(connectConfig, allFiles, uploadPath, concurrency);
|
|
355
|
+
const successCount = results.filter((r) => r.success).length;
|
|
356
|
+
const failedCount = results.length - successCount;
|
|
357
|
+
const durationSeconds = (Date.now() - startTime) / 1e3;
|
|
358
|
+
const duration = durationSeconds.toFixed(2);
|
|
359
|
+
const uploadedBytes = results.reduce((sum, result) => result.success ? sum + result.size : sum, 0);
|
|
360
|
+
const retryCount = results.reduce((sum, result) => sum + result.retries, 0);
|
|
361
|
+
const avgSpeed = durationSeconds > 0 ? uploadedBytes / durationSeconds : 0;
|
|
362
|
+
clearScreen();
|
|
363
|
+
console.log("\n" + chalk.gray("\u2500".repeat(40)) + "\n");
|
|
364
|
+
if (failedCount === 0) {
|
|
365
|
+
console.log(`${chalk.green("\u{1F389} \u90E8\u7F72\u6210\u529F!")}`);
|
|
366
|
+
} else {
|
|
367
|
+
console.log(`${chalk.yellow("\u26A0 \u90E8\u7F72\u5B8C\u6210\u4F46\u5B58\u5728\u9519\u8BEF")}`);
|
|
368
|
+
}
|
|
369
|
+
console.log(`
|
|
370
|
+
${chalk.gray("\u7EDF\u8BA1:")}`);
|
|
371
|
+
console.log(` ${chalk.green("\u2714")} \u6210\u529F: ${chalk.bold(successCount)}`);
|
|
372
|
+
if (failedCount > 0) {
|
|
373
|
+
console.log(` ${chalk.red("\u2717")} \u5931\u8D25: ${chalk.bold(failedCount)}`);
|
|
374
|
+
}
|
|
375
|
+
console.log(` ${chalk.cyan("\u21C4")} \u91CD\u8BD5: ${chalk.bold(retryCount)}`);
|
|
376
|
+
console.log(` ${chalk.blue("\u{1F4E6}")} \u6570\u636E: ${chalk.bold(formatBytes(uploadedBytes))}`);
|
|
377
|
+
console.log(` ${chalk.magenta("\u26A1")} \u5E73\u5747\u901F\u5EA6: ${chalk.bold(`${formatBytes(avgSpeed)}/s`)}`);
|
|
378
|
+
console.log(` ${chalk.blue("\u23F1")} \u8017\u65F6: ${chalk.bold(duration)}s`);
|
|
379
|
+
if (baseUrl) {
|
|
380
|
+
console.log(` ${chalk.green("\u{1F517}")} \u8BBF\u95EE\u5730\u5740: ${chalk.bold(buildUrl(protocol, baseUrl, uploadPath))}`);
|
|
381
|
+
}
|
|
382
|
+
console.log("");
|
|
383
|
+
if (failedCount > 0) {
|
|
384
|
+
const failedItems = results.filter((result) => !result.success);
|
|
385
|
+
const previewCount = Math.min(5, failedItems.length);
|
|
386
|
+
console.log(chalk.red("\u5931\u8D25\u660E\u7EC6:"));
|
|
387
|
+
for (let i = 0; i < previewCount; i++) {
|
|
388
|
+
const item = failedItems[i];
|
|
389
|
+
const reason = item.error?.message || "unknown error";
|
|
390
|
+
console.log(` ${chalk.red("\u2022")} ${item.name} => ${reason}`);
|
|
391
|
+
}
|
|
392
|
+
if (failedItems.length > previewCount) {
|
|
393
|
+
console.log(chalk.gray(` ... \u8FD8\u6709 ${failedItems.length - previewCount} \u4E2A\u5931\u8D25\u6587\u4EF6`));
|
|
94
394
|
}
|
|
395
|
+
console.log("");
|
|
95
396
|
}
|
|
397
|
+
return { name: displayName, totalFiles: results.length, failedCount };
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (preflightSpinner) preflightSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
|
|
400
|
+
console.log(`
|
|
401
|
+
${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} ${error}
|
|
402
|
+
`);
|
|
403
|
+
return {
|
|
404
|
+
name: displayName,
|
|
405
|
+
totalFiles,
|
|
406
|
+
failedCount: totalFiles > 0 ? totalFiles : 1,
|
|
407
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
408
|
+
};
|
|
409
|
+
} finally {
|
|
410
|
+
preflightClient.close();
|
|
96
411
|
}
|
|
97
412
|
};
|
|
98
|
-
async
|
|
413
|
+
const deployToFtp = async () => {
|
|
99
414
|
if (!autoUpload) {
|
|
100
|
-
const ftpUploadChoice = await
|
|
415
|
+
const ftpUploadChoice = await select({
|
|
101
416
|
message: "\u662F\u5426\u4E0A\u4F20FTP",
|
|
102
417
|
choices: ["\u662F", "\u5426"],
|
|
103
418
|
default: "\u662F"
|
|
104
419
|
});
|
|
105
|
-
if (ftpUploadChoice === "\u5426") return;
|
|
420
|
+
if (ftpUploadChoice === "\u5426") return [];
|
|
106
421
|
}
|
|
107
422
|
let selectedConfigs = [];
|
|
108
423
|
if (isMultiFtp) {
|
|
109
424
|
if (defaultFtp) {
|
|
110
425
|
const defaultConfig = ftpConfigs.find((ftp) => ftp.name === defaultFtp);
|
|
111
|
-
if (defaultConfig) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
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`));
|
|
117
|
-
}
|
|
426
|
+
if (defaultConfig && validateFtpConfig(defaultConfig)) {
|
|
427
|
+
console.log(chalk.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
|
|
428
|
+
selectedConfigs = [defaultConfig];
|
|
429
|
+
} else if (defaultConfig) {
|
|
430
|
+
console.log(chalk.yellow(`\u26A0 \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
|
|
118
431
|
}
|
|
119
432
|
}
|
|
120
433
|
if (selectedConfigs.length === 0) {
|
|
121
434
|
const validConfigs = ftpConfigs.filter(validateFtpConfig);
|
|
122
435
|
const invalidConfigs = ftpConfigs.filter((config) => !validateFtpConfig(config));
|
|
123
436
|
if (invalidConfigs.length > 0) {
|
|
124
|
-
console.log(
|
|
437
|
+
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:"));
|
|
125
438
|
invalidConfigs.forEach((config) => {
|
|
126
439
|
const missing = [];
|
|
127
440
|
if (!config.host) missing.push("host");
|
|
128
441
|
if (!config.user) missing.push("user");
|
|
129
442
|
if (!config.password) missing.push("password");
|
|
130
|
-
console.log(
|
|
443
|
+
console.log(chalk.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
|
|
131
444
|
});
|
|
132
445
|
console.log();
|
|
133
446
|
}
|
|
134
447
|
if (validConfigs.length === 0) {
|
|
135
|
-
console.error(
|
|
136
|
-
return;
|
|
448
|
+
console.error(chalk.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
|
|
449
|
+
return [];
|
|
137
450
|
}
|
|
138
|
-
|
|
139
|
-
name: ftp.name,
|
|
140
|
-
value: ftp
|
|
141
|
-
}));
|
|
142
|
-
selectedConfigs = await (0, import_prompts.checkbox)({
|
|
451
|
+
selectedConfigs = await checkbox({
|
|
143
452
|
message: "\u9009\u62E9\u8981\u4E0A\u4F20\u7684FTP\u670D\u52A1\u5668\uFF08\u53EF\u591A\u9009\uFF09",
|
|
144
|
-
choices
|
|
453
|
+
choices: validConfigs.map((ftp) => ({
|
|
454
|
+
name: ftp.name || ftp.host || "\u672A\u547D\u540DFTP",
|
|
455
|
+
value: ftp
|
|
456
|
+
})),
|
|
145
457
|
required: true
|
|
146
458
|
});
|
|
147
459
|
}
|
|
@@ -151,130 +463,71 @@ function vitePluginDeployFtp(option) {
|
|
|
151
463
|
selectedConfigs = [{ ...singleConfig, name: singleConfig.name || singleConfig.host }];
|
|
152
464
|
} else {
|
|
153
465
|
const missing = [];
|
|
154
|
-
if (!singleConfig
|
|
155
|
-
if (!singleConfig
|
|
156
|
-
if (!singleConfig
|
|
157
|
-
console.error(
|
|
158
|
-
return;
|
|
466
|
+
if (!singleConfig?.host) missing.push("host");
|
|
467
|
+
if (!singleConfig?.user) missing.push("user");
|
|
468
|
+
if (!singleConfig?.password) missing.push("password");
|
|
469
|
+
console.error(chalk.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
|
|
470
|
+
return [];
|
|
159
471
|
}
|
|
160
472
|
}
|
|
473
|
+
const deployResults = [];
|
|
161
474
|
for (const ftpConfig of selectedConfigs) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
475
|
+
const targetResult = await deploySingleTarget(ftpConfig);
|
|
476
|
+
deployResults.push(targetResult);
|
|
477
|
+
}
|
|
478
|
+
return deployResults;
|
|
479
|
+
};
|
|
480
|
+
return {
|
|
481
|
+
name: "vite-plugin-deploy-ftp",
|
|
482
|
+
apply: "build",
|
|
483
|
+
enforce: "post",
|
|
484
|
+
buildEnd(error) {
|
|
485
|
+
if (error) buildFailed = true;
|
|
486
|
+
},
|
|
487
|
+
config(config) {
|
|
488
|
+
if (!open || buildFailed) return;
|
|
489
|
+
clearScreen();
|
|
490
|
+
const validationErrors = validateOptions();
|
|
491
|
+
if (validationErrors.length > 0) {
|
|
492
|
+
console.log(`${chalk.red("\u2717 \u914D\u7F6E\u9519\u8BEF:")}
|
|
493
|
+
${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
|
|
494
|
+
return;
|
|
169
495
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
uploadSpinner = (0, import_ora.default)(`\u8FDE\u63A5\u5230 ${displayName} \u4E2D...`).start();
|
|
188
|
-
await connectWithRetry(client, { host, port, user, password }, maxRetries, retryDelay);
|
|
189
|
-
uploadSpinner.color = "green";
|
|
190
|
-
uploadSpinner.text = "\u8FDE\u63A5\u6210\u529F";
|
|
191
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
192
|
-
const fileList = await client.list(uploadPath);
|
|
193
|
-
uploadSpinner.succeed("\u8FDE\u63A5\u6210\u529F!");
|
|
194
|
-
const startDir = await client.pwd();
|
|
195
|
-
if (fileList.length) {
|
|
196
|
-
if (singleBack) {
|
|
197
|
-
await createSingleBackup(client, uploadPath, protocol, baseUrl, singleBackFiles, showBackFile);
|
|
198
|
-
} else {
|
|
199
|
-
const isBackFiles = await (0, import_prompts.select)({
|
|
200
|
-
message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
|
|
201
|
-
choices: ["\u5426", "\u662F"],
|
|
202
|
-
default: "\u5426"
|
|
203
|
-
});
|
|
204
|
-
if (isBackFiles === "\u662F") {
|
|
205
|
-
await createBackupFile(client, uploadPath, protocol, baseUrl, showBackFile);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const progressSpinner = (0, import_ora.default)("\u51C6\u5907\u4E0A\u4F20...").start();
|
|
210
|
-
let uploadedCount = 0;
|
|
211
|
-
const groups = {};
|
|
212
|
-
for (const file of allFiles) {
|
|
213
|
-
const dir = import_node_path.default.dirname(file);
|
|
214
|
-
if (!groups[dir]) groups[dir] = [];
|
|
215
|
-
groups[dir].push(import_node_path.default.basename(file));
|
|
216
|
-
}
|
|
217
|
-
for (const relDir of Object.keys(groups)) {
|
|
218
|
-
await client.cd(startDir);
|
|
219
|
-
const remoteDir = (0, import_vite.normalizePath)(import_node_path.default.join(uploadPath, relDir));
|
|
220
|
-
await client.ensureDir(remoteDir);
|
|
221
|
-
for (const fileName of groups[relDir]) {
|
|
222
|
-
const currentFile = import_node_path.default.join(relDir, fileName);
|
|
223
|
-
const displayPath = (0, import_vite.normalizePath)(currentFile);
|
|
224
|
-
progressSpinner.text = `\u6B63\u5728\u4E0A\u4F20: ${import_chalk.default.dim(displayPath)}
|
|
225
|
-
${formatProgressBar(uploadedCount, totalFiles)}`;
|
|
226
|
-
const localFile = import_node_path.default.join(outDir, relDir, fileName);
|
|
227
|
-
await client.uploadFrom(localFile, fileName);
|
|
228
|
-
uploadedCount++;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
progressSpinner.succeed("\u6240\u6709\u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210!");
|
|
232
|
-
console.log(import_chalk.default.green(formatProgressBar(totalFiles, totalFiles)));
|
|
233
|
-
process.stdout.write("\x1B[2J\x1B[0f");
|
|
234
|
-
console.log(import_chalk.default.gray("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
235
|
-
const duration = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
236
|
-
console.log(`\u{1F389} \u90E8\u7F72\u6210\u529F!`);
|
|
237
|
-
console.log();
|
|
238
|
-
console.log(`\u7EDF\u8BA1:`);
|
|
239
|
-
console.log(` \u2714 \u6210\u529F: ${import_chalk.default.green(totalFiles)}`);
|
|
240
|
-
console.log(` \u23F1 \u8017\u65F6: ${import_chalk.default.green(duration + "s")}`);
|
|
241
|
-
console.log();
|
|
242
|
-
if (baseUrl) {
|
|
243
|
-
console.log(`\u8BBF\u95EE\u5730\u5740: ${import_chalk.default.green(buildUrl(protocol, baseUrl, uploadPath))}`);
|
|
244
|
-
console.log();
|
|
245
|
-
}
|
|
246
|
-
} catch (error) {
|
|
247
|
-
if (uploadSpinner) {
|
|
248
|
-
uploadSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
|
|
496
|
+
upload = true;
|
|
497
|
+
return config;
|
|
498
|
+
},
|
|
499
|
+
configResolved(config) {
|
|
500
|
+
resolvedConfig = config;
|
|
501
|
+
outDir = normalizePath(path.resolve(config.root, config.build.outDir));
|
|
502
|
+
},
|
|
503
|
+
closeBundle: {
|
|
504
|
+
sequential: true,
|
|
505
|
+
order: "post",
|
|
506
|
+
async handler() {
|
|
507
|
+
if (!open || !upload || buildFailed || !resolvedConfig) return;
|
|
508
|
+
const deployResults = await deployToFtp();
|
|
509
|
+
if (deployResults.length === 0) return;
|
|
510
|
+
const failedTargets = deployResults.filter((target) => target.failedCount > 0);
|
|
511
|
+
if (failedTargets.length > 0 && failOnError) {
|
|
512
|
+
throw new Error(`Failed to deploy ${failedTargets.length} of ${deployResults.length} FTP targets`);
|
|
249
513
|
}
|
|
250
|
-
console.error(import_chalk.default.red(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
251
|
-
throw error;
|
|
252
|
-
} finally {
|
|
253
|
-
client.close();
|
|
254
514
|
}
|
|
255
515
|
}
|
|
256
|
-
}
|
|
516
|
+
};
|
|
257
517
|
}
|
|
258
518
|
function getAllFiles(dirPath, arrayOfFiles = [], relativePath = "") {
|
|
259
|
-
const files =
|
|
260
|
-
files.forEach(
|
|
261
|
-
const fullPath =
|
|
262
|
-
const relPath =
|
|
263
|
-
if (
|
|
519
|
+
const files = fs.readdirSync(dirPath);
|
|
520
|
+
files.forEach((file) => {
|
|
521
|
+
const fullPath = path.join(dirPath, file);
|
|
522
|
+
const relPath = path.join(relativePath, file);
|
|
523
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
264
524
|
getAllFiles(fullPath, arrayOfFiles, relPath);
|
|
265
525
|
} else {
|
|
266
|
-
arrayOfFiles.push(relPath);
|
|
526
|
+
arrayOfFiles.push(normalizePath(relPath));
|
|
267
527
|
}
|
|
268
528
|
});
|
|
269
529
|
return arrayOfFiles;
|
|
270
530
|
}
|
|
271
|
-
function formatProgressBar(current, total, width = 30) {
|
|
272
|
-
const percentage = Math.round(current / total * 100);
|
|
273
|
-
const filled = Math.round(width * current / total);
|
|
274
|
-
const empty = width - filled;
|
|
275
|
-
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
276
|
-
return `${bar} ${percentage}% (${current}/${total})`;
|
|
277
|
-
}
|
|
278
531
|
function validateFtpConfig(config) {
|
|
279
532
|
return !!(config.host && config.user && config.password);
|
|
280
533
|
}
|
|
@@ -285,10 +538,10 @@ function parseAlias(alias = "") {
|
|
|
285
538
|
baseUrl: baseUrl || ""
|
|
286
539
|
};
|
|
287
540
|
}
|
|
288
|
-
function buildUrl(protocol, baseUrl,
|
|
289
|
-
return protocol +
|
|
541
|
+
function buildUrl(protocol, baseUrl, targetPath) {
|
|
542
|
+
return protocol + normalizePath(baseUrl + targetPath);
|
|
290
543
|
}
|
|
291
|
-
async function connectWithRetry(client, config, maxRetries, retryDelay) {
|
|
544
|
+
async function connectWithRetry(client, config, maxRetries, retryDelay, silentLogs = false) {
|
|
292
545
|
let lastError;
|
|
293
546
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
294
547
|
try {
|
|
@@ -302,66 +555,70 @@ async function connectWithRetry(client, config, maxRetries, retryDelay) {
|
|
|
302
555
|
} catch (error) {
|
|
303
556
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
304
557
|
if (attempt < maxRetries) {
|
|
305
|
-
|
|
306
|
-
|
|
558
|
+
if (!silentLogs) {
|
|
559
|
+
console.log(chalk.yellow(`\u26A0 \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
560
|
+
}
|
|
561
|
+
await sleep(retryDelay * attempt);
|
|
307
562
|
}
|
|
308
563
|
}
|
|
309
564
|
}
|
|
310
565
|
throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
|
|
311
566
|
}
|
|
312
567
|
function createTempDir(basePath) {
|
|
313
|
-
const tempBaseDir =
|
|
314
|
-
const tempPath =
|
|
315
|
-
if (!
|
|
316
|
-
|
|
568
|
+
const tempBaseDir = os.tmpdir();
|
|
569
|
+
const tempPath = path.join(tempBaseDir, "vite-plugin-deploy-ftp", basePath);
|
|
570
|
+
if (!fs.existsSync(tempPath)) {
|
|
571
|
+
fs.mkdirSync(tempPath, { recursive: true });
|
|
317
572
|
}
|
|
318
573
|
return {
|
|
319
574
|
path: tempPath,
|
|
320
575
|
cleanup: () => {
|
|
321
576
|
try {
|
|
322
|
-
if (
|
|
323
|
-
|
|
577
|
+
if (fs.existsSync(tempPath)) {
|
|
578
|
+
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
324
579
|
}
|
|
325
580
|
} catch (error) {
|
|
326
|
-
console.warn(
|
|
581
|
+
console.warn(chalk.yellow(`\u26A0 \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
327
582
|
}
|
|
328
583
|
}
|
|
329
584
|
};
|
|
330
585
|
}
|
|
331
586
|
async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
|
|
332
|
-
const backupSpinner = (
|
|
333
|
-
const fileName = `backup_${(
|
|
587
|
+
const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
588
|
+
const fileName = `backup_${dayjs().format("YYYYMMDD_HHmmss")}.zip`;
|
|
334
589
|
const tempDir = createTempDir("backup-zip");
|
|
335
|
-
const zipFilePath =
|
|
590
|
+
const zipFilePath = path.join(os.tmpdir(), "vite-plugin-deploy-ftp", fileName);
|
|
336
591
|
try {
|
|
337
|
-
const zipDir =
|
|
338
|
-
if (!
|
|
339
|
-
|
|
592
|
+
const zipDir = path.dirname(zipFilePath);
|
|
593
|
+
if (!fs.existsSync(zipDir)) {
|
|
594
|
+
fs.mkdirSync(zipDir, { recursive: true });
|
|
340
595
|
}
|
|
341
596
|
const remoteFiles = await client.list(dir);
|
|
342
|
-
const filteredFiles = remoteFiles.filter(
|
|
597
|
+
const filteredFiles = remoteFiles.filter(
|
|
598
|
+
(file) => !(file.name.startsWith("backup_") && file.name.endsWith(".zip"))
|
|
599
|
+
);
|
|
343
600
|
if (showBackFile) {
|
|
344
|
-
console.log(
|
|
601
|
+
console.log(chalk.cyan(`
|
|
345
602
|
\u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${filteredFiles.length} \u4E2A\u6587\u4EF6:`));
|
|
346
603
|
filteredFiles.forEach((file) => {
|
|
347
|
-
console.log(
|
|
604
|
+
console.log(chalk.gray(` - ${file.name} (${file.size} bytes)`));
|
|
348
605
|
});
|
|
349
606
|
}
|
|
350
607
|
for (const file of filteredFiles) {
|
|
351
608
|
if (file.type === 1) {
|
|
352
|
-
await client.downloadTo(
|
|
609
|
+
await client.downloadTo(path.join(tempDir.path, file.name), normalizePath(`${dir}/${file.name}`));
|
|
353
610
|
}
|
|
354
611
|
}
|
|
355
|
-
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${
|
|
612
|
+
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
|
|
356
613
|
await createZipFile(tempDir.path, zipFilePath);
|
|
357
|
-
backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${
|
|
358
|
-
`==> ${buildUrl(protocol, baseUrl, dir
|
|
614
|
+
backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk.yellow(
|
|
615
|
+
`==> ${buildUrl(protocol, baseUrl, `${dir}/${fileName}`)}`
|
|
359
616
|
)}`;
|
|
360
|
-
await client.uploadFrom(zipFilePath,
|
|
617
|
+
await client.uploadFrom(zipFilePath, normalizePath(`${dir}/${fileName}`));
|
|
361
618
|
const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
|
|
362
619
|
backupSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
|
|
363
|
-
console.log(
|
|
364
|
-
console.log(
|
|
620
|
+
console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
621
|
+
console.log(chalk.green(`${backupUrl}`));
|
|
365
622
|
console.log();
|
|
366
623
|
} catch (error) {
|
|
367
624
|
backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
|
|
@@ -369,18 +626,18 @@ async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = f
|
|
|
369
626
|
} finally {
|
|
370
627
|
tempDir.cleanup();
|
|
371
628
|
try {
|
|
372
|
-
if (
|
|
373
|
-
|
|
629
|
+
if (fs.existsSync(zipFilePath)) {
|
|
630
|
+
fs.rmSync(zipFilePath);
|
|
374
631
|
}
|
|
375
632
|
} catch (error) {
|
|
376
|
-
console.warn(
|
|
633
|
+
console.warn(chalk.yellow("\u26A0 \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
377
634
|
}
|
|
378
635
|
}
|
|
379
636
|
}
|
|
380
637
|
async function createZipFile(sourceDir, outputPath) {
|
|
381
638
|
return new Promise((resolve, reject) => {
|
|
382
|
-
const output =
|
|
383
|
-
const archive = (
|
|
639
|
+
const output = fs.createWriteStream(outputPath);
|
|
640
|
+
const archive = archiver("zip", {
|
|
384
641
|
zlib: { level: 9 }
|
|
385
642
|
});
|
|
386
643
|
output.on("close", () => {
|
|
@@ -395,8 +652,8 @@ async function createZipFile(sourceDir, outputPath) {
|
|
|
395
652
|
});
|
|
396
653
|
}
|
|
397
654
|
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
|
|
398
|
-
const timestamp = (
|
|
399
|
-
const backupSpinner = (
|
|
655
|
+
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
|
656
|
+
const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
400
657
|
const tempDir = createTempDir("single-backup");
|
|
401
658
|
let backupProgressSpinner;
|
|
402
659
|
try {
|
|
@@ -411,13 +668,13 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
411
668
|
}
|
|
412
669
|
backupSpinner.stop();
|
|
413
670
|
if (showBackFile) {
|
|
414
|
-
console.log(
|
|
671
|
+
console.log(chalk.cyan(`
|
|
415
672
|
\u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
|
|
416
673
|
backupTasks.forEach((task) => {
|
|
417
|
-
console.log(
|
|
674
|
+
console.log(chalk.gray(` - ${task.fileName}`));
|
|
418
675
|
});
|
|
419
676
|
}
|
|
420
|
-
backupProgressSpinner = (
|
|
677
|
+
backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
|
|
421
678
|
const concurrencyLimit = 3;
|
|
422
679
|
let backedUpCount = 0;
|
|
423
680
|
const backedUpFiles = [];
|
|
@@ -425,18 +682,18 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
425
682
|
const batch = backupTasks.slice(i, i + concurrencyLimit);
|
|
426
683
|
const promises = batch.map(async ({ fileName }) => {
|
|
427
684
|
try {
|
|
428
|
-
const localTempPath =
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
|
|
685
|
+
const localTempPath = path.join(tempDir.path, fileName);
|
|
686
|
+
const extIndex = fileName.lastIndexOf(".");
|
|
687
|
+
const name = extIndex > -1 ? fileName.slice(0, extIndex) : fileName;
|
|
688
|
+
const ext = extIndex > -1 ? fileName.slice(extIndex) : "";
|
|
689
|
+
const backupFileName = `${name}.${timestamp}${ext}`;
|
|
690
|
+
const backupRemotePath = normalizePath(`${dir}/${backupFileName}`);
|
|
691
|
+
await client.downloadTo(localTempPath, normalizePath(`${dir}/${fileName}`));
|
|
434
692
|
await client.uploadFrom(localTempPath, backupRemotePath);
|
|
435
|
-
|
|
436
|
-
backedUpFiles.push(backupUrl);
|
|
693
|
+
backedUpFiles.push(buildUrl(protocol, baseUrl, backupRemotePath));
|
|
437
694
|
return true;
|
|
438
695
|
} catch (error) {
|
|
439
|
-
console.warn(
|
|
696
|
+
console.warn(chalk.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
440
697
|
return false;
|
|
441
698
|
}
|
|
442
699
|
});
|
|
@@ -445,9 +702,9 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
445
702
|
}
|
|
446
703
|
if (backedUpCount > 0) {
|
|
447
704
|
backupProgressSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
|
|
448
|
-
console.log(
|
|
705
|
+
console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
|
|
449
706
|
backedUpFiles.forEach((url) => {
|
|
450
|
-
console.log(
|
|
707
|
+
console.log(chalk.green(`\u{1F517} ${url}`));
|
|
451
708
|
});
|
|
452
709
|
console.log();
|
|
453
710
|
} else {
|
|
@@ -464,3 +721,6 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
|
|
|
464
721
|
tempDir.cleanup();
|
|
465
722
|
}
|
|
466
723
|
}
|
|
724
|
+
export {
|
|
725
|
+
vitePluginDeployFtp as default
|
|
726
|
+
};
|