refactor(utils): 重构文件流式分割上传功能

- 将 streamSplitAndUpload 函数拆分为独立的 processFileLines 函数
- 简化文件按行处理逻辑,移除冗余的行收集和缓存机制
- 优化并发上传实现,使用 Promise 集合管理上传任务
- 修复上传过程中断信号处理和错误传播机制
- 统一进度回调参数结构,改进字节和行数跟踪逻辑
- 优化空行跳过计数和上传结果返回值处理
This commit is contained in:
2026-02-04 16:11:03 +08:00
parent 8415166949
commit 4220284f5a

View File

@@ -417,6 +417,63 @@ export interface StreamUploadResult {
skippedEmptyCount: number; skippedEmptyCount: number;
} }
async function processFileLines(
file: File,
chunkSize: number,
signal: AbortSignal | undefined,
onLine?: (line: string, index: number) => Promise<void> | void,
onProgress?: (currentBytes: number, totalBytes: number, processedLines: number) => void
): Promise<{ lineCount: number; skippedEmptyCount: number }> {
const fileSize = file.size;
let offset = 0;
let buffer = "";
let skippedEmptyCount = 0;
let lineIndex = 0;
while (offset < fileSize) {
if (signal?.aborted) {
throw new Error("Upload cancelled");
}
const end = Math.min(offset + chunkSize, fileSize);
const chunk = file.slice(offset, end);
const text = await readFileAsText(chunk);
const combined = buffer + text;
const lines = combined.split(/\r?\n/);
buffer = lines.pop() || "";
for (const line of lines) {
if (signal?.aborted) {
throw new Error("Upload cancelled");
}
if (!line.trim()) {
skippedEmptyCount++;
continue;
}
const currentIndex = lineIndex;
lineIndex += 1;
if (onLine) {
await onLine(line, currentIndex);
}
}
offset = end;
onProgress?.(offset, fileSize, lineIndex);
}
if (buffer.trim()) {
const currentIndex = lineIndex;
lineIndex += 1;
if (onLine) {
await onLine(buffer, currentIndex);
}
} else if (buffer.length > 0) {
skippedEmptyCount++;
}
return { lineCount: lineIndex, skippedEmptyCount };
}
export async function streamSplitAndUpload( export async function streamSplitAndUpload(
file: File, file: File,
uploadFn: (formData: FormData, config?: { onUploadProgress?: (e: { loaded: number; total: number }) => void }) => Promise<unknown>, uploadFn: (formData: FormData, config?: { onUploadProgress?: (e: { loaded: number; total: number }) => void }) => Promise<unknown>,
@@ -435,11 +492,8 @@ export async function streamSplitAndUpload(
} = options; } = options;
const fileSize = file.size; const fileSize = file.size;
let offset = 0;
let buffer = "";
let uploadedCount = 0; let uploadedCount = 0;
let skippedEmptyCount = 0; let skippedEmptyCount = 0;
let currentBytes = 0;
// 获取文件名基础部分和扩展名 // 获取文件名基础部分和扩展名
const originalFileName = fileNamePrefix || file.name; const originalFileName = fileNamePrefix || file.name;
@@ -447,71 +501,21 @@ export async function streamSplitAndUpload(
const baseName = lastDotIndex > 0 ? originalFileName.slice(0, lastDotIndex) : originalFileName; const baseName = lastDotIndex > 0 ? originalFileName.slice(0, lastDotIndex) : originalFileName;
const fileExtension = lastDotIndex > 0 ? originalFileName.slice(lastDotIndex) : ""; const fileExtension = lastDotIndex > 0 ? originalFileName.slice(lastDotIndex) : "";
// 收集所有需要上传的行 let resolvedReqId = initialReqId;
const pendingLines: { line: string; index: number }[] = []; if (!resolvedReqId) {
let lineIndex = 0; const scanResult = await processFileLines(file, chunkSize, signal);
const totalFileNum = scanResult.lineCount;
// 逐块读取文件并收集非空行 skippedEmptyCount = scanResult.skippedEmptyCount;
while (offset < fileSize) { if (totalFileNum === 0) {
// 检查是否已取消 return {
uploadedCount: 0,
totalBytes: fileSize,
skippedEmptyCount,
};
}
if (signal?.aborted) { if (signal?.aborted) {
throw new Error("Upload cancelled"); throw new Error("Upload cancelled");
} }
const end = Math.min(offset + chunkSize, fileSize);
const chunk = file.slice(offset, end);
const text = await readFileAsText(chunk);
// 将新读取的内容追加到 buffer
const combined = buffer + text;
// 按换行符分割(支持 \n 和 \r\n)
const lines = combined.split(/\r?\n/);
// 保留最后一行(可能不完整)
buffer = lines.pop() || "";
// 收集完整行(跳过空行)
for (const line of lines) {
if (signal?.aborted) {
throw new Error("Upload cancelled");
}
if (!line.trim()) {
skippedEmptyCount++;
continue;
}
pendingLines.push({ line, index: lineIndex++ });
}
currentBytes = end;
offset = end;
// 每处理完一个 chunk,更新进度
onProgress?.(currentBytes, fileSize, uploadedCount);
}
// 处理最后剩余的 buffer(如果文件不以换行符结尾)
if (buffer.trim()) {
pendingLines.push({ line: buffer, index: lineIndex++ });
} else if (buffer.length > 0) {
skippedEmptyCount++;
}
const totalFileNum = pendingLines.length;
if (totalFileNum === 0) {
return {
uploadedCount: 0,
totalBytes: fileSize,
skippedEmptyCount,
};
}
if (signal?.aborted) {
throw new Error("Upload cancelled");
}
let resolvedReqId = initialReqId;
if (!resolvedReqId) {
if (!resolveReqId) { if (!resolveReqId) {
throw new Error("Missing pre-upload request id"); throw new Error("Missing pre-upload request id");
} }
@@ -521,6 +525,9 @@ export async function streamSplitAndUpload(
} }
onReqIdResolved?.(resolvedReqId); onReqIdResolved?.(resolvedReqId);
} }
if (!resolvedReqId) {
throw new Error("Missing pre-upload request id");
}
/** /**
* 上传单行内容 * 上传单行内容
@@ -573,55 +580,65 @@ export async function streamSplitAndUpload(
}); });
} }
/** const inFlight = new Set<Promise<void>>();
* 带并发控制的上传队列执行器 let uploadError: unknown = null;
* 使用任务队列模式,确保不会同时启动所有上传任务 const enqueueUpload = async (line: string, index: number) => {
*/ if (signal?.aborted) {
async function executeUploadsWithConcurrency(): Promise<void> { throw new Error("Upload cancelled");
const lines = [...pendingLines]; }
let currentIndex = 0; if (uploadError) {
let activeCount = 0; throw uploadError;
let resolvedCount = 0; }
const uploadPromise = uploadLine(line, index)
return new Promise((resolve, reject) => { .then(() => {
function tryStartNext() { uploadedCount++;
// 检查是否已完成 })
if (resolvedCount >= lines.length) { .catch((err) => {
if (activeCount === 0) { uploadError = err;
resolve(); });
} inFlight.add(uploadPromise);
return; uploadPromise.finally(() => inFlight.delete(uploadPromise));
} if (inFlight.size >= maxConcurrency) {
await Promise.race(inFlight);
// 启动新的上传任务,直到达到最大并发数 if (uploadError) {
while (activeCount < maxConcurrency && currentIndex < lines.length) { throw uploadError;
const { line, index } = lines[currentIndex++];
activeCount++;
uploadLine(line, index)
.then(() => {
uploadedCount++;
onProgress?.(fileSize, fileSize, uploadedCount);
})
.catch((err) => {
reject(err);
})
.finally(() => {
activeCount--;
resolvedCount++;
// 尝试启动下一个任务
tryStartNext();
});
}
} }
}
};
// 开始执行 let uploadResult: { lineCount: number; skippedEmptyCount: number } | null = null;
tryStartNext(); try {
}); uploadResult = await processFileLines(
file,
chunkSize,
signal,
enqueueUpload,
(currentBytes, totalBytes) => {
onProgress?.(currentBytes, totalBytes, uploadedCount);
}
);
if (uploadError) {
throw uploadError;
}
} finally {
if (inFlight.size > 0) {
await Promise.allSettled(inFlight);
}
} }
// 使用并发控制执行所有上传 if (!uploadResult || (initialReqId && uploadResult.lineCount === 0)) {
await executeUploadsWithConcurrency(); return {
uploadedCount: 0,
totalBytes: fileSize,
skippedEmptyCount: uploadResult?.skippedEmptyCount ?? 0,
};
}
if (!initialReqId) {
skippedEmptyCount = skippedEmptyCount || uploadResult.skippedEmptyCount;
} else {
skippedEmptyCount = uploadResult.skippedEmptyCount;
}
return { return {
uploadedCount, uploadedCount,