vite-plugin-deploy-oss 3.4.2 → 3.5.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.
@@ -0,0 +1,728 @@
1
+ // src/deploy.ts
2
+ import oss from "ali-oss";
3
+ import chalk3 from "chalk";
4
+ import cliProgress from "cli-progress";
5
+ import { globSync } from "glob";
6
+ import { mkdir, stat, unlink, writeFile } from "fs/promises";
7
+ import { dirname, resolve as resolve2 } from "path";
8
+
9
+ // src/utils/file.ts
10
+ import { createHash } from "crypto";
11
+ import { createReadStream } from "fs";
12
+ import { readdir, rm } from "fs/promises";
13
+ import { resolve } from "path";
14
+ var GARBAGE_FILE_REGEX = /(?:Thumbs\.db|\.DS_Store)$/i;
15
+ var getFileMd5 = (filePath) => {
16
+ return new Promise((resolvePromise, reject) => {
17
+ const hash = createHash("md5");
18
+ const stream = createReadStream(filePath);
19
+ stream.on("error", (err) => reject(err));
20
+ stream.on("data", (chunk) => hash.update(chunk));
21
+ stream.on("end", () => resolvePromise(hash.digest("hex")));
22
+ });
23
+ };
24
+ var removeEmptyDirectories = async (rootDir) => {
25
+ const deletedDirectories = [];
26
+ const visit = async (dirPath) => {
27
+ const entries = await readdir(dirPath, { withFileTypes: true });
28
+ let hasNonEmptyContent = false;
29
+ for (const entry of entries) {
30
+ const entryPath = resolve(dirPath, entry.name);
31
+ if (entry.isDirectory()) {
32
+ const removed = await visit(entryPath);
33
+ if (!removed) hasNonEmptyContent = true;
34
+ continue;
35
+ }
36
+ if (!GARBAGE_FILE_REGEX.test(entry.name)) {
37
+ hasNonEmptyContent = true;
38
+ }
39
+ }
40
+ if (hasNonEmptyContent) return false;
41
+ await rm(dirPath, { recursive: true, force: true });
42
+ deletedDirectories.push(dirPath);
43
+ return true;
44
+ };
45
+ await visit(resolve(rootDir));
46
+ return deletedDirectories;
47
+ };
48
+
49
+ // src/utils/path.ts
50
+ var DEFAULT_MANIFEST_FILE_NAME = "oss-manifest.json";
51
+ var normalizeSlash = (value) => value.replace(/\\/g, "/").trim();
52
+ var normalizePathSegments = (...values) => values.filter((value) => Boolean(value)).flatMap((value) => normalizeSlash(value).split("/")).filter(Boolean).join("/");
53
+ var splitUrlLikeBase = (value) => {
54
+ const normalized = normalizeSlash(value);
55
+ const protocolMatch = normalized.match(/^([a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]+)(.*)$/);
56
+ if (protocolMatch) {
57
+ return {
58
+ prefix: protocolMatch[1],
59
+ path: protocolMatch[2] || ""
60
+ };
61
+ }
62
+ const protocolRelativeMatch = normalized.match(/^(\/\/[^/]+)(.*)$/);
63
+ if (protocolRelativeMatch) {
64
+ return {
65
+ prefix: protocolRelativeMatch[1],
66
+ path: protocolRelativeMatch[2] || ""
67
+ };
68
+ }
69
+ if (normalized.startsWith("/")) {
70
+ return {
71
+ prefix: "/",
72
+ path: normalized
73
+ };
74
+ }
75
+ return {
76
+ prefix: "",
77
+ path: normalized
78
+ };
79
+ };
80
+ var normalizeUrlLikeBase = (base) => {
81
+ const { prefix, path } = splitUrlLikeBase(base);
82
+ const normalizedPath = normalizePathSegments(path);
83
+ if (!prefix) return normalizedPath;
84
+ if (!normalizedPath) return prefix;
85
+ if (prefix === "/") return `/${normalizedPath}`;
86
+ return `${prefix}/${normalizedPath}`;
87
+ };
88
+ var ensureTrailingSlash = (value) => {
89
+ if (!value || value.endsWith("/")) return value;
90
+ return `${value}/`;
91
+ };
92
+ var normalizeObjectKey = (targetDir, relativeFilePath) => normalizePathSegments(targetDir, relativeFilePath);
93
+ var normalizeManifestFileName = (fileName) => {
94
+ const normalized = normalizePathSegments(fileName || DEFAULT_MANIFEST_FILE_NAME);
95
+ return normalized || DEFAULT_MANIFEST_FILE_NAME;
96
+ };
97
+ var resolveManifestFileName = (manifest) => {
98
+ if (!manifest) return null;
99
+ if (manifest === true) return DEFAULT_MANIFEST_FILE_NAME;
100
+ return normalizeManifestFileName(manifest.fileName);
101
+ };
102
+ var encodeUrlPath = (path) => encodeURI(normalizePathSegments(path));
103
+ var joinUrlPath = (base, path) => `${normalizeUrlLikeBase(base).replace(/\/+$/, "")}/${encodeUrlPath(path)}`;
104
+ var resolveUploadedFileUrl = (relativeFilePath, objectKey, configBase, alias) => {
105
+ if (configBase) return joinUrlPath(configBase, relativeFilePath);
106
+ if (alias) return joinUrlPath(alias, objectKey);
107
+ return objectKey;
108
+ };
109
+
110
+ // src/utils/progress.ts
111
+ import chalk from "chalk";
112
+ var formatBytes = (bytes) => {
113
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
114
+ const units = ["B", "KB", "MB", "GB", "TB"];
115
+ let value = bytes;
116
+ let unitIndex = 0;
117
+ while (value >= 1024 && unitIndex < units.length - 1) {
118
+ value /= 1024;
119
+ unitIndex++;
120
+ }
121
+ const digits = value >= 100 || unitIndex === 0 ? 0 : 1;
122
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
123
+ };
124
+ var formatDuration = (seconds) => {
125
+ if (!Number.isFinite(seconds) || seconds < 0) return "--";
126
+ const rounded = Math.round(seconds);
127
+ const mins = Math.floor(rounded / 60);
128
+ const secs = rounded % 60;
129
+ if (mins === 0) return `${secs}s`;
130
+ return `${mins}m${String(secs).padStart(2, "0")}s`;
131
+ };
132
+
133
+ // src/utils/terminal.ts
134
+ import chalk2 from "chalk";
135
+ import cliTruncate from "cli-truncate";
136
+ import logSymbols from "log-symbols";
137
+ import stringWidth from "string-width";
138
+ var panelBorderColor = {
139
+ info: "cyan",
140
+ success: "green",
141
+ warning: "yellow",
142
+ danger: "red"
143
+ };
144
+ var getTerminalWidth = () => process.stdout?.columns || 100;
145
+ var getPanelInnerWidth = () => Math.max(46, Math.min(84, getTerminalWidth() - 4));
146
+ var padVisual = (text, width) => `${text}${" ".repeat(Math.max(0, width - stringWidth(text)))}`;
147
+ var fitVisual = (text, width) => {
148
+ if (width <= 0) return "";
149
+ return padVisual(cliTruncate(text, width, { position: "middle" }), width);
150
+ };
151
+ var truncateTerminalText = (text, reservedWidth = 26) => {
152
+ const maxWidth = Math.max(24, Math.min(88, getTerminalWidth() - reservedWidth));
153
+ return cliTruncate(text, maxWidth, { position: "middle" });
154
+ };
155
+ var renderPanel = (title, rows, tone = "info", footer) => {
156
+ const color = chalk2[panelBorderColor[tone]];
157
+ const innerWidth = getPanelInnerWidth();
158
+ const labelWidth = rows.length > 0 ? Math.max(...rows.map((row) => stringWidth(row.label))) : 0;
159
+ const contentLines = [chalk2.bold(cliTruncate(title, innerWidth, { position: "end" }))];
160
+ if (rows.length > 0) {
161
+ contentLines.push("");
162
+ for (const row of rows) {
163
+ const paddedLabel = padVisual(row.label, labelWidth);
164
+ const prefix = `${paddedLabel} `;
165
+ const availableValueWidth = Math.max(8, innerWidth - stringWidth(prefix));
166
+ contentLines.push(`${chalk2.gray(prefix)}${fitVisual(row.value, availableValueWidth)}`);
167
+ }
168
+ }
169
+ if (footer) {
170
+ contentLines.push("");
171
+ contentLines.push(chalk2.gray(cliTruncate(footer, innerWidth, { position: "middle" })));
172
+ }
173
+ const top = color(`\u256D${"\u2500".repeat(innerWidth + 2)}\u256E`);
174
+ const bottom = color(`\u2570${"\u2500".repeat(innerWidth + 2)}\u256F`);
175
+ const body = contentLines.map((line) => `${color("\u2502")} ${fitVisual(line, innerWidth)} ${color("\u2502")}`).join("\n");
176
+ return `${top}
177
+ ${body}
178
+ ${bottom}`;
179
+ };
180
+ var renderInlineStats = (items) => items.filter(Boolean).join(chalk2.gray(" \xB7 "));
181
+ var getPanelDot = (tone = "success") => {
182
+ switch (tone) {
183
+ case "info":
184
+ return chalk2.green("\u25CF");
185
+ case "success":
186
+ return chalk2.green("\u25CF");
187
+ case "warning":
188
+ return chalk2.yellow("\u25CF");
189
+ case "danger":
190
+ return chalk2.red("\u25CF");
191
+ }
192
+ };
193
+ var getLogSymbol = (tone) => {
194
+ switch (tone) {
195
+ case "success":
196
+ return logSymbols.success;
197
+ case "warning":
198
+ return logSymbols.warning;
199
+ case "danger":
200
+ return logSymbols.error;
201
+ }
202
+ };
203
+
204
+ // src/deploy.ts
205
+ var formatTimingDuration = (durationMs) => {
206
+ if (durationMs < 1e3) return `${durationMs}ms`;
207
+ const seconds = durationMs / 1e3;
208
+ return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`;
209
+ };
210
+ var renderDebugPanel = (entries) => {
211
+ const rows = entries.map((entry) => ({
212
+ label: `${entry.label}:`,
213
+ value: chalk3.cyan(
214
+ entry.detail ? `${formatTimingDuration(entry.durationMs)} \xB7 ${truncateTerminalText(entry.detail, 24)}` : formatTimingDuration(entry.durationMs)
215
+ )
216
+ }));
217
+ return renderPanel(`${getPanelDot("success")} \u8C03\u8BD5\u8017\u65F6`, rows, "info");
218
+ };
219
+ var createManifestPayload = async (results, configBase, alias) => {
220
+ const successfulResults = results.filter((result) => result.success);
221
+ const files = await Promise.all(
222
+ successfulResults.map(async (result) => {
223
+ const md5 = await getFileMd5(result.file);
224
+ return {
225
+ file: result.relativeFilePath,
226
+ key: result.name,
227
+ url: resolveUploadedFileUrl(result.relativeFilePath, result.name, configBase, alias),
228
+ md5
229
+ };
230
+ })
231
+ );
232
+ return {
233
+ version: Date.now(),
234
+ files
235
+ };
236
+ };
237
+ var validateOptions = (option, runtimeOption) => {
238
+ const errors = [];
239
+ if (!option.accessKeyId) errors.push("accessKeyId is required");
240
+ if (!option.accessKeySecret) errors.push("accessKeySecret is required");
241
+ if (!option.bucket) errors.push("bucket is required");
242
+ if (!option.region) errors.push("region is required");
243
+ if (!option.uploadDir) errors.push("uploadDir is required");
244
+ if (!Number.isInteger(runtimeOption.retryTimes) || runtimeOption.retryTimes < 1)
245
+ errors.push("retryTimes must be >= 1");
246
+ if (!Number.isInteger(runtimeOption.concurrency) || runtimeOption.concurrency < 1) {
247
+ errors.push("concurrency must be >= 1");
248
+ }
249
+ if (!Number.isFinite(runtimeOption.multipartThreshold) || runtimeOption.multipartThreshold <= 0) {
250
+ errors.push("multipartThreshold must be > 0");
251
+ }
252
+ return errors;
253
+ };
254
+ var deployOss = async (option) => {
255
+ const rawOption = option || {};
256
+ const {
257
+ accessKeyId,
258
+ accessKeySecret,
259
+ region,
260
+ bucket,
261
+ configBase,
262
+ skip = "**/index.html",
263
+ uploadDir,
264
+ overwrite = true,
265
+ secure = true,
266
+ autoDelete = false,
267
+ alias,
268
+ open = true,
269
+ debug = false,
270
+ fancy = true,
271
+ noCache = false,
272
+ failOnError = true,
273
+ concurrency = 5,
274
+ retryTimes = 3,
275
+ multipartThreshold = 10 * 1024 * 1024,
276
+ manifest = false,
277
+ outDir = "dist",
278
+ ...props
279
+ } = rawOption;
280
+ if (!open) {
281
+ return {
282
+ success: true,
283
+ results: [],
284
+ outDir: normalizeSlash(resolve2(outDir)),
285
+ durationSeconds: 0,
286
+ uploadedBytes: 0,
287
+ retryCount: 0
288
+ };
289
+ }
290
+ const validationErrors = validateOptions(rawOption, {
291
+ retryTimes,
292
+ concurrency,
293
+ multipartThreshold
294
+ });
295
+ if (validationErrors.length > 0) {
296
+ throw new Error(`vite-plugin-deploy-oss \u914D\u7F6E\u9519\u8BEF:
297
+ ${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
298
+ }
299
+ const normalizedUploadDir = normalizePathSegments(uploadDir);
300
+ const normalizedConfigBase = configBase ? ensureTrailingSlash(normalizeUrlLikeBase(configBase)) : void 0;
301
+ const normalizedAlias = alias ? normalizeUrlLikeBase(alias) : void 0;
302
+ const manifestFileName = resolveManifestFileName(manifest);
303
+ const effectiveAutoDelete = manifestFileName ? false : autoDelete;
304
+ const effectiveSkip = manifestFileName ? [] : Array.isArray(skip) ? skip : [skip];
305
+ const resolvedOutDir = normalizeSlash(resolve2(outDir));
306
+ const useInteractiveOutput = fancy && Boolean(process.stdout?.isTTY) && Boolean(process.stderr?.isTTY) && !process.env.CI;
307
+ const clearViewport = () => {
308
+ if (!useInteractiveOutput) return;
309
+ process.stdout.write("\x1B[2J\x1B[0f");
310
+ };
311
+ const uploadFileWithRetry = async (client2, task, silentLogs, maxRetries = retryTimes) => {
312
+ const shouldUseMultipart = task.size >= multipartThreshold;
313
+ const headers = {
314
+ "x-oss-storage-class": "Standard",
315
+ "x-oss-object-acl": "default",
316
+ "Cache-Control": task.cacheControl || (noCache || task.name.endsWith(".html") ? "no-cache" : "public, max-age=86400, immutable"),
317
+ "x-oss-forbid-overwrite": overwrite ? "false" : "true"
318
+ };
319
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
320
+ try {
321
+ const result = shouldUseMultipart ? await client2.multipartUpload(task.name, task.filePath, {
322
+ timeout: 6e5,
323
+ partSize: 1024 * 1024,
324
+ parallel: Math.max(1, Math.min(concurrency, 4)),
325
+ headers
326
+ }) : await client2.put(task.name, task.filePath, {
327
+ timeout: 6e5,
328
+ headers
329
+ });
330
+ if (result.res.status === 200) {
331
+ if (effectiveAutoDelete) {
332
+ try {
333
+ await unlink(task.filePath);
334
+ } catch {
335
+ console.warn(
336
+ `${getLogSymbol("warning")} \u5220\u9664\u672C\u5730\u6587\u4EF6\u5931\u8D25: ${truncateTerminalText(task.relativeFilePath, 18)}`
337
+ );
338
+ }
339
+ }
340
+ return {
341
+ success: true,
342
+ file: task.filePath,
343
+ relativeFilePath: task.relativeFilePath,
344
+ name: task.name,
345
+ size: task.size,
346
+ retries: attempt - 1
347
+ };
348
+ }
349
+ throw new Error(`Upload failed with status: ${result.res.status}`);
350
+ } catch (error) {
351
+ if (attempt === maxRetries) {
352
+ if (!silentLogs) {
353
+ const reason = error instanceof Error ? error.message : String(error);
354
+ console.log(`${getLogSymbol("danger")} ${truncateTerminalText(task.relativeFilePath, 18)} ${reason}`);
355
+ }
356
+ return {
357
+ success: false,
358
+ file: task.filePath,
359
+ relativeFilePath: task.relativeFilePath,
360
+ name: task.name,
361
+ size: task.size,
362
+ retries: attempt - 1,
363
+ error
364
+ };
365
+ }
366
+ if (!silentLogs) {
367
+ console.log(
368
+ `${getLogSymbol("warning")} ${truncateTerminalText(task.relativeFilePath, 18)} \u6B63\u5728\u91CD\u8BD5 (${attempt}/${maxRetries})`
369
+ );
370
+ }
371
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 1e3 * attempt));
372
+ }
373
+ }
374
+ return {
375
+ success: false,
376
+ file: task.filePath,
377
+ relativeFilePath: task.relativeFilePath,
378
+ name: task.name,
379
+ size: task.size,
380
+ retries: maxRetries,
381
+ error: new Error("Max retries exceeded")
382
+ };
383
+ };
384
+ const uploadSingleTask = async (client2, task) => uploadFileWithRetry(client2, task, false);
385
+ const uploadFilesInBatches = async (client2, files2, windowSize = concurrency) => {
386
+ const results = [];
387
+ const debugEntries2 = [];
388
+ const totalFiles = files2.length;
389
+ const tasks = [];
390
+ let completed = 0;
391
+ let failed = 0;
392
+ let uploadedBytes = 0;
393
+ let retries = 0;
394
+ const taskPrepareStartedAt = Date.now();
395
+ const taskCandidates = await Promise.all(
396
+ files2.map(async (relativeFilePath) => {
397
+ const filePath = normalizeSlash(resolve2(resolvedOutDir, relativeFilePath));
398
+ const name = normalizeObjectKey(normalizedUploadDir, relativeFilePath);
399
+ try {
400
+ const fileStats = await stat(filePath);
401
+ return { task: { filePath, relativeFilePath, name, size: fileStats.size } };
402
+ } catch (error) {
403
+ return { task: null, error, filePath, relativeFilePath, name };
404
+ }
405
+ })
406
+ );
407
+ debugEntries2.push({
408
+ label: "\u751F\u6210\u4E0A\u4F20\u4EFB\u52A1",
409
+ durationMs: Date.now() - taskPrepareStartedAt,
410
+ detail: `${files2.length} \u4E2A\u6587\u4EF6`
411
+ });
412
+ for (const candidate of taskCandidates) {
413
+ if (candidate.task) {
414
+ tasks.push(candidate.task);
415
+ } else {
416
+ failed++;
417
+ completed++;
418
+ results.push({
419
+ success: false,
420
+ file: candidate.filePath,
421
+ relativeFilePath: candidate.relativeFilePath,
422
+ name: candidate.name,
423
+ size: 0,
424
+ retries: 0,
425
+ error: candidate.error
426
+ });
427
+ }
428
+ }
429
+ const totalBytes = tasks.reduce((sum, task) => sum + task.size, 0);
430
+ const startAt = Date.now();
431
+ const safeWindowSize = Math.max(1, Math.min(windowSize, tasks.length || 1));
432
+ const silentLogs = Boolean(useInteractiveOutput);
433
+ const progressBar = useInteractiveOutput ? new cliProgress.SingleBar({
434
+ hideCursor: true,
435
+ clearOnComplete: true,
436
+ stopOnComplete: true,
437
+ barsize: 18,
438
+ barCompleteChar: "\u2588",
439
+ barIncompleteChar: "\u2591",
440
+ format: `${chalk3.gray("\u4E0A\u4F20")} ${chalk3.bold("{percentage}%")} ${chalk3.cyan("{bar}")} ${chalk3.gray("\xB7")} ${chalk3.magenta("{speed}/s")} ${chalk3.gray("\xB7")} ${chalk3.gray("{elapsed}")}s`
441
+ }) : null;
442
+ const reportEvery = Math.max(1, Math.ceil(totalFiles / 6));
443
+ let lastReportedCompleted = -1;
444
+ if (progressBar) {
445
+ progressBar.start(totalFiles, 0, {
446
+ speed: formatBytes(0),
447
+ elapsed: "0"
448
+ });
449
+ }
450
+ const updateProgress = () => {
451
+ const elapsedSeconds = (Date.now() - startAt) / 1e3;
452
+ const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
453
+ if (!progressBar) {
454
+ const progressRatio = totalFiles > 0 ? completed / totalFiles : 1;
455
+ const percentage = Math.round(progressRatio * 100);
456
+ if (completed === 0 && totalFiles > 0) return;
457
+ if (completed === lastReportedCompleted) return;
458
+ if (completed === totalFiles || completed % reportEvery === 0) {
459
+ console.log(
460
+ `${chalk3.gray("\u4E0A\u4F20\u8FDB\u5EA6")} ${renderInlineStats([
461
+ chalk3.bold(`${completed}/${totalFiles}`),
462
+ `${percentage}%`,
463
+ `${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)}`,
464
+ `${formatBytes(speed)}/s`
465
+ ])}`
466
+ );
467
+ lastReportedCompleted = completed;
468
+ }
469
+ } else {
470
+ progressBar.update(completed, {
471
+ speed: chalk3.magenta(formatBytes(speed)),
472
+ elapsed: formatDuration(elapsedSeconds).replace(/s$/, "")
473
+ });
474
+ }
475
+ };
476
+ const refreshTimer = progressBar ? setInterval(updateProgress, 120) : null;
477
+ let currentIndex = 0;
478
+ const worker = async () => {
479
+ while (true) {
480
+ const index = currentIndex++;
481
+ if (index >= tasks.length) return;
482
+ const task = tasks[index];
483
+ updateProgress();
484
+ const result = await uploadFileWithRetry(client2, task, silentLogs);
485
+ completed++;
486
+ retries += result.retries;
487
+ if (result.success) {
488
+ uploadedBytes += result.size;
489
+ } else {
490
+ failed++;
491
+ }
492
+ results.push(result);
493
+ updateProgress();
494
+ }
495
+ };
496
+ updateProgress();
497
+ try {
498
+ await Promise.all(Array.from({ length: safeWindowSize }, () => worker()));
499
+ } finally {
500
+ if (refreshTimer) clearInterval(refreshTimer);
501
+ }
502
+ if (progressBar) {
503
+ const elapsedSeconds = (Date.now() - startAt) / 1e3;
504
+ const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
505
+ progressBar.update(totalFiles, {
506
+ speed: chalk3.magenta(formatBytes(speed)),
507
+ elapsed: formatDuration(elapsedSeconds).replace(/s$/, "")
508
+ });
509
+ progressBar.stop();
510
+ } else {
511
+ console.log(`${getLogSymbol("success")} \u6240\u6709\u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
512
+ }
513
+ debugEntries2.push({
514
+ label: "\u4E0A\u4F20\u6587\u4EF6",
515
+ durationMs: Date.now() - startAt,
516
+ detail: `${tasks.length} \u4E2A\u6210\u529F\u5019\u9009 \xB7 \u5E76\u53D1 ${safeWindowSize}`
517
+ });
518
+ return { results, debugEntries: debugEntries2 };
519
+ };
520
+ const startTime = Date.now();
521
+ const debugEntries = [];
522
+ const client = new oss({ region, accessKeyId, accessKeySecret, secure, bucket, ...props });
523
+ const collectFilesStartedAt = Date.now();
524
+ const files = globSync("**/*", {
525
+ cwd: resolvedOutDir,
526
+ nodir: true,
527
+ ignore: effectiveSkip
528
+ }).map((file) => normalizeSlash(file)).filter((file) => file !== manifestFileName);
529
+ debugEntries.push({
530
+ label: "\u626B\u63CF\u672C\u5730\u6587\u4EF6",
531
+ durationMs: Date.now() - collectFilesStartedAt,
532
+ detail: `${files.length} \u4E2A\u6587\u4EF6`
533
+ });
534
+ if (files.length === 0) {
535
+ console.log(`${getLogSymbol("warning")} \u6CA1\u6709\u627E\u5230\u9700\u8981\u4E0A\u4F20\u7684\u6587\u4EF6`);
536
+ return {
537
+ success: true,
538
+ results: [],
539
+ outDir: resolvedOutDir,
540
+ durationSeconds: (Date.now() - startTime) / 1e3,
541
+ uploadedBytes: 0,
542
+ retryCount: 0
543
+ };
544
+ }
545
+ clearViewport();
546
+ console.log(
547
+ renderPanel(
548
+ `${getPanelDot("success")} \u51C6\u5907\u90E8\u7F72`,
549
+ [
550
+ { label: "\u4F4D\u7F6E:", value: chalk3.green(`${bucket} \xB7 ${region}`) },
551
+ {
552
+ label: "\u76EE\u6807:",
553
+ value: chalk3.yellow(
554
+ truncateTerminalText(
555
+ normalizedAlias ? `${normalizedUploadDir || "/"} \xB7 ${normalizedAlias}` : normalizedUploadDir || "/",
556
+ 18
557
+ )
558
+ )
559
+ },
560
+ {
561
+ label: "\u6587\u4EF6:",
562
+ value: chalk3.blue(`${files.length} \u4E2A \xB7 ${truncateTerminalText(resolvedOutDir, 30)}`)
563
+ }
564
+ ],
565
+ "info"
566
+ )
567
+ );
568
+ try {
569
+ const uploadExecution = await uploadFilesInBatches(client, files, concurrency);
570
+ const { results, debugEntries: uploadDebugEntries } = uploadExecution;
571
+ if (debug) {
572
+ debugEntries.push(...uploadDebugEntries);
573
+ }
574
+ const successCount = results.filter((r) => r.success).length;
575
+ const failedCount = results.length - successCount;
576
+ const durationSeconds = (Date.now() - startTime) / 1e3;
577
+ const uploadedBytes = results.reduce((sum, result) => result.success ? sum + result.size : sum, 0);
578
+ const retryCount = results.reduce((sum, result) => sum + result.retries, 0);
579
+ const avgSpeed = durationSeconds > 0 ? uploadedBytes / durationSeconds : 0;
580
+ let manifestSummary = null;
581
+ let manifestUrl;
582
+ if (manifestFileName) {
583
+ const manifestRelativeFilePath = manifestFileName;
584
+ const manifestFilePath = normalizeSlash(resolve2(resolvedOutDir, manifestRelativeFilePath));
585
+ const manifestObjectKey = normalizeObjectKey(normalizedUploadDir, manifestRelativeFilePath);
586
+ const manifestStartedAt = Date.now();
587
+ await mkdir(dirname(manifestFilePath), { recursive: true });
588
+ await writeFile(
589
+ manifestFilePath,
590
+ JSON.stringify(await createManifestPayload(results, normalizedConfigBase, normalizedAlias), null, 2),
591
+ "utf8"
592
+ );
593
+ if (debug) {
594
+ debugEntries.push({
595
+ label: "\u751F\u6210\u6E05\u5355\u6587\u4EF6",
596
+ durationMs: Date.now() - manifestStartedAt,
597
+ detail: manifestRelativeFilePath
598
+ });
599
+ }
600
+ const manifestStats = await stat(manifestFilePath);
601
+ const manifestUploadStartedAt = Date.now();
602
+ const manifestResult = await uploadSingleTask(client, {
603
+ filePath: manifestFilePath,
604
+ relativeFilePath: manifestRelativeFilePath,
605
+ name: manifestObjectKey,
606
+ size: manifestStats.size,
607
+ cacheControl: "no-cache, no-store, must-revalidate"
608
+ });
609
+ if (!manifestResult.success) {
610
+ throw manifestResult.error || new Error(`Failed to upload manifest: ${manifestRelativeFilePath}`);
611
+ }
612
+ if (debug) {
613
+ debugEntries.push({
614
+ label: "\u4E0A\u4F20\u6E05\u5355\u6587\u4EF6",
615
+ durationMs: Date.now() - manifestUploadStartedAt,
616
+ detail: manifestRelativeFilePath
617
+ });
618
+ }
619
+ manifestUrl = resolveUploadedFileUrl(
620
+ manifestRelativeFilePath,
621
+ manifestObjectKey,
622
+ normalizedConfigBase,
623
+ normalizedAlias
624
+ );
625
+ manifestSummary = truncateTerminalText(manifestUrl || manifestObjectKey, 20);
626
+ }
627
+ try {
628
+ const cleanupStartedAt = Date.now();
629
+ await removeEmptyDirectories(resolvedOutDir);
630
+ if (debug) {
631
+ debugEntries.push({
632
+ label: "\u6E05\u7406\u7A7A\u76EE\u5F55",
633
+ durationMs: Date.now() - cleanupStartedAt
634
+ });
635
+ }
636
+ } catch (error) {
637
+ console.warn(`${getLogSymbol("warning")} \u6E05\u7406\u7A7A\u76EE\u5F55\u5931\u8D25: ${error}`);
638
+ }
639
+ const resultRows = [
640
+ {
641
+ label: "\u7ED3\u679C:",
642
+ value: failedCount === 0 ? chalk3.green(`${successCount}/${results.length} \u5168\u90E8\u6210\u529F`) : chalk3.yellow(`\u6210\u529F ${successCount} \u4E2A\uFF0C\u5931\u8D25 ${failedCount} \u4E2A`)
643
+ },
644
+ {
645
+ label: "\u7EDF\u8BA1:",
646
+ value: renderInlineStats([
647
+ `${retryCount} \u6B21\u91CD\u8BD5`,
648
+ formatBytes(uploadedBytes),
649
+ `${formatBytes(avgSpeed)}/s`,
650
+ formatDuration(durationSeconds)
651
+ ])
652
+ },
653
+ ...manifestSummary ? [{ label: "\u6E05\u5355:", value: chalk3.cyan(manifestSummary) }] : []
654
+ ];
655
+ if (failedCount > 0) {
656
+ const failedItems = results.filter((result) => !result.success).slice(0, 2);
657
+ resultRows.push(
658
+ ...failedItems.map((item, index) => ({
659
+ label: `\u5931\u8D25 ${index + 1}`,
660
+ value: chalk3.red(
661
+ `${truncateTerminalText(item.name, 26)} \xB7 ${truncateTerminalText(item.error?.message || "unknown error", 22)}`
662
+ )
663
+ }))
664
+ );
665
+ if (failedCount > failedItems.length) {
666
+ resultRows.push({
667
+ label: "\u5176\u4F59",
668
+ value: chalk3.gray(`\u8FD8\u6709 ${failedCount - failedItems.length} \u4E2A\u5931\u8D25\u9879\u672A\u5C55\u5F00`)
669
+ });
670
+ }
671
+ }
672
+ console.log(
673
+ renderPanel(
674
+ failedCount === 0 ? `${getPanelDot("success")} \u90E8\u7F72\u5B8C\u6210` : `${getPanelDot("warning")} \u90E8\u7F72\u5B8C\u6210`,
675
+ resultRows,
676
+ failedCount === 0 ? "success" : "warning"
677
+ )
678
+ );
679
+ if (debug) {
680
+ debugEntries.push({
681
+ label: "\u603B\u8017\u65F6",
682
+ durationMs: Date.now() - startTime
683
+ });
684
+ console.log(renderDebugPanel(debugEntries));
685
+ }
686
+ if (failedCount > 0 && failOnError) {
687
+ throw new Error(`Failed to upload ${failedCount} of ${results.length} files`);
688
+ }
689
+ return {
690
+ success: failedCount === 0,
691
+ results,
692
+ outDir: resolvedOutDir,
693
+ durationSeconds,
694
+ uploadedBytes,
695
+ retryCount,
696
+ manifestUrl
697
+ };
698
+ } catch (error) {
699
+ console.log(`
700
+ ${getLogSymbol("danger")} \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF: ${error}
701
+ `);
702
+ if (debug && debugEntries.length > 0) {
703
+ debugEntries.push({
704
+ label: "\u5931\u8D25\u524D\u8017\u65F6",
705
+ durationMs: Date.now() - startTime
706
+ });
707
+ console.log(renderDebugPanel(debugEntries));
708
+ }
709
+ if (failOnError) {
710
+ throw error instanceof Error ? error : new Error(String(error));
711
+ }
712
+ return {
713
+ success: false,
714
+ results: [],
715
+ outDir: resolvedOutDir,
716
+ durationSeconds: (Date.now() - startTime) / 1e3,
717
+ uploadedBytes: 0,
718
+ retryCount: 0
719
+ };
720
+ }
721
+ };
722
+
723
+ export {
724
+ normalizeSlash,
725
+ normalizeUrlLikeBase,
726
+ ensureTrailingSlash,
727
+ deployOss
728
+ };