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/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 chalk from "chalk";
4
+ import chalk5 from "chalk";
5
+ import cliProgress from "cli-progress";
5
6
  import dayjs from "dayjs";
6
- import fs from "fs";
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
- 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)}`;
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 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));
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
- 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 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 sleep = async (ms) => {
64
- await new Promise((resolve) => setTimeout(resolve, ms));
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 = normalizeUploadPath(uploadPath);
85
- let outDir = normalizePath(path.resolve("dist"));
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 = normalizePath(path.posix.dirname(task.remotePath));
353
+ const remoteDir = normalizePath2(path2.posix.dirname(task.remotePath));
124
354
  if (remoteDir && remoteDir !== ".") {
125
- await context.client.ensureDir(remoteDir);
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
- `${chalk.red("\u2717")} ${task.filePath} => ${error instanceof Error ? error.message : String(error)}`
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 (connectConfig, files, targetDir, windowSize = concurrency) => {
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 = normalizePath(path.resolve(outDir, relativeFilePath));
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, tasks.length || 1));
212
- const activeFiles = /* @__PURE__ */ new Set();
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 spinner = useInteractiveOutput ? ora({ text: "\u51C6\u5907\u4E0A\u4F20...", spinner: "dots12" }).start() : null;
215
- const reportEvery = Math.max(1, Math.ceil(totalFiles / 10));
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
- const etaSeconds = speed > 0 ? Math.max(0, (totalBytes - uploadedBytes) / speed) : 0;
223
- const activeList = Array.from(activeFiles);
224
- const currentFile = activeList.length > 0 ? trimMiddle(activeList[activeList.length - 1], 86) : "-";
225
- if (!spinner) {
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
- `${chalk.gray("\u8FDB\u5EA6:")} ${completed}/${totalFiles} (${percentage}%) | ${chalk.gray("\u6570\u636E:")} ${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)} | ${chalk.gray("\u901F\u5EA6:")} ${formatBytes(speed)}/s`
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
- const bar = buildCapsuleBar(progressRatio);
236
- const warnLine = retries > 0 || failed > 0 ? `
237
- ${chalk.yellow("\u91CD\u8BD5")}: ${retries} ${chalk.yellow("\u5931\u8D25")}: ${failed}` : "";
238
- spinner.text = [
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 = spinner ? setInterval(updateProgress, 120) : null;
245
- let currentIndex = 0;
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 index = currentIndex++;
260
- if (index >= tasks.length) return;
261
- const task = tasks[index];
262
- activeFiles.add(task.remotePath);
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
- await Promise.all(Array.from({ length: safeWindowSize }, () => worker()));
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 (spinner) {
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
- spinner.succeed(
298
- `${chalk.green("\u4E0A\u4F20\u6210\u529F")} ${successCount} \u4E2A\u6587\u4EF6\u3002
299
- ${buildCapsuleBar(1)} 100% (${totalFiles}/${totalFiles}) ${chalk.gray("|")} \u901F\u5EA6 ${chalk.magenta(`${formatBytes(speed)}/s`)} ${chalk.gray("|")} \u8017\u65F6 ${chalk.yellow(formatDuration(elapsedSeconds))}`
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(`${chalk.green("\u2714")} \u6240\u6709\u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
713
+ console.log(`${getLogSymbol("success")} \u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
303
714
  }
304
- return results;
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(chalk.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
310
- if (!host) console.error(chalk.red(" - \u7F3A\u5C11 host"));
311
- if (!user) console.error(chalk.red(" - \u7F3A\u5C11 user"));
312
- if (!password) console.error(chalk.red(" - \u7F3A\u5C11 password"));
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(`${chalk.yellow("\u26A0 \u6CA1\u6709\u627E\u5230\u9700\u8981\u4E0A\u4F20\u7684\u6587\u4EF6")}`);
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(chalk.cyan(`
326
- \u{1F680} FTP \u90E8\u7F72\u5F00\u59CB
327
- `));
328
- console.log(`${chalk.gray("Server:")} ${chalk.green(displayName)}`);
329
- console.log(`${chalk.gray("Host:")} ${chalk.green(host)}`);
330
- console.log(`${chalk.gray("Source:")} ${chalk.yellow(outDir)}`);
331
- console.log(`${chalk.gray("Target:")} ${chalk.yellow(normalizedUploadPath)}`);
332
- if (alias) console.log(`${chalk.gray("Alias:")} ${chalk.green(alias)}`);
333
- console.log(`${chalk.gray("Files:")} ${chalk.blue(totalFiles)}
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
- if (preflightSpinner) preflightSpinner.succeed("\u8FDE\u63A5\u6210\u529F");
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
- await createSingleBackup(
819
+ const backupStartedAt = Date.now();
820
+ backupSummary = await createSingleBackup(
346
821
  preflightClient,
347
822
  normalizedUploadPath,
348
- protocol,
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
- await createBackupFile(
840
+ const backupStartedAt = Date.now();
841
+ backupSummary = await createBackupFile(
361
842
  preflightClient,
362
843
  normalizedUploadPath,
363
- protocol,
364
- baseUrl,
365
- showBackFile
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 results = await uploadFilesInBatches(
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
- console.log("\n" + chalk.gray("\u2500".repeat(40)) + "\n");
385
- if (failedCount === 0) {
386
- console.log(`${chalk.green("\u{1F389} \u90E8\u7F72\u6210\u529F!")}`);
387
- } else {
388
- console.log(`${chalk.yellow("\u26A0 \u90E8\u7F72\u5B8C\u6210\u4F46\u5B58\u5728\u9519\u8BEF")}`);
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
- console.log(` ${chalk.red("\u2717")} \u5931\u8D25: ${chalk.bold(failedCount)}`);
395
- }
396
- console.log(` ${chalk.cyan("\u21C4")} \u91CD\u8BD5: ${chalk.bold(retryCount)}`);
397
- console.log(` ${chalk.blue("\u{1F4E6}")} \u6570\u636E: ${chalk.bold(formatBytes(uploadedBytes))}`);
398
- console.log(` ${chalk.magenta("\u26A1")} \u5E73\u5747\u901F\u5EA6: ${chalk.bold(`${formatBytes(avgSpeed)}/s`)}`);
399
- console.log(` ${chalk.blue("\u23F1")} \u8017\u65F6: ${chalk.bold(duration)}s`);
400
- if (baseUrl) {
401
- console.log(
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
- console.log("");
406
- if (failedCount > 0) {
407
- const failedItems = results.filter((result) => !result.success);
408
- const previewCount = Math.min(5, failedItems.length);
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
- console.log("");
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.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
941
+ if (preflightSpinner) preflightSpinner.stop();
423
942
  console.log(`
424
- ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} ${error}
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(chalk.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
976
+ console.log(chalk5.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
451
977
  selectedConfigs = [defaultConfig];
452
978
  } else if (defaultConfig) {
453
- console.log(chalk.yellow(`\u26A0 \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
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(chalk.yellow("\n\u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
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(chalk.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
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(chalk.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
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(chalk.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
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
- console.log(`${chalk.red("\u2717 \u914D\u7F6E\u9519\u8BEF:")}
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 = normalizePath(path.resolve(config.root, config.build.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 (!fs.existsSync(localDir)) {
570
- fs.mkdirSync(localDir, { recursive: true });
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 = normalizePath(`${remoteDir}/${entry.name}`);
575
- const localPath = path.join(localDir, entry.name);
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 connectWithRetry(client, config, maxRetries, retryDelay, silentLogs = false) {
605
- let lastError;
606
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
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-zip");
650
- const zipFilePath = path.join(os.tmpdir(), "vite-plugin-deploy-ftp", fileName);
1106
+ const tempDir = createTempDir("backup-download");
1107
+ const zipTempDir = createTempDir("backup-zip");
1108
+ const zipFilePath = path2.join(zipTempDir.path, fileName);
651
1109
  try {
652
- const zipDir = path.dirname(zipFilePath);
653
- if (!fs.existsSync(zipDir)) {
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.warn("\u672A\u627E\u5230\u53EF\u5907\u4EFD\u7684\u8FDC\u7A0B\u6587\u4EF6");
660
- return;
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(chalk.cyan(`
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(chalk.gray(` - ${file.remotePath} (${file.size} bytes)`));
1124
+ console.log(chalk5.gray(` - ${file.remotePath} (${file.size} bytes)`));
667
1125
  });
668
1126
  }
669
- backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
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
- backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk.yellow(
672
- `==> ${buildUrl(protocol, baseUrl, `${dir}/${fileName}`)}`
673
- )}`;
674
- await client.uploadFrom(zipFilePath, normalizePath(`${dir}/${fileName}`));
675
- const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
676
- backupSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
677
- console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
678
- console.log(chalk.green(`${backupUrl}`));
679
- console.log();
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.fail("\u5907\u4EFD\u5931\u8D25");
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 (fs.existsSync(zipFilePath)) {
687
- fs.rmSync(zipFilePath);
1151
+ if (fs2.existsSync(zipFilePath)) {
1152
+ fs2.rmSync(zipFilePath);
688
1153
  }
689
1154
  } catch (error) {
690
- console.warn(chalk.yellow("\u26A0 \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
1155
+ console.warn(chalk5.yellow("\u26A0 \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
691
1156
  }
692
1157
  }
693
1158
  }
694
- async function createZipFile(sourceDir, outputPath) {
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 ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
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 remoteFiles = await client.list(dir);
719
- const backupTasks = singleBackFiles.map((fileName) => {
720
- const remoteFile = remoteFiles.find((f) => f.name === fileName);
721
- return remoteFile ? { fileName, exists: true } : { fileName, exists: false };
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.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
725
- return;
1170
+ if (backupSpinner) {
1171
+ backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
1172
+ }
1173
+ return null;
726
1174
  }
727
- backupSpinner.stop();
1175
+ backupSpinner?.stop();
728
1176
  if (showBackFile) {
729
- console.log(chalk.cyan(`
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(chalk.gray(` - ${task.fileName}`));
1180
+ console.log(chalk5.gray(` - ${task.fileName}`));
733
1181
  });
734
1182
  }
735
- backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
736
- const concurrencyLimit = 3;
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 (let i = 0; i < backupTasks.length; i += concurrencyLimit) {
740
- const batch = backupTasks.slice(i, i + concurrencyLimit);
741
- const promises = batch.map(async ({ fileName }) => {
742
- try {
743
- const localTempPath = path.join(tempDir.path, fileName);
744
- const extIndex = fileName.lastIndexOf(".");
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
- const results = await Promise.all(promises);
759
- backedUpCount += results.filter(Boolean).length;
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.succeed("\u5907\u4EFD\u5B8C\u6210");
763
- console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
764
- backedUpFiles.forEach((url) => {
765
- console.log(chalk.green(`\u{1F517} ${url}`));
766
- });
767
- console.log();
1213
+ backupProgressSpinner?.stop();
1214
+ return {
1215
+ title: "\u5907\u4EFD\u5B8C\u6210",
1216
+ items: backedUpFiles
1217
+ };
768
1218
  } else {
769
- backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
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;