vite-plugin-deploy-ftp 3.1.1 → 3.3.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/.prettierignore +5 -0
- package/.prettierrc.json +12 -0
- package/dist/index.d.ts +46 -5
- package/dist/index.js +766 -313
- package/package.json +20 -11
- package/pnpm-workspace.yaml +2 -0
package/dist/index.js
CHANGED
|
@@ -1,15 +1,243 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { checkbox, select } from "@inquirer/prompts";
|
|
3
3
|
import { Client, FileType } from "basic-ftp";
|
|
4
|
-
import
|
|
4
|
+
import chalk5 from "chalk";
|
|
5
|
+
import cliProgress from "cli-progress";
|
|
5
6
|
import dayjs from "dayjs";
|
|
6
|
-
import
|
|
7
|
+
import fs2 from "fs";
|
|
7
8
|
import { stat } from "fs/promises";
|
|
9
|
+
import path2 from "path";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import { normalizePath as normalizePath2 } from "vite";
|
|
12
|
+
|
|
13
|
+
// src/utils/file.ts
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import fs from "fs";
|
|
8
16
|
import os from "os";
|
|
9
17
|
import path from "path";
|
|
10
|
-
import ora from "ora";
|
|
11
18
|
import { normalizePath } from "vite";
|
|
12
19
|
import yazl from "yazl";
|
|
20
|
+
function getAllFiles(dirPath, arrayOfFiles = [], relativePath = "") {
|
|
21
|
+
const files = fs.readdirSync(dirPath);
|
|
22
|
+
files.forEach((file) => {
|
|
23
|
+
const fullPath = path.join(dirPath, file);
|
|
24
|
+
const relPath = path.join(relativePath, file);
|
|
25
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
26
|
+
getAllFiles(fullPath, arrayOfFiles, relPath);
|
|
27
|
+
} else {
|
|
28
|
+
arrayOfFiles.push(normalizePath(relPath));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return arrayOfFiles;
|
|
32
|
+
}
|
|
33
|
+
function createTempDir(basePath) {
|
|
34
|
+
const tempBaseDir = os.tmpdir();
|
|
35
|
+
const tempParentDir = path.join(tempBaseDir, "vite-plugin-deploy-ftp");
|
|
36
|
+
const safeBasePath = basePath.replace(/[\\/]+/g, "-").replace(/[^a-zA-Z0-9._-]/g, "-") || "temp";
|
|
37
|
+
if (!fs.existsSync(tempParentDir)) {
|
|
38
|
+
fs.mkdirSync(tempParentDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
const tempPath = fs.mkdtempSync(path.join(tempParentDir, `${safeBasePath}-`));
|
|
41
|
+
return {
|
|
42
|
+
path: tempPath,
|
|
43
|
+
cleanup: () => {
|
|
44
|
+
try {
|
|
45
|
+
if (fs.existsSync(tempPath)) {
|
|
46
|
+
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn(chalk.yellow(`\u26A0 \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function createZipFile(sourceDir, outputPath) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const output = fs.createWriteStream(outputPath);
|
|
57
|
+
const zipFile = new yazl.ZipFile();
|
|
58
|
+
const handleError = (error) => {
|
|
59
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
60
|
+
};
|
|
61
|
+
output.on("close", resolve);
|
|
62
|
+
output.on("error", handleError);
|
|
63
|
+
zipFile.outputStream.on("error", handleError);
|
|
64
|
+
zipFile.outputStream.pipe(output);
|
|
65
|
+
for (const relativePath of getAllFiles(sourceDir)) {
|
|
66
|
+
const filePath = path.join(sourceDir, relativePath);
|
|
67
|
+
zipFile.addFile(filePath, normalizePath(relativePath));
|
|
68
|
+
}
|
|
69
|
+
zipFile.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/utils/ftp.ts
|
|
74
|
+
import chalk2 from "chalk";
|
|
75
|
+
var sleep = async (ms) => {
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
|
+
};
|
|
78
|
+
function validateFtpConfig(config) {
|
|
79
|
+
return !!(config.host && config.user && config.password);
|
|
80
|
+
}
|
|
81
|
+
async function connectWithRetry(client, config, maxRetries, retryDelay, silentLogs = false) {
|
|
82
|
+
let lastError;
|
|
83
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
client.ftp.verbose = false;
|
|
86
|
+
await client.access({
|
|
87
|
+
...config,
|
|
88
|
+
secure: true,
|
|
89
|
+
secureOptions: { rejectUnauthorized: false, timeout: 6e4 }
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
94
|
+
if (attempt < maxRetries) {
|
|
95
|
+
if (!silentLogs) {
|
|
96
|
+
console.log(chalk2.yellow(`\u26A0 \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
97
|
+
}
|
|
98
|
+
await sleep(retryDelay * attempt);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/utils/output.ts
|
|
106
|
+
import chalk3 from "chalk";
|
|
107
|
+
import cliTruncate from "cli-truncate";
|
|
108
|
+
import logSymbols from "log-symbols";
|
|
109
|
+
import stringWidth from "string-width";
|
|
110
|
+
var panelBorderColor = {
|
|
111
|
+
info: "cyan",
|
|
112
|
+
success: "green",
|
|
113
|
+
warning: "yellow",
|
|
114
|
+
danger: "red"
|
|
115
|
+
};
|
|
116
|
+
var getTerminalWidth = () => process.stdout?.columns || 100;
|
|
117
|
+
var getPanelInnerWidth = () => Math.max(46, Math.min(84, getTerminalWidth() - 4));
|
|
118
|
+
var padVisual = (text, width) => `${text}${" ".repeat(Math.max(0, width - stringWidth(text)))}`;
|
|
119
|
+
var fitVisual = (text, width) => {
|
|
120
|
+
if (width <= 0) return "";
|
|
121
|
+
return padVisual(cliTruncate(text, width, { position: "middle" }), width);
|
|
122
|
+
};
|
|
123
|
+
var truncateTerminalText = (text, reservedWidth = 26) => {
|
|
124
|
+
const maxWidth = Math.max(24, Math.min(88, getTerminalWidth() - reservedWidth));
|
|
125
|
+
return cliTruncate(text, maxWidth, { position: "middle" });
|
|
126
|
+
};
|
|
127
|
+
var renderPanel = (title, rows, tone = "info", footer) => {
|
|
128
|
+
const color = chalk3[panelBorderColor[tone]];
|
|
129
|
+
const innerWidth = getPanelInnerWidth();
|
|
130
|
+
const labelWidth = rows.length > 0 ? Math.max(...rows.map((row) => stringWidth(row.label))) : 0;
|
|
131
|
+
const contentLines = [chalk3.bold(cliTruncate(title, innerWidth, { position: "end" }))];
|
|
132
|
+
if (rows.length > 0) {
|
|
133
|
+
contentLines.push("");
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
const paddedLabel = padVisual(row.label, labelWidth);
|
|
136
|
+
const prefix = `${paddedLabel} `;
|
|
137
|
+
const availableValueWidth = Math.max(8, innerWidth - stringWidth(prefix));
|
|
138
|
+
contentLines.push(`${chalk3.gray(prefix)}${fitVisual(row.value, availableValueWidth)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (footer) {
|
|
142
|
+
contentLines.push("");
|
|
143
|
+
contentLines.push(chalk3.gray(cliTruncate(footer, innerWidth, { position: "middle" })));
|
|
144
|
+
}
|
|
145
|
+
const top = color(`\u256D${"\u2500".repeat(innerWidth + 2)}\u256E`);
|
|
146
|
+
const bottom = color(`\u2570${"\u2500".repeat(innerWidth + 2)}\u256F`);
|
|
147
|
+
const body = contentLines.map((line) => `${color("\u2502")} ${fitVisual(line, innerWidth)} ${color("\u2502")}`).join("\n");
|
|
148
|
+
return `${top}
|
|
149
|
+
${body}
|
|
150
|
+
${bottom}`;
|
|
151
|
+
};
|
|
152
|
+
var renderInlineStats = (items) => items.filter(Boolean).join(chalk3.gray(" \xB7 "));
|
|
153
|
+
var getPanelDot = (tone = "success") => {
|
|
154
|
+
switch (tone) {
|
|
155
|
+
case "info":
|
|
156
|
+
return chalk3.green("\u25CF");
|
|
157
|
+
case "success":
|
|
158
|
+
return chalk3.green("\u25CF");
|
|
159
|
+
case "warning":
|
|
160
|
+
return chalk3.yellow("\u25CF");
|
|
161
|
+
case "danger":
|
|
162
|
+
return chalk3.red("\u25CF");
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var getLogSymbol = (tone) => {
|
|
166
|
+
switch (tone) {
|
|
167
|
+
case "success":
|
|
168
|
+
return logSymbols.success;
|
|
169
|
+
case "warning":
|
|
170
|
+
return logSymbols.warning;
|
|
171
|
+
case "danger":
|
|
172
|
+
return logSymbols.error;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/utils/path.ts
|
|
177
|
+
var normalizeSlash = (value) => value.replace(/\\/g, "/").trim();
|
|
178
|
+
var normalizePathSegments = (...values) => values.filter((value) => Boolean(value)).flatMap((value) => normalizeSlash(value).split("/")).filter(Boolean).join("/");
|
|
179
|
+
var normalizeFtpUploadPath = (targetPath) => {
|
|
180
|
+
const normalized = normalizePathSegments(targetPath);
|
|
181
|
+
return normalized ? `/${normalized}` : "/";
|
|
182
|
+
};
|
|
183
|
+
var normalizeRemotePath = (targetDir, relativeFilePath) => {
|
|
184
|
+
const normalizedTargetDir = normalizeFtpUploadPath(targetDir);
|
|
185
|
+
const normalizedRelativePath = normalizePathSegments(relativeFilePath);
|
|
186
|
+
if (!normalizedRelativePath) return normalizedTargetDir;
|
|
187
|
+
if (normalizedTargetDir === "/") return `/${normalizedRelativePath}`;
|
|
188
|
+
return `${normalizedTargetDir}/${normalizedRelativePath}`;
|
|
189
|
+
};
|
|
190
|
+
var splitUrlLikeBase = (value) => {
|
|
191
|
+
const normalized = normalizeSlash(value);
|
|
192
|
+
const protocolMatch = normalized.match(/^([a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]+)(.*)$/);
|
|
193
|
+
if (protocolMatch) {
|
|
194
|
+
return {
|
|
195
|
+
prefix: protocolMatch[1],
|
|
196
|
+
path: protocolMatch[2] || ""
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const protocolRelativeMatch = normalized.match(/^(\/\/[^/]+)(.*)$/);
|
|
200
|
+
if (protocolRelativeMatch) {
|
|
201
|
+
return {
|
|
202
|
+
prefix: protocolRelativeMatch[1],
|
|
203
|
+
path: protocolRelativeMatch[2] || ""
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (normalized.startsWith("/")) {
|
|
207
|
+
return {
|
|
208
|
+
prefix: "/",
|
|
209
|
+
path: normalized
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
prefix: "",
|
|
214
|
+
path: normalized
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
var normalizeUrlLikeBase = (base) => {
|
|
218
|
+
const { prefix, path: path3 } = splitUrlLikeBase(base);
|
|
219
|
+
const normalizedPath = normalizePathSegments(path3);
|
|
220
|
+
if (!prefix) return normalizedPath;
|
|
221
|
+
if (!normalizedPath) return prefix;
|
|
222
|
+
if (prefix === "/") return `/${normalizedPath}`;
|
|
223
|
+
return `${prefix}/${normalizedPath}`;
|
|
224
|
+
};
|
|
225
|
+
var normalizeSelectionPath = (value) => normalizePathSegments(value);
|
|
226
|
+
var joinUrlLikePath = (base, targetPath) => {
|
|
227
|
+
const normalizedBase = normalizeUrlLikeBase(base).replace(/\/+$/, "");
|
|
228
|
+
const normalizedTargetPath = normalizePathSegments(targetPath);
|
|
229
|
+
if (!normalizedTargetPath) return normalizedBase;
|
|
230
|
+
if (!normalizedBase) return `/${normalizedTargetPath}`;
|
|
231
|
+
return `${normalizedBase}/${normalizedTargetPath}`;
|
|
232
|
+
};
|
|
233
|
+
var resolveDisplayUrl = (alias, targetPath) => {
|
|
234
|
+
const normalizedTargetPath = normalizeFtpUploadPath(targetPath);
|
|
235
|
+
if (!alias) return normalizedTargetPath;
|
|
236
|
+
return joinUrlLikePath(alias, normalizedTargetPath);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/utils/progress.ts
|
|
240
|
+
import chalk4 from "chalk";
|
|
13
241
|
var formatBytes = (bytes) => {
|
|
14
242
|
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
15
243
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
@@ -30,38 +258,39 @@ var formatDuration = (seconds) => {
|
|
|
30
258
|
if (mins === 0) return `${secs}s`;
|
|
31
259
|
return `${mins}m${String(secs).padStart(2, "0")}s`;
|
|
32
260
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
261
|
+
|
|
262
|
+
// src/index.ts
|
|
263
|
+
var backupArchivePattern = /^backup_\d{8}_\d{6}\.zip$/i;
|
|
264
|
+
var formatTimingDuration = (durationMs) => {
|
|
265
|
+
if (durationMs < 1e3) return `${durationMs}ms`;
|
|
266
|
+
const seconds = durationMs / 1e3;
|
|
267
|
+
return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`;
|
|
39
268
|
};
|
|
40
|
-
var
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
269
|
+
var renderBackupPanel = (summary) => {
|
|
270
|
+
const previewItems = summary.items.slice(0, 2);
|
|
271
|
+
const rows = [
|
|
272
|
+
{ label: "\u7ED3\u679C:", value: chalk5.green(`${summary.items.length} \u4E2A\u5907\u4EFD\u6587\u4EF6`) },
|
|
273
|
+
...previewItems.map((item, index) => ({
|
|
274
|
+
label: `\u6587\u4EF6 ${index + 1}:`,
|
|
275
|
+
value: chalk5.cyan(truncateTerminalText(item, 22))
|
|
276
|
+
}))
|
|
277
|
+
];
|
|
278
|
+
if (summary.items.length > previewItems.length) {
|
|
279
|
+
rows.push({
|
|
280
|
+
label: "\u5176\u4F59:",
|
|
281
|
+
value: chalk5.gray(`\u8FD8\u6709 ${summary.items.length - previewItems.length} \u4E2A\u5907\u4EFD\u9879\u672A\u5C55\u5F00`)
|
|
282
|
+
});
|
|
45
283
|
}
|
|
46
|
-
|
|
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 normalizeUploadPath = (targetPath) => {
|
|
58
|
-
const normalized = normalizePath(targetPath).replace(/\/{2,}/g, "/").trim();
|
|
59
|
-
if (!normalized || normalized === "." || normalized === "/") return "/";
|
|
60
|
-
const withoutTrailingSlash = normalized.replace(/\/+$/, "");
|
|
61
|
-
return withoutTrailingSlash.startsWith("/") ? withoutTrailingSlash : `/${withoutTrailingSlash}`;
|
|
284
|
+
return renderPanel(`${getPanelDot("success")} ${summary.title}`, rows, "success");
|
|
62
285
|
};
|
|
63
|
-
var
|
|
64
|
-
|
|
286
|
+
var renderDebugPanel = (entries) => {
|
|
287
|
+
const rows = entries.map((entry) => ({
|
|
288
|
+
label: `${entry.label}:`,
|
|
289
|
+
value: chalk5.cyan(
|
|
290
|
+
entry.detail ? `${formatTimingDuration(entry.durationMs)} \xB7 ${truncateTerminalText(entry.detail, 24)}` : formatTimingDuration(entry.durationMs)
|
|
291
|
+
)
|
|
292
|
+
}));
|
|
293
|
+
return renderPanel(`${getPanelDot("success")} \u8C03\u8BD5\u8017\u65F6`, rows, "info");
|
|
65
294
|
};
|
|
66
295
|
function vitePluginDeployFtp(option) {
|
|
67
296
|
const safeOption = option || {};
|
|
@@ -71,6 +300,7 @@ function vitePluginDeployFtp(option) {
|
|
|
71
300
|
singleBack = false,
|
|
72
301
|
singleBackFiles = ["index.html"],
|
|
73
302
|
showBackFile = false,
|
|
303
|
+
debug = false,
|
|
74
304
|
maxRetries = 3,
|
|
75
305
|
retryDelay = 1e3,
|
|
76
306
|
autoUpload = false,
|
|
@@ -81,8 +311,8 @@ function vitePluginDeployFtp(option) {
|
|
|
81
311
|
const isMultiFtp = "ftps" in safeOption;
|
|
82
312
|
const ftpConfigs = isMultiFtp ? safeOption.ftps || [] : [{ ...safeOption, name: safeOption.name || safeOption.alias || safeOption.host }];
|
|
83
313
|
const defaultFtp = isMultiFtp ? safeOption.defaultFtp : void 0;
|
|
84
|
-
const normalizedUploadPath =
|
|
85
|
-
let outDir =
|
|
314
|
+
const normalizedUploadPath = normalizeFtpUploadPath(uploadPath);
|
|
315
|
+
let outDir = normalizePath2(path2.resolve("dist"));
|
|
86
316
|
let upload = false;
|
|
87
317
|
let buildFailed = false;
|
|
88
318
|
let resolvedConfig = null;
|
|
@@ -120,11 +350,15 @@ function vitePluginDeployFtp(option) {
|
|
|
120
350
|
for (let attempt = 1; attempt <= context.maxRetries; attempt++) {
|
|
121
351
|
try {
|
|
122
352
|
await context.ensureConnected();
|
|
123
|
-
const remoteDir =
|
|
353
|
+
const remoteDir = normalizePath2(path2.posix.dirname(task.remotePath));
|
|
124
354
|
if (remoteDir && remoteDir !== ".") {
|
|
125
|
-
await context.
|
|
355
|
+
await context.ensureRemoteDir(remoteDir);
|
|
356
|
+
}
|
|
357
|
+
const uploadStartedAt = Date.now();
|
|
358
|
+
await context.client.uploadFrom(task.filePath, path2.posix.basename(task.remotePath));
|
|
359
|
+
if (context.debugMetrics) {
|
|
360
|
+
context.debugMetrics.uploadMs += Date.now() - uploadStartedAt;
|
|
126
361
|
}
|
|
127
|
-
await context.client.uploadFrom(task.filePath, path.posix.basename(task.remotePath));
|
|
128
362
|
return {
|
|
129
363
|
success: true,
|
|
130
364
|
file: task.filePath,
|
|
@@ -141,7 +375,7 @@ function vitePluginDeployFtp(option) {
|
|
|
141
375
|
if (attempt === context.maxRetries) {
|
|
142
376
|
if (!context.silentLogs) {
|
|
143
377
|
console.log(
|
|
144
|
-
`${
|
|
378
|
+
`${chalk5.red("\u2717")} ${task.filePath} => ${error instanceof Error ? error.message : String(error)}`
|
|
145
379
|
);
|
|
146
380
|
}
|
|
147
381
|
return {
|
|
@@ -154,9 +388,7 @@ function vitePluginDeployFtp(option) {
|
|
|
154
388
|
};
|
|
155
389
|
}
|
|
156
390
|
if (!context.silentLogs) {
|
|
157
|
-
console.log(
|
|
158
|
-
`${chalk.yellow("\u26A0")} ${task.filePath} \u4E0A\u4F20\u5931\u8D25\uFF0C\u6B63\u5728\u91CD\u8BD5 (${attempt}/${context.maxRetries})...`
|
|
159
|
-
);
|
|
391
|
+
console.log(`${chalk5.yellow("\u26A0")} ${task.filePath} \u4E0A\u4F20\u5931\u8D25\uFF0C\u6B63\u5728\u91CD\u8BD5 (${attempt}/${context.maxRetries})...`);
|
|
160
392
|
}
|
|
161
393
|
await sleep(context.retryDelay * attempt);
|
|
162
394
|
}
|
|
@@ -170,17 +402,21 @@ function vitePluginDeployFtp(option) {
|
|
|
170
402
|
error: new Error("Max retries exceeded")
|
|
171
403
|
};
|
|
172
404
|
};
|
|
173
|
-
const uploadFilesInBatches = async (
|
|
405
|
+
const uploadFilesInBatches = async (context) => {
|
|
406
|
+
const { connectConfig, files, targetDir, windowSize = concurrency, reusableClient } = context;
|
|
174
407
|
const results = [];
|
|
408
|
+
const debugEntries = [];
|
|
175
409
|
const totalFiles = files.length;
|
|
176
410
|
const tasks = [];
|
|
411
|
+
const taskGroups = [];
|
|
177
412
|
let completed = 0;
|
|
178
413
|
let failed = 0;
|
|
179
414
|
let uploadedBytes = 0;
|
|
180
415
|
let retries = 0;
|
|
416
|
+
const taskPrepareStartedAt = Date.now();
|
|
181
417
|
const taskCandidates = await Promise.all(
|
|
182
418
|
files.map(async (relativeFilePath) => {
|
|
183
|
-
const filePath =
|
|
419
|
+
const filePath = normalizePath2(path2.resolve(outDir, relativeFilePath));
|
|
184
420
|
const remotePath = normalizeRemotePath(targetDir, relativeFilePath);
|
|
185
421
|
try {
|
|
186
422
|
const fileStats = await stat(filePath);
|
|
@@ -206,68 +442,240 @@ function vitePluginDeployFtp(option) {
|
|
|
206
442
|
});
|
|
207
443
|
}
|
|
208
444
|
}
|
|
445
|
+
debugEntries.push({
|
|
446
|
+
label: "\u751F\u6210\u4E0A\u4F20\u4EFB\u52A1",
|
|
447
|
+
durationMs: Date.now() - taskPrepareStartedAt,
|
|
448
|
+
detail: `${tasks.length} \u4E2A\u6587\u4EF6`
|
|
449
|
+
});
|
|
450
|
+
const normalizedTargetDir = normalizeFtpUploadPath(targetDir);
|
|
451
|
+
const groupStartedAt = Date.now();
|
|
452
|
+
const groupsByRelativeDir = /* @__PURE__ */ new Map();
|
|
453
|
+
for (const task of tasks) {
|
|
454
|
+
const remoteDir = normalizePath2(path2.posix.dirname(task.remotePath));
|
|
455
|
+
const normalizedRemoteDir = remoteDir && remoteDir !== "." ? remoteDir : normalizedTargetDir;
|
|
456
|
+
const relativeDir = normalizedRemoteDir === normalizedTargetDir ? "" : normalizedRemoteDir.slice(normalizedTargetDir.length).replace(/^\/+/, "");
|
|
457
|
+
const currentTasks = groupsByRelativeDir.get(relativeDir);
|
|
458
|
+
if (currentTasks) {
|
|
459
|
+
currentTasks.push(task);
|
|
460
|
+
} else {
|
|
461
|
+
groupsByRelativeDir.set(relativeDir, [task]);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const [relativeDir, groupedTasks] of groupsByRelativeDir) {
|
|
465
|
+
const remoteDir = relativeDir ? normalizeRemotePath(normalizedTargetDir, relativeDir) : normalizedTargetDir;
|
|
466
|
+
taskGroups.push({ relativeDir, remoteDir, tasks: groupedTasks });
|
|
467
|
+
}
|
|
468
|
+
taskGroups.sort((left, right) => left.remoteDir.localeCompare(right.remoteDir));
|
|
469
|
+
debugEntries.push({
|
|
470
|
+
label: "\u76EE\u5F55\u5206\u7EC4",
|
|
471
|
+
durationMs: Date.now() - groupStartedAt,
|
|
472
|
+
detail: `${taskGroups.length} \u7EC4`
|
|
473
|
+
});
|
|
209
474
|
const totalBytes = tasks.reduce((sum, task) => sum + task.size, 0);
|
|
210
475
|
const startAt = Date.now();
|
|
211
|
-
const safeWindowSize = Math.max(1, Math.min(windowSize,
|
|
212
|
-
const
|
|
476
|
+
const safeWindowSize = Math.max(1, Math.min(windowSize, taskGroups.length || 1));
|
|
477
|
+
const extraWorkerCount = reusableClient ? Math.max(0, safeWindowSize - 1) : safeWindowSize;
|
|
213
478
|
const silentLogs = Boolean(useInteractiveOutput);
|
|
214
|
-
const
|
|
215
|
-
|
|
479
|
+
const progressBar = useInteractiveOutput ? new cliProgress.SingleBar({
|
|
480
|
+
hideCursor: true,
|
|
481
|
+
clearOnComplete: true,
|
|
482
|
+
stopOnComplete: true,
|
|
483
|
+
barsize: 18,
|
|
484
|
+
barCompleteChar: "\u2588",
|
|
485
|
+
barIncompleteChar: "\u2591",
|
|
486
|
+
format: `${chalk5.gray("\u4E0A\u4F20")} ${chalk5.bold("{percentage}%")} ${chalk5.cyan("{bar}")} ${chalk5.gray("\xB7")} ${chalk5.magenta("{speed}/s")} ${chalk5.gray("\xB7")} ${chalk5.gray("{elapsed}")}s`
|
|
487
|
+
}) : null;
|
|
488
|
+
const reportEvery = Math.max(1, Math.ceil(totalFiles / 6));
|
|
216
489
|
let lastReportedCompleted = -1;
|
|
490
|
+
const debugMetrics = {
|
|
491
|
+
connectMs: 0,
|
|
492
|
+
rootDirMs: 0,
|
|
493
|
+
switchDirMs: 0,
|
|
494
|
+
uploadMs: 0
|
|
495
|
+
};
|
|
496
|
+
if (progressBar) {
|
|
497
|
+
progressBar.start(totalFiles, 0, {
|
|
498
|
+
speed: formatBytes(0),
|
|
499
|
+
elapsed: "0"
|
|
500
|
+
});
|
|
501
|
+
}
|
|
217
502
|
const updateProgress = () => {
|
|
218
|
-
const progressRatio = totalFiles > 0 ? completed / totalFiles : 1;
|
|
219
|
-
const percentage = Math.round(progressRatio * 100);
|
|
220
503
|
const elapsedSeconds = (Date.now() - startAt) / 1e3;
|
|
221
504
|
const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
505
|
+
if (!progressBar) {
|
|
506
|
+
const progressRatio = totalFiles > 0 ? completed / totalFiles : 1;
|
|
507
|
+
const percentage = Math.round(progressRatio * 100);
|
|
508
|
+
if (completed === 0 && totalFiles > 0) return;
|
|
226
509
|
if (completed === lastReportedCompleted) return;
|
|
227
510
|
if (completed === totalFiles || completed % reportEvery === 0) {
|
|
228
511
|
console.log(
|
|
229
|
-
`${
|
|
512
|
+
`${chalk5.gray("\u4E0A\u4F20\u8FDB\u5EA6")} ${renderInlineStats([
|
|
513
|
+
chalk5.bold(`${completed}/${totalFiles}`),
|
|
514
|
+
`${percentage}%`,
|
|
515
|
+
`${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)}`,
|
|
516
|
+
`${formatBytes(speed)}/s`
|
|
517
|
+
])}`
|
|
230
518
|
);
|
|
231
519
|
lastReportedCompleted = completed;
|
|
232
520
|
}
|
|
233
521
|
return;
|
|
234
522
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`${chalk.cyan("\u6B63\u5728\u4E0A\u4F20:")} ${chalk.white(currentFile)}`,
|
|
240
|
-
`${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))}`
|
|
241
|
-
].join("\n");
|
|
242
|
-
spinner.text += warnLine;
|
|
523
|
+
progressBar.update(completed, {
|
|
524
|
+
speed: chalk5.magenta(formatBytes(speed)),
|
|
525
|
+
elapsed: formatDuration(elapsedSeconds).replace(/s$/, "")
|
|
526
|
+
});
|
|
243
527
|
};
|
|
244
|
-
const refreshTimer =
|
|
245
|
-
let
|
|
528
|
+
const refreshTimer = progressBar ? setInterval(updateProgress, 120) : null;
|
|
529
|
+
let currentGroupIndex = 0;
|
|
246
530
|
const worker = async () => {
|
|
247
531
|
const client = new Client();
|
|
248
532
|
let connected = false;
|
|
533
|
+
let currentRelativeDir = "";
|
|
534
|
+
let rooted = false;
|
|
535
|
+
const ensuredRelativeDirs = /* @__PURE__ */ new Set();
|
|
249
536
|
const ensureConnected = async () => {
|
|
250
537
|
if (connected) return;
|
|
538
|
+
const connectStartedAt = Date.now();
|
|
251
539
|
await connectWithRetry(client, connectConfig, maxRetries, retryDelay, true);
|
|
252
540
|
connected = true;
|
|
541
|
+
rooted = false;
|
|
542
|
+
currentRelativeDir = "";
|
|
543
|
+
debugMetrics.connectMs += Date.now() - connectStartedAt;
|
|
544
|
+
};
|
|
545
|
+
const ensureRootDir = async () => {
|
|
546
|
+
if (rooted) return;
|
|
547
|
+
const rootStartedAt = Date.now();
|
|
548
|
+
await client.ensureDir(normalizedTargetDir);
|
|
549
|
+
rooted = true;
|
|
550
|
+
currentRelativeDir = "";
|
|
551
|
+
debugMetrics.rootDirMs += Date.now() - rootStartedAt;
|
|
552
|
+
};
|
|
553
|
+
const ensureRemoteDir = async (remoteDir) => {
|
|
554
|
+
await ensureRootDir();
|
|
555
|
+
const relativeDir = remoteDir === normalizedTargetDir ? "" : remoteDir.slice(normalizedTargetDir.length).replace(/^\/+/, "");
|
|
556
|
+
if (currentRelativeDir === relativeDir) return;
|
|
557
|
+
if (!relativeDir) {
|
|
558
|
+
const switchStartedAt2 = Date.now();
|
|
559
|
+
await client.cd(normalizedTargetDir);
|
|
560
|
+
currentRelativeDir = "";
|
|
561
|
+
debugMetrics.switchDirMs += Date.now() - switchStartedAt2;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const switchStartedAt = Date.now();
|
|
565
|
+
await client.cd(normalizedTargetDir);
|
|
566
|
+
if (!ensuredRelativeDirs.has(relativeDir)) {
|
|
567
|
+
await client.ensureDir(relativeDir);
|
|
568
|
+
ensuredRelativeDirs.add(relativeDir);
|
|
569
|
+
} else {
|
|
570
|
+
await client.cd(relativeDir);
|
|
571
|
+
}
|
|
572
|
+
currentRelativeDir = relativeDir;
|
|
573
|
+
debugMetrics.switchDirMs += Date.now() - switchStartedAt;
|
|
253
574
|
};
|
|
254
575
|
const markDisconnected = () => {
|
|
255
576
|
connected = false;
|
|
577
|
+
rooted = false;
|
|
578
|
+
currentRelativeDir = "";
|
|
579
|
+
ensuredRelativeDirs.clear();
|
|
256
580
|
};
|
|
257
581
|
try {
|
|
258
582
|
while (true) {
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
const
|
|
262
|
-
|
|
583
|
+
const groupIndex = currentGroupIndex++;
|
|
584
|
+
if (groupIndex >= taskGroups.length) return;
|
|
585
|
+
const taskGroup = taskGroups[groupIndex];
|
|
586
|
+
for (const task of taskGroup.tasks) {
|
|
587
|
+
updateProgress();
|
|
588
|
+
const result = await uploadFileWithRetry(task, {
|
|
589
|
+
client,
|
|
590
|
+
ensureConnected,
|
|
591
|
+
ensureRemoteDir,
|
|
592
|
+
markDisconnected,
|
|
593
|
+
silentLogs,
|
|
594
|
+
maxRetries,
|
|
595
|
+
retryDelay,
|
|
596
|
+
debugMetrics
|
|
597
|
+
});
|
|
598
|
+
completed++;
|
|
599
|
+
retries += result.retries;
|
|
600
|
+
if (result.success) {
|
|
601
|
+
uploadedBytes += result.size;
|
|
602
|
+
} else {
|
|
603
|
+
failed++;
|
|
604
|
+
}
|
|
605
|
+
results.push(result);
|
|
606
|
+
updateProgress();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} finally {
|
|
610
|
+
client.close();
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
const runReusableWorker = async (seed) => {
|
|
614
|
+
const client = seed.client;
|
|
615
|
+
let connected = true;
|
|
616
|
+
let currentRelativeDir = "";
|
|
617
|
+
let rooted = false;
|
|
618
|
+
const ensuredRelativeDirs = /* @__PURE__ */ new Set();
|
|
619
|
+
const ensureConnected = async () => {
|
|
620
|
+
if (connected) return;
|
|
621
|
+
const connectStartedAt = Date.now();
|
|
622
|
+
await connectWithRetry(client, connectConfig, maxRetries, retryDelay, true);
|
|
623
|
+
connected = true;
|
|
624
|
+
rooted = false;
|
|
625
|
+
currentRelativeDir = "";
|
|
626
|
+
debugMetrics.connectMs += Date.now() - connectStartedAt;
|
|
627
|
+
};
|
|
628
|
+
const ensureRootDir = async () => {
|
|
629
|
+
if (rooted) return;
|
|
630
|
+
const rootStartedAt = Date.now();
|
|
631
|
+
await client.ensureDir(normalizedTargetDir);
|
|
632
|
+
rooted = true;
|
|
633
|
+
currentRelativeDir = "";
|
|
634
|
+
debugMetrics.rootDirMs += Date.now() - rootStartedAt;
|
|
635
|
+
};
|
|
636
|
+
const ensureRemoteDir = async (remoteDir) => {
|
|
637
|
+
await ensureRootDir();
|
|
638
|
+
const relativeDir = remoteDir === normalizedTargetDir ? "" : remoteDir.slice(normalizedTargetDir.length).replace(/^\/+/, "");
|
|
639
|
+
if (currentRelativeDir === relativeDir) return;
|
|
640
|
+
if (!relativeDir) {
|
|
641
|
+
const switchStartedAt2 = Date.now();
|
|
642
|
+
await client.cd(normalizedTargetDir);
|
|
643
|
+
currentRelativeDir = "";
|
|
644
|
+
debugMetrics.switchDirMs += Date.now() - switchStartedAt2;
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const switchStartedAt = Date.now();
|
|
648
|
+
await client.cd(normalizedTargetDir);
|
|
649
|
+
if (!ensuredRelativeDirs.has(relativeDir)) {
|
|
650
|
+
await client.ensureDir(relativeDir);
|
|
651
|
+
ensuredRelativeDirs.add(relativeDir);
|
|
652
|
+
} else {
|
|
653
|
+
await client.cd(relativeDir);
|
|
654
|
+
}
|
|
655
|
+
currentRelativeDir = relativeDir;
|
|
656
|
+
debugMetrics.switchDirMs += Date.now() - switchStartedAt;
|
|
657
|
+
};
|
|
658
|
+
const markDisconnected = () => {
|
|
659
|
+
connected = false;
|
|
660
|
+
rooted = false;
|
|
661
|
+
currentRelativeDir = "";
|
|
662
|
+
ensuredRelativeDirs.clear();
|
|
663
|
+
};
|
|
664
|
+
while (true) {
|
|
665
|
+
const groupIndex = currentGroupIndex++;
|
|
666
|
+
if (groupIndex >= taskGroups.length) return;
|
|
667
|
+
const taskGroup = taskGroups[groupIndex];
|
|
668
|
+
for (const task of taskGroup.tasks) {
|
|
263
669
|
updateProgress();
|
|
264
670
|
const result = await uploadFileWithRetry(task, {
|
|
265
671
|
client,
|
|
266
672
|
ensureConnected,
|
|
673
|
+
ensureRemoteDir,
|
|
267
674
|
markDisconnected,
|
|
268
675
|
silentLogs,
|
|
269
676
|
maxRetries,
|
|
270
|
-
retryDelay
|
|
677
|
+
retryDelay,
|
|
678
|
+
debugMetrics
|
|
271
679
|
});
|
|
272
680
|
completed++;
|
|
273
681
|
retries += result.retries;
|
|
@@ -277,79 +685,151 @@ ${chalk.yellow("\u91CD\u8BD5")}: ${retries} ${chalk.yellow("\u5931\u8D25")}: ${
|
|
|
277
685
|
failed++;
|
|
278
686
|
}
|
|
279
687
|
results.push(result);
|
|
280
|
-
activeFiles.delete(task.remotePath);
|
|
281
688
|
updateProgress();
|
|
282
689
|
}
|
|
283
|
-
} finally {
|
|
284
|
-
client.close();
|
|
285
690
|
}
|
|
286
691
|
};
|
|
287
692
|
updateProgress();
|
|
288
693
|
try {
|
|
289
|
-
|
|
694
|
+
const workers = Array.from({ length: extraWorkerCount }, () => worker());
|
|
695
|
+
if (reusableClient) {
|
|
696
|
+
workers.unshift(runReusableWorker(reusableClient));
|
|
697
|
+
}
|
|
698
|
+
await Promise.all(workers);
|
|
290
699
|
} finally {
|
|
291
700
|
if (refreshTimer) clearInterval(refreshTimer);
|
|
292
701
|
}
|
|
293
|
-
if (
|
|
702
|
+
if (progressBar) {
|
|
294
703
|
const elapsedSeconds = (Date.now() - startAt) / 1e3;
|
|
295
|
-
const successCount = results.filter((item) => item.success).length;
|
|
296
704
|
const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
705
|
+
progressBar.update(totalFiles, {
|
|
706
|
+
speed: chalk5.magenta(formatBytes(speed)),
|
|
707
|
+
elapsed: formatDuration(elapsedSeconds).replace(/s$/, "")
|
|
708
|
+
});
|
|
709
|
+
progressBar.stop();
|
|
710
|
+
} else if (failed > 0) {
|
|
711
|
+
console.log(`${getLogSymbol("warning")} \u6587\u4EF6\u4E0A\u4F20\u7ED3\u675F\uFF0C\u6210\u529F ${completed - failed}/${totalFiles}\uFF0C\u5931\u8D25 ${failed}`);
|
|
301
712
|
} else {
|
|
302
|
-
console.log(`${
|
|
713
|
+
console.log(`${getLogSymbol("success")} \u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
|
|
303
714
|
}
|
|
304
|
-
|
|
715
|
+
debugEntries.push(
|
|
716
|
+
{
|
|
717
|
+
label: "\u8FDE\u63A5\u670D\u52A1\u5668",
|
|
718
|
+
durationMs: debugMetrics.connectMs
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
label: "\u51C6\u5907\u6839\u76EE\u5F55",
|
|
722
|
+
durationMs: debugMetrics.rootDirMs
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
label: "\u5207\u6362\u5B50\u76EE\u5F55",
|
|
726
|
+
durationMs: debugMetrics.switchDirMs
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
label: "\u6587\u4EF6\u4F20\u8F93",
|
|
730
|
+
durationMs: debugMetrics.uploadMs,
|
|
731
|
+
detail: `${tasks.length} \u4E2A\u6587\u4EF6`
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
label: "\u4E0A\u4F20\u9636\u6BB5",
|
|
735
|
+
durationMs: Date.now() - startAt
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
return { results, debugEntries };
|
|
305
739
|
};
|
|
306
740
|
const deploySingleTarget = async (ftpConfig) => {
|
|
307
741
|
const { host, port = 21, user, password, alias = "", name } = ftpConfig;
|
|
742
|
+
const normalizedAlias = alias ? normalizeUrlLikeBase(alias) : "";
|
|
308
743
|
if (!host || !user || !password) {
|
|
309
|
-
console.error(
|
|
310
|
-
if (!host) console.error(
|
|
311
|
-
if (!user) console.error(
|
|
312
|
-
if (!password) console.error(
|
|
744
|
+
console.error(chalk5.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
|
|
745
|
+
if (!host) console.error(chalk5.red(" - \u7F3A\u5C11 host"));
|
|
746
|
+
if (!user) console.error(chalk5.red(" - \u7F3A\u5C11 user"));
|
|
747
|
+
if (!password) console.error(chalk5.red(" - \u7F3A\u5C11 password"));
|
|
313
748
|
return { name: name || host || "unknown", totalFiles: 0, failedCount: 1 };
|
|
314
749
|
}
|
|
750
|
+
const debugEntries = [];
|
|
751
|
+
const collectFilesStartedAt = Date.now();
|
|
315
752
|
const allFiles = getAllFiles(outDir);
|
|
753
|
+
debugEntries.push({
|
|
754
|
+
label: "\u626B\u63CF\u672C\u5730\u6587\u4EF6",
|
|
755
|
+
durationMs: Date.now() - collectFilesStartedAt,
|
|
756
|
+
detail: `${allFiles.length} \u4E2A\u6587\u4EF6`
|
|
757
|
+
});
|
|
316
758
|
const totalFiles = allFiles.length;
|
|
317
|
-
const { protocol, baseUrl } = parseAlias(alias);
|
|
318
759
|
const displayName = name || host;
|
|
319
760
|
const startTime = Date.now();
|
|
320
761
|
if (allFiles.length === 0) {
|
|
321
|
-
console.log(`${
|
|
762
|
+
console.log(`${getLogSymbol("warning")} \u6CA1\u6709\u627E\u5230\u9700\u8981\u4E0A\u4F20\u7684\u6587\u4EF6`);
|
|
322
763
|
return { name: displayName, totalFiles: 0, failedCount: 0 };
|
|
323
764
|
}
|
|
324
765
|
clearScreen();
|
|
325
|
-
console.log(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
766
|
+
console.log(
|
|
767
|
+
renderPanel(
|
|
768
|
+
`${getPanelDot("success")} \u51C6\u5907\u90E8\u7F72`,
|
|
769
|
+
[
|
|
770
|
+
{
|
|
771
|
+
label: "\u4F4D\u7F6E:",
|
|
772
|
+
value: chalk5.green(`${displayName} \xB7 ${port === 21 ? host : `${host}:${port}`}`)
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
label: "\u76EE\u6807:",
|
|
776
|
+
value: chalk5.yellow(
|
|
777
|
+
truncateTerminalText(
|
|
778
|
+
normalizedAlias ? `${normalizedUploadPath} \xB7 ${normalizedAlias}` : normalizedUploadPath,
|
|
779
|
+
18
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
label: "\u6587\u4EF6:",
|
|
785
|
+
value: chalk5.blue(`${totalFiles} \u4E2A \xB7 ${truncateTerminalText(outDir, 30)}`)
|
|
786
|
+
}
|
|
787
|
+
],
|
|
788
|
+
"info"
|
|
789
|
+
)
|
|
790
|
+
);
|
|
335
791
|
const connectConfig = { host, port, user, password };
|
|
336
792
|
const preflightClient = new Client();
|
|
337
793
|
const preflightSpinner = useInteractiveOutput ? ora(`\u8FDE\u63A5\u5230 ${displayName}...`).start() : null;
|
|
338
794
|
try {
|
|
795
|
+
const preflightConnectStartedAt = Date.now();
|
|
339
796
|
await connectWithRetry(preflightClient, connectConfig, maxRetries, retryDelay, Boolean(preflightSpinner));
|
|
340
|
-
|
|
797
|
+
debugEntries.push({
|
|
798
|
+
label: "\u9884\u68C0\u8FDE\u63A5",
|
|
799
|
+
durationMs: Date.now() - preflightConnectStartedAt
|
|
800
|
+
});
|
|
801
|
+
if (preflightSpinner) preflightSpinner.stop();
|
|
802
|
+
const ensureTargetStartedAt = Date.now();
|
|
341
803
|
await preflightClient.ensureDir(normalizedUploadPath);
|
|
804
|
+
debugEntries.push({
|
|
805
|
+
label: "\u786E\u8BA4\u76EE\u6807\u76EE\u5F55",
|
|
806
|
+
durationMs: Date.now() - ensureTargetStartedAt,
|
|
807
|
+
detail: normalizedUploadPath
|
|
808
|
+
});
|
|
809
|
+
const listRemoteStartedAt = Date.now();
|
|
342
810
|
const fileList = await preflightClient.list();
|
|
811
|
+
debugEntries.push({
|
|
812
|
+
label: "\u8BFB\u53D6\u8FDC\u7AEF\u6587\u4EF6",
|
|
813
|
+
durationMs: Date.now() - listRemoteStartedAt,
|
|
814
|
+
detail: `${fileList.length} \u4E2A`
|
|
815
|
+
});
|
|
816
|
+
let backupSummary = null;
|
|
343
817
|
if (fileList.length) {
|
|
344
818
|
if (singleBack) {
|
|
345
|
-
|
|
819
|
+
const backupStartedAt = Date.now();
|
|
820
|
+
backupSummary = await createSingleBackup(
|
|
346
821
|
preflightClient,
|
|
347
822
|
normalizedUploadPath,
|
|
348
|
-
|
|
349
|
-
baseUrl,
|
|
823
|
+
normalizedAlias,
|
|
350
824
|
singleBackFiles,
|
|
351
|
-
showBackFile
|
|
825
|
+
showBackFile,
|
|
826
|
+
useInteractiveOutput
|
|
352
827
|
);
|
|
828
|
+
debugEntries.push({
|
|
829
|
+
label: "\u6267\u884C\u5907\u4EFD",
|
|
830
|
+
durationMs: Date.now() - backupStartedAt,
|
|
831
|
+
detail: backupSummary ? `${backupSummary.items.length} \u4E2A\u5907\u4EFD\u6587\u4EF6` : "\u672A\u751F\u6210\u5907\u4EFD"
|
|
832
|
+
});
|
|
353
833
|
} else {
|
|
354
834
|
const shouldBackup = await select({
|
|
355
835
|
message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
|
|
@@ -357,72 +837,118 @@ ${buildCapsuleBar(1)} 100% (${totalFiles}/${totalFiles}) ${chalk.gray("|")} \u90
|
|
|
357
837
|
default: "\u5426"
|
|
358
838
|
});
|
|
359
839
|
if (shouldBackup === "\u662F") {
|
|
360
|
-
|
|
840
|
+
const backupStartedAt = Date.now();
|
|
841
|
+
backupSummary = await createBackupFile(
|
|
361
842
|
preflightClient,
|
|
362
843
|
normalizedUploadPath,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
844
|
+
normalizedAlias,
|
|
845
|
+
showBackFile,
|
|
846
|
+
useInteractiveOutput
|
|
366
847
|
);
|
|
848
|
+
debugEntries.push({
|
|
849
|
+
label: "\u6267\u884C\u5907\u4EFD",
|
|
850
|
+
durationMs: Date.now() - backupStartedAt,
|
|
851
|
+
detail: backupSummary ? `${backupSummary.items.length} \u4E2A\u5907\u4EFD\u6587\u4EF6` : "\u672A\u751F\u6210\u5907\u4EFD"
|
|
852
|
+
});
|
|
853
|
+
} else if (debug) {
|
|
854
|
+
debugEntries.push({
|
|
855
|
+
label: "\u6267\u884C\u5907\u4EFD",
|
|
856
|
+
durationMs: 0,
|
|
857
|
+
detail: "\u624B\u52A8\u8DF3\u8FC7"
|
|
858
|
+
});
|
|
367
859
|
}
|
|
368
860
|
}
|
|
861
|
+
} else if (debug) {
|
|
862
|
+
debugEntries.push({
|
|
863
|
+
label: "\u6267\u884C\u5907\u4EFD",
|
|
864
|
+
durationMs: 0,
|
|
865
|
+
detail: "\u8FDC\u7AEF\u4E3A\u7A7A\uFF0C\u8DF3\u8FC7"
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
if (backupSummary) {
|
|
869
|
+
console.log(renderBackupPanel(backupSummary));
|
|
369
870
|
}
|
|
370
|
-
const
|
|
871
|
+
const uploadExecution = await uploadFilesInBatches({
|
|
371
872
|
connectConfig,
|
|
372
|
-
allFiles,
|
|
373
|
-
normalizedUploadPath,
|
|
374
|
-
concurrency
|
|
375
|
-
|
|
873
|
+
files: allFiles,
|
|
874
|
+
targetDir: normalizedUploadPath,
|
|
875
|
+
windowSize: concurrency,
|
|
876
|
+
reusableClient: { client: preflightClient }
|
|
877
|
+
});
|
|
878
|
+
const { results, debugEntries: uploadDebugEntries } = uploadExecution;
|
|
879
|
+
if (debug) {
|
|
880
|
+
debugEntries.push(...uploadDebugEntries);
|
|
881
|
+
}
|
|
376
882
|
const successCount = results.filter((r) => r.success).length;
|
|
377
883
|
const failedCount = results.length - successCount;
|
|
378
884
|
const durationSeconds = (Date.now() - startTime) / 1e3;
|
|
379
|
-
const duration = durationSeconds.toFixed(2);
|
|
380
885
|
const uploadedBytes = results.reduce((sum, result) => result.success ? sum + result.size : sum, 0);
|
|
381
886
|
const retryCount = results.reduce((sum, result) => sum + result.retries, 0);
|
|
382
887
|
const avgSpeed = durationSeconds > 0 ? uploadedBytes / durationSeconds : 0;
|
|
888
|
+
const accessUrl = normalizedAlias ? resolveDisplayUrl(normalizedAlias, normalizedUploadPath) : "";
|
|
383
889
|
clearScreen();
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
890
|
+
const resultRows = [
|
|
891
|
+
{
|
|
892
|
+
label: "\u7ED3\u679C:",
|
|
893
|
+
value: failedCount === 0 ? chalk5.green(`${successCount}/${results.length} \u5168\u90E8\u6210\u529F`) : chalk5.yellow(`\u6210\u529F ${successCount} \u4E2A\uFF0C\u5931\u8D25 ${failedCount} \u4E2A`)
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
label: "\u7EDF\u8BA1:",
|
|
897
|
+
value: renderInlineStats([
|
|
898
|
+
`${retryCount} \u6B21\u91CD\u8BD5`,
|
|
899
|
+
formatBytes(uploadedBytes),
|
|
900
|
+
`${formatBytes(avgSpeed)}/s`,
|
|
901
|
+
formatDuration(durationSeconds)
|
|
902
|
+
])
|
|
903
|
+
}
|
|
904
|
+
];
|
|
905
|
+
if (accessUrl) {
|
|
906
|
+
resultRows.push({ label: "\u8BBF\u95EE:", value: chalk5.cyan(truncateTerminalText(accessUrl, 20)) });
|
|
389
907
|
}
|
|
390
|
-
console.log(`
|
|
391
|
-
${chalk.gray("\u7EDF\u8BA1:")}`);
|
|
392
|
-
console.log(` ${chalk.green("\u2714")} \u6210\u529F: ${chalk.bold(successCount)}`);
|
|
393
908
|
if (failedCount > 0) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
` ${chalk.green("\u{1F517}")} \u8BBF\u95EE\u5730\u5740: ${chalk.bold(buildUrl(protocol, baseUrl, normalizedUploadPath))}`
|
|
909
|
+
const failedItems = results.filter((result) => !result.success).slice(0, 2);
|
|
910
|
+
resultRows.push(
|
|
911
|
+
...failedItems.map((item, index) => ({
|
|
912
|
+
label: `\u5931\u8D25 ${index + 1}:`,
|
|
913
|
+
value: chalk5.red(
|
|
914
|
+
`${truncateTerminalText(item.name, 26)} \xB7 ${truncateTerminalText(item.error?.message || "unknown error", 22)}`
|
|
915
|
+
)
|
|
916
|
+
}))
|
|
403
917
|
);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
console.log(chalk.red("\u5931\u8D25\u660E\u7EC6:"));
|
|
410
|
-
for (let i = 0; i < previewCount; i++) {
|
|
411
|
-
const item = failedItems[i];
|
|
412
|
-
const reason = item.error?.message || "unknown error";
|
|
413
|
-
console.log(` ${chalk.red("\u2022")} ${item.name} => ${reason}`);
|
|
414
|
-
}
|
|
415
|
-
if (failedItems.length > previewCount) {
|
|
416
|
-
console.log(chalk.gray(` ... \u8FD8\u6709 ${failedItems.length - previewCount} \u4E2A\u5931\u8D25\u6587\u4EF6`));
|
|
918
|
+
if (failedCount > failedItems.length) {
|
|
919
|
+
resultRows.push({
|
|
920
|
+
label: "\u5176\u4F59:",
|
|
921
|
+
value: chalk5.gray(`\u8FD8\u6709 ${failedCount - failedItems.length} \u4E2A\u5931\u8D25\u9879\u672A\u5C55\u5F00`)
|
|
922
|
+
});
|
|
417
923
|
}
|
|
418
|
-
|
|
924
|
+
}
|
|
925
|
+
console.log(
|
|
926
|
+
renderPanel(
|
|
927
|
+
failedCount === 0 ? `${getPanelDot("success")} \u90E8\u7F72\u5B8C\u6210` : `${getPanelDot("warning")} \u90E8\u7F72\u5B8C\u6210`,
|
|
928
|
+
resultRows,
|
|
929
|
+
failedCount === 0 ? "success" : "warning"
|
|
930
|
+
)
|
|
931
|
+
);
|
|
932
|
+
if (debug) {
|
|
933
|
+
debugEntries.push({
|
|
934
|
+
label: "\u603B\u8017\u65F6",
|
|
935
|
+
durationMs: Date.now() - startTime
|
|
936
|
+
});
|
|
937
|
+
console.log(renderDebugPanel(debugEntries));
|
|
419
938
|
}
|
|
420
939
|
return { name: displayName, totalFiles: results.length, failedCount };
|
|
421
940
|
} catch (error) {
|
|
422
|
-
if (preflightSpinner) preflightSpinner.
|
|
941
|
+
if (preflightSpinner) preflightSpinner.stop();
|
|
423
942
|
console.log(`
|
|
424
|
-
${
|
|
943
|
+
${getLogSymbol("danger")} \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF: ${error}
|
|
425
944
|
`);
|
|
945
|
+
if (debug && debugEntries.length > 0) {
|
|
946
|
+
debugEntries.push({
|
|
947
|
+
label: "\u5931\u8D25\u524D\u8017\u65F6",
|
|
948
|
+
durationMs: Date.now() - startTime
|
|
949
|
+
});
|
|
950
|
+
console.log(renderDebugPanel(debugEntries));
|
|
951
|
+
}
|
|
426
952
|
return {
|
|
427
953
|
name: displayName,
|
|
428
954
|
totalFiles,
|
|
@@ -447,28 +973,28 @@ ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} $
|
|
|
447
973
|
if (defaultFtp) {
|
|
448
974
|
const defaultConfig = ftpConfigs.find((ftp) => ftp.name === defaultFtp);
|
|
449
975
|
if (defaultConfig && validateFtpConfig(defaultConfig)) {
|
|
450
|
-
console.log(
|
|
976
|
+
console.log(chalk5.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
|
|
451
977
|
selectedConfigs = [defaultConfig];
|
|
452
978
|
} else if (defaultConfig) {
|
|
453
|
-
console.log(
|
|
979
|
+
console.log(chalk5.yellow(`\u26A0 \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
|
|
454
980
|
}
|
|
455
981
|
}
|
|
456
982
|
if (selectedConfigs.length === 0) {
|
|
457
983
|
const validConfigs = ftpConfigs.filter(validateFtpConfig);
|
|
458
984
|
const invalidConfigs = ftpConfigs.filter((config) => !validateFtpConfig(config));
|
|
459
985
|
if (invalidConfigs.length > 0) {
|
|
460
|
-
console.log(
|
|
986
|
+
console.log(chalk5.yellow("\n\u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
|
|
461
987
|
invalidConfigs.forEach((config) => {
|
|
462
988
|
const missing = [];
|
|
463
989
|
if (!config.host) missing.push("host");
|
|
464
990
|
if (!config.user) missing.push("user");
|
|
465
991
|
if (!config.password) missing.push("password");
|
|
466
|
-
console.log(
|
|
992
|
+
console.log(chalk5.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
|
|
467
993
|
});
|
|
468
994
|
console.log();
|
|
469
995
|
}
|
|
470
996
|
if (validConfigs.length === 0) {
|
|
471
|
-
console.error(
|
|
997
|
+
console.error(chalk5.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
|
|
472
998
|
return [];
|
|
473
999
|
}
|
|
474
1000
|
selectedConfigs = await checkbox({
|
|
@@ -489,7 +1015,7 @@ ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} $
|
|
|
489
1015
|
if (!singleConfig?.host) missing.push("host");
|
|
490
1016
|
if (!singleConfig?.user) missing.push("user");
|
|
491
1017
|
if (!singleConfig?.password) missing.push("password");
|
|
492
|
-
console.error(
|
|
1018
|
+
console.error(chalk5.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
|
|
493
1019
|
return [];
|
|
494
1020
|
}
|
|
495
1021
|
}
|
|
@@ -512,16 +1038,15 @@ ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} $
|
|
|
512
1038
|
clearScreen();
|
|
513
1039
|
const validationErrors = validateOptions();
|
|
514
1040
|
if (validationErrors.length > 0) {
|
|
515
|
-
|
|
1041
|
+
throw new Error(`\u914D\u7F6E\u9519\u8BEF:
|
|
516
1042
|
${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
|
|
517
|
-
return;
|
|
518
1043
|
}
|
|
519
1044
|
upload = true;
|
|
520
1045
|
return config;
|
|
521
1046
|
},
|
|
522
1047
|
configResolved(config) {
|
|
523
1048
|
resolvedConfig = config;
|
|
524
|
-
outDir =
|
|
1049
|
+
outDir = normalizePath2(path2.resolve(config.root, config.build.outDir));
|
|
525
1050
|
},
|
|
526
1051
|
closeBundle: {
|
|
527
1052
|
sequential: true,
|
|
@@ -538,41 +1063,14 @@ ${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
|
|
|
538
1063
|
}
|
|
539
1064
|
};
|
|
540
1065
|
}
|
|
541
|
-
function getAllFiles(dirPath, arrayOfFiles = [], relativePath = "") {
|
|
542
|
-
const files = fs.readdirSync(dirPath);
|
|
543
|
-
files.forEach((file) => {
|
|
544
|
-
const fullPath = path.join(dirPath, file);
|
|
545
|
-
const relPath = path.join(relativePath, file);
|
|
546
|
-
if (fs.statSync(fullPath).isDirectory()) {
|
|
547
|
-
getAllFiles(fullPath, arrayOfFiles, relPath);
|
|
548
|
-
} else {
|
|
549
|
-
arrayOfFiles.push(normalizePath(relPath));
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
return arrayOfFiles;
|
|
553
|
-
}
|
|
554
|
-
function validateFtpConfig(config) {
|
|
555
|
-
return !!(config.host && config.user && config.password);
|
|
556
|
-
}
|
|
557
|
-
function parseAlias(alias = "") {
|
|
558
|
-
const [protocol = "", baseUrl = ""] = alias.split("://");
|
|
559
|
-
return {
|
|
560
|
-
protocol: protocol ? `${protocol}://` : "",
|
|
561
|
-
baseUrl: baseUrl || ""
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
function buildUrl(protocol, baseUrl, targetPath) {
|
|
565
|
-
return protocol + normalizePath(baseUrl + targetPath);
|
|
566
|
-
}
|
|
567
|
-
var backupArchivePattern = /^backup_\d{8}_\d{6}\.zip$/i;
|
|
568
1066
|
async function downloadRemoteFilesForBackup(client, remoteDir, localDir, downloadedFiles = []) {
|
|
569
|
-
if (!
|
|
570
|
-
|
|
1067
|
+
if (!fs2.existsSync(localDir)) {
|
|
1068
|
+
fs2.mkdirSync(localDir, { recursive: true });
|
|
571
1069
|
}
|
|
572
1070
|
const remoteEntries = await client.list(remoteDir);
|
|
573
1071
|
for (const entry of remoteEntries) {
|
|
574
|
-
const remotePath =
|
|
575
|
-
const localPath =
|
|
1072
|
+
const remotePath = normalizeRemotePath(remoteDir, entry.name);
|
|
1073
|
+
const localPath = path2.join(localDir, entry.name);
|
|
576
1074
|
if (entry.type === FileType.Directory) {
|
|
577
1075
|
await downloadRemoteFilesForBackup(client, remotePath, localPath, downloadedFiles);
|
|
578
1076
|
continue;
|
|
@@ -601,177 +1099,132 @@ async function downloadRemoteFilesForBackup(client, remoteDir, localDir, downloa
|
|
|
601
1099
|
}
|
|
602
1100
|
return downloadedFiles;
|
|
603
1101
|
}
|
|
604
|
-
async function
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
try {
|
|
608
|
-
client.ftp.verbose = false;
|
|
609
|
-
await client.access({
|
|
610
|
-
...config,
|
|
611
|
-
secure: true,
|
|
612
|
-
secureOptions: { rejectUnauthorized: false, timeout: 6e4 }
|
|
613
|
-
});
|
|
614
|
-
return;
|
|
615
|
-
} catch (error) {
|
|
616
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
617
|
-
if (attempt < maxRetries) {
|
|
618
|
-
if (!silentLogs) {
|
|
619
|
-
console.log(chalk.yellow(`\u26A0 \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
|
|
620
|
-
}
|
|
621
|
-
await sleep(retryDelay * attempt);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
|
|
626
|
-
}
|
|
627
|
-
function createTempDir(basePath) {
|
|
628
|
-
const tempBaseDir = os.tmpdir();
|
|
629
|
-
const tempPath = path.join(tempBaseDir, "vite-plugin-deploy-ftp", basePath);
|
|
630
|
-
if (!fs.existsSync(tempPath)) {
|
|
631
|
-
fs.mkdirSync(tempPath, { recursive: true });
|
|
632
|
-
}
|
|
633
|
-
return {
|
|
634
|
-
path: tempPath,
|
|
635
|
-
cleanup: () => {
|
|
636
|
-
try {
|
|
637
|
-
if (fs.existsSync(tempPath)) {
|
|
638
|
-
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
639
|
-
}
|
|
640
|
-
} catch (error) {
|
|
641
|
-
console.warn(chalk.yellow(`\u26A0 \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
|
|
647
|
-
const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
|
|
1102
|
+
async function createBackupFile(client, dir, alias, showBackFile = false, useSpinner = true) {
|
|
1103
|
+
const targetUrl = resolveDisplayUrl(alias, dir);
|
|
1104
|
+
const backupSpinner = useSpinner ? ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${targetUrl}`)}`).start() : null;
|
|
648
1105
|
const fileName = `backup_${dayjs().format("YYYYMMDD_HHmmss")}.zip`;
|
|
649
|
-
const tempDir = createTempDir("backup-
|
|
650
|
-
const
|
|
1106
|
+
const tempDir = createTempDir("backup-download");
|
|
1107
|
+
const zipTempDir = createTempDir("backup-zip");
|
|
1108
|
+
const zipFilePath = path2.join(zipTempDir.path, fileName);
|
|
651
1109
|
try {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
fs.mkdirSync(zipDir, { recursive: true });
|
|
1110
|
+
if (backupSpinner) {
|
|
1111
|
+
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${targetUrl}`)}`;
|
|
655
1112
|
}
|
|
656
|
-
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
|
|
657
1113
|
const downloadedFiles = await downloadRemoteFilesForBackup(client, dir, tempDir.path);
|
|
658
1114
|
if (downloadedFiles.length === 0) {
|
|
659
|
-
backupSpinner
|
|
660
|
-
|
|
1115
|
+
if (backupSpinner) {
|
|
1116
|
+
backupSpinner.warn("\u672A\u627E\u5230\u53EF\u5907\u4EFD\u7684\u8FDC\u7A0B\u6587\u4EF6");
|
|
1117
|
+
}
|
|
1118
|
+
return null;
|
|
661
1119
|
}
|
|
662
1120
|
if (showBackFile) {
|
|
663
|
-
console.log(
|
|
1121
|
+
console.log(chalk5.cyan(`
|
|
664
1122
|
\u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${downloadedFiles.length} \u4E2A\u6587\u4EF6:`));
|
|
665
1123
|
downloadedFiles.forEach((file) => {
|
|
666
|
-
console.log(
|
|
1124
|
+
console.log(chalk5.gray(` - ${file.remotePath} (${file.size} bytes)`));
|
|
667
1125
|
});
|
|
668
1126
|
}
|
|
669
|
-
|
|
1127
|
+
if (backupSpinner) {
|
|
1128
|
+
backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk5.yellow(`==> ${targetUrl}`)}`;
|
|
1129
|
+
}
|
|
670
1130
|
await createZipFile(tempDir.path, zipFilePath);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1131
|
+
const backupRemotePath = normalizeRemotePath(dir, fileName);
|
|
1132
|
+
if (backupSpinner) {
|
|
1133
|
+
backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk5.yellow(`==> ${resolveDisplayUrl(alias, backupRemotePath)}`)}`;
|
|
1134
|
+
}
|
|
1135
|
+
await client.uploadFrom(zipFilePath, backupRemotePath);
|
|
1136
|
+
const backupUrl = resolveDisplayUrl(alias, backupRemotePath);
|
|
1137
|
+
backupSpinner?.stop();
|
|
1138
|
+
return {
|
|
1139
|
+
title: "\u5907\u4EFD\u5B8C\u6210",
|
|
1140
|
+
items: [backupUrl]
|
|
1141
|
+
};
|
|
680
1142
|
} catch (error) {
|
|
681
|
-
backupSpinner
|
|
1143
|
+
if (backupSpinner) {
|
|
1144
|
+
backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
|
|
1145
|
+
}
|
|
682
1146
|
throw error;
|
|
683
1147
|
} finally {
|
|
684
1148
|
tempDir.cleanup();
|
|
1149
|
+
zipTempDir.cleanup();
|
|
685
1150
|
try {
|
|
686
|
-
if (
|
|
687
|
-
|
|
1151
|
+
if (fs2.existsSync(zipFilePath)) {
|
|
1152
|
+
fs2.rmSync(zipFilePath);
|
|
688
1153
|
}
|
|
689
1154
|
} catch (error) {
|
|
690
|
-
console.warn(
|
|
1155
|
+
console.warn(chalk5.yellow("\u26A0 \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
|
|
691
1156
|
}
|
|
692
1157
|
}
|
|
693
1158
|
}
|
|
694
|
-
async function
|
|
695
|
-
return new Promise((resolve, reject) => {
|
|
696
|
-
const output = fs.createWriteStream(outputPath);
|
|
697
|
-
const zipFile = new yazl.ZipFile();
|
|
698
|
-
const handleError = (error) => {
|
|
699
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
700
|
-
};
|
|
701
|
-
output.on("close", resolve);
|
|
702
|
-
output.on("error", handleError);
|
|
703
|
-
zipFile.outputStream.on("error", handleError);
|
|
704
|
-
zipFile.outputStream.pipe(output);
|
|
705
|
-
for (const relativePath of getAllFiles(sourceDir)) {
|
|
706
|
-
const filePath = path.join(sourceDir, relativePath);
|
|
707
|
-
zipFile.addFile(filePath, normalizePath(relativePath));
|
|
708
|
-
}
|
|
709
|
-
zipFile.end();
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
|
|
1159
|
+
async function createSingleBackup(client, dir, alias, singleBackFiles, showBackFile = false, useSpinner = true) {
|
|
713
1160
|
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
|
714
|
-
const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${
|
|
1161
|
+
const backupSpinner = useSpinner ? ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${resolveDisplayUrl(alias, dir)}`)}`).start() : null;
|
|
715
1162
|
const tempDir = createTempDir("single-backup");
|
|
716
1163
|
let backupProgressSpinner;
|
|
717
1164
|
try {
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}).filter((task) => task.exists);
|
|
1165
|
+
const normalizedSingleBackFiles = singleBackFiles.map((fileName) => normalizeSelectionPath(fileName)).map(
|
|
1166
|
+
(fileName) => fileName.split("/").filter((segment) => segment && segment !== ".").join("/")
|
|
1167
|
+
).filter((fileName) => !fileName.split("/").includes("..")).filter(Boolean);
|
|
1168
|
+
const backupTasks = normalizedSingleBackFiles.map((fileName) => ({ fileName }));
|
|
723
1169
|
if (backupTasks.length === 0) {
|
|
724
|
-
backupSpinner
|
|
725
|
-
|
|
1170
|
+
if (backupSpinner) {
|
|
1171
|
+
backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
726
1174
|
}
|
|
727
|
-
backupSpinner
|
|
1175
|
+
backupSpinner?.stop();
|
|
728
1176
|
if (showBackFile) {
|
|
729
|
-
console.log(
|
|
1177
|
+
console.log(chalk5.cyan(`
|
|
730
1178
|
\u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
|
|
731
1179
|
backupTasks.forEach((task) => {
|
|
732
|
-
console.log(
|
|
1180
|
+
console.log(chalk5.gray(` - ${task.fileName}`));
|
|
733
1181
|
});
|
|
734
1182
|
}
|
|
735
|
-
|
|
736
|
-
|
|
1183
|
+
if (useSpinner) {
|
|
1184
|
+
backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
|
|
1185
|
+
}
|
|
737
1186
|
let backedUpCount = 0;
|
|
738
1187
|
const backedUpFiles = [];
|
|
739
|
-
for (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const name = extIndex > -1 ? fileName.slice(0, extIndex) : fileName;
|
|
746
|
-
const ext = extIndex > -1 ? fileName.slice(extIndex) : "";
|
|
747
|
-
const backupFileName = `${name}.${timestamp}${ext}`;
|
|
748
|
-
const backupRemotePath = normalizePath(`${dir}/${backupFileName}`);
|
|
749
|
-
await client.downloadTo(localTempPath, normalizePath(`${dir}/${fileName}`));
|
|
750
|
-
await client.uploadFrom(localTempPath, backupRemotePath);
|
|
751
|
-
backedUpFiles.push(buildUrl(protocol, baseUrl, backupRemotePath));
|
|
752
|
-
return true;
|
|
753
|
-
} catch (error) {
|
|
754
|
-
console.warn(chalk.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
755
|
-
return false;
|
|
1188
|
+
for (const { fileName } of backupTasks) {
|
|
1189
|
+
try {
|
|
1190
|
+
const localTempPath = path2.join(tempDir.path, fileName);
|
|
1191
|
+
const localTempDir = path2.dirname(localTempPath);
|
|
1192
|
+
if (!fs2.existsSync(localTempDir)) {
|
|
1193
|
+
fs2.mkdirSync(localTempDir, { recursive: true });
|
|
756
1194
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1195
|
+
const fileDir = path2.posix.dirname(fileName);
|
|
1196
|
+
const fileBaseName = path2.posix.basename(fileName);
|
|
1197
|
+
const extIndex = fileBaseName.lastIndexOf(".");
|
|
1198
|
+
const name = extIndex > -1 ? fileBaseName.slice(0, extIndex) : fileBaseName;
|
|
1199
|
+
const ext = extIndex > -1 ? fileBaseName.slice(extIndex) : "";
|
|
1200
|
+
const backupFileName = `${name}.${timestamp}${ext}`;
|
|
1201
|
+
const backupRelativePath = fileDir === "." ? backupFileName : normalizeRemotePath(fileDir, backupFileName);
|
|
1202
|
+
const sourceRemotePath = normalizeRemotePath(dir, fileName);
|
|
1203
|
+
const backupRemotePath = normalizeRemotePath(dir, backupRelativePath);
|
|
1204
|
+
await client.downloadTo(localTempPath, sourceRemotePath);
|
|
1205
|
+
await client.uploadFrom(localTempPath, backupRemotePath);
|
|
1206
|
+
backedUpFiles.push(resolveDisplayUrl(alias, backupRemotePath));
|
|
1207
|
+
backedUpCount++;
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
console.warn(chalk5.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
|
|
1210
|
+
}
|
|
760
1211
|
}
|
|
761
1212
|
if (backedUpCount > 0) {
|
|
762
|
-
backupProgressSpinner
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
console.log();
|
|
1213
|
+
backupProgressSpinner?.stop();
|
|
1214
|
+
return {
|
|
1215
|
+
title: "\u5907\u4EFD\u5B8C\u6210",
|
|
1216
|
+
items: backedUpFiles
|
|
1217
|
+
};
|
|
768
1218
|
} else {
|
|
769
|
-
backupProgressSpinner
|
|
1219
|
+
if (backupProgressSpinner) {
|
|
1220
|
+
backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
|
|
1221
|
+
}
|
|
1222
|
+
return null;
|
|
770
1223
|
}
|
|
771
1224
|
} catch (error) {
|
|
772
1225
|
if (backupProgressSpinner) {
|
|
773
1226
|
backupProgressSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
774
|
-
} else {
|
|
1227
|
+
} else if (backupSpinner) {
|
|
775
1228
|
backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
|
|
776
1229
|
}
|
|
777
1230
|
throw error;
|