UNPKG

parallel-file-uploader

Version:

高性能并行文件上传工具,支持大文件分片上传、断点续传、Web Worker多线程处理

239 lines 8.9 kB
import { ChunkStatusEnum } from '../type'; /** * 分片管理器 * 负责分片队列管理、断点续传、分片状态管理等 */ export class ChunkManager { constructor(chunkSize = 1024 * 1024 * 5) { this.chunkQueue = new Map(); this.uploadedChunks = new Map(); this.pendingChunks = new Map(); this.chunkSize = chunkSize; } /** * 🔧 获取分片大小 */ getChunkSize() { return this.chunkSize; } /** * 准备分片队列 */ prepareChunkQueue(fileInfo) { const { fileId, fileSize } = fileInfo; const totalChunks = Math.ceil(fileSize / this.chunkSize); const chunks = []; // 创建已上传分片集合 this.uploadedChunks.set(fileId, new Set()); this.pendingChunks.set(fileId, new Set()); for (let i = 0; i < totalChunks; i++) { const partNumber = i + 1; const start = i * this.chunkSize; const end = i === totalChunks - 1 ? fileSize : start + this.chunkSize; const currentChunkSize = end - start; chunks.push({ partNumber, start, end, partSize: currentChunkSize, status: ChunkStatusEnum.waiting, retryCount: 0, }); } // 确保分片按partNumber排序 chunks.sort((a, b) => a.partNumber - b.partNumber); this.chunkQueue.set(fileId, chunks); // 更新文件信息中的分片总数 fileInfo.totalChunks = totalChunks; } /** * 获取下一个待上传的分片 */ getNextChunk(fileId) { const chunks = this.chunkQueue.get(fileId) || []; return chunks.shift(); } /** * 获取待上传分片数量 */ getRemainingChunkCount(fileId) { const chunks = this.chunkQueue.get(fileId) || []; return chunks.length; } /** * 获取待处理分片数量 */ getPendingChunkCount(fileId) { const pendingChunks = this.pendingChunks.get(fileId) || new Set(); return pendingChunks.size; } /** * 添加分片到待处理集合 */ addToPending(fileId, partNumber) { const pendingChunks = this.pendingChunks.get(fileId) || new Set(); pendingChunks.add(partNumber); this.pendingChunks.set(fileId, pendingChunks); } /** * 从待处理集合移除分片 */ removeFromPending(fileId, partNumber) { const pendingChunks = this.pendingChunks.get(fileId); if (pendingChunks) { pendingChunks.delete(partNumber); } } /** * 标记分片为已完成 */ markChunkCompleted(fileId, partNumber) { const uploadedChunks = this.uploadedChunks.get(fileId) || new Set(); uploadedChunks.add(partNumber); this.uploadedChunks.set(fileId, uploadedChunks); this.removeFromPending(fileId, partNumber); } /** * 重新加入分片到队列(用于重试) */ requeueChunk(fileId, chunkInfo) { const chunks = this.chunkQueue.get(fileId) || []; chunkInfo.retryCount = (chunkInfo.retryCount || 0) + 1; chunks.unshift(chunkInfo); this.chunkQueue.set(fileId, chunks); this.removeFromPending(fileId, chunkInfo.partNumber); } /** * 计算已上传大小 */ calculateUploadedSize(fileInfo) { const { fileId, fileSize } = fileInfo; const uploadedChunks = this.uploadedChunks.get(fileId); if (!uploadedChunks) return 0; let uploadedSize = 0; for (const partNumber of uploadedChunks) { // 计算分片大小 const start = (partNumber - 1) * this.chunkSize; const end = Math.min(start + this.chunkSize, fileSize); uploadedSize += end - start; } return uploadedSize; } /** * 🔧 计算指定分片的预期大小 */ calculateExpectedPartSize(partNumber, fileSize) { const totalChunks = Math.ceil(fileSize / this.chunkSize); const isLastChunk = partNumber === totalChunks; if (isLastChunk) { // 最后一个分片的大小 = 文件总大小 - 前面所有分片的大小 const remainingSize = fileSize - (partNumber - 1) * this.chunkSize; return remainingSize > 0 ? remainingSize : this.chunkSize; } else { // 普通分片使用标准分片大小 return this.chunkSize; } } /** * 🔧 验证和修复分片数据 */ validateAndFixPartInfo(part, fileSize) { // 如果没有 partSize 或 partSize 无效,计算预期的分片大小 if (!part.partSize || part.partSize <= 0) { const expectedSize = this.calculateExpectedPartSize(part.partNumber, fileSize); console.log(`🔧 修复分片 ${part.partNumber} 的 partSize: ${part.partSize || 'undefined'} -> ${expectedSize}`); return { ...part, partSize: expectedSize }; } return part; } /** * 从已上传的分片恢复上传 - 🔧 增强兼容性版本 */ resumeFromExistingParts(fileInfo, existingParts) { const { fileId, fileSize } = fileInfo; const uploadedChunks = this.uploadedChunks.get(fileId); if (!existingParts || existingParts.length === 0) { console.log('📋 没有已上传的分片数据'); return; } // 🔧 修复和验证分片数据 const validatedParts = existingParts.map(part => this.validateAndFixPartInfo(part, fileSize)); // 🔧 更宽松的验证逻辑 - 只检查 etag 异常 const hasAllSameEtag = new Set(validatedParts.map(part => part.etag)).size === 1 && validatedParts.length > 1; if (hasAllSameEtag) { console.warn('检测到所有分片具有相同的 etag,可能存在异常,将重新上传所有分片', { hasAllSameEtag, parts: validatedParts, }); return; } // 🔧 额外的合理性检查 const maxValidPartNumber = Math.ceil(fileSize / this.chunkSize); const invalidParts = validatedParts.filter(part => part.partNumber < 1 || part.partNumber > maxValidPartNumber); if (invalidParts.length > 0) { console.warn('检测到无效的分片编号,将重新上传所有分片', { invalidParts, maxValidPartNumber, fileSize, chunkSize: this.chunkSize }); return; } if (uploadedChunks) { console.log(`🔄 恢复 ${validatedParts.length} 个已上传分片的断点续传`); // 标记已上传的分片 for (const part of validatedParts) { uploadedChunks.add(part.partNumber); // 从队列中移除已上传的分片 const chunks = this.chunkQueue.get(fileId) || []; const index = chunks.findIndex(c => c.partNumber === part.partNumber); if (index !== -1) { console.log(`✅ 跳过已上传分片 #${part.partNumber} (大小: ${part.partSize} bytes)`); chunks.splice(index, 1); } } // 🔧 输出断点续传统计信息 const remainingChunks = this.chunkQueue.get(fileId) || []; console.log(`📊 断点续传统计: 已完成 ${validatedParts.length} 个分片,剩余 ${remainingChunks.length} 个分片`); } } /** * 检查是否所有分片都已完成 */ isAllChunksCompleted(fileId) { const chunks = this.chunkQueue.get(fileId) || []; const pendingChunks = this.pendingChunks.get(fileId) || new Set(); return chunks.length === 0 && pendingChunks.size === 0; } /** * 清理文件相关的分片数据 */ cleanup(fileId) { this.chunkQueue.delete(fileId); this.uploadedChunks.delete(fileId); this.pendingChunks.delete(fileId); } /** * 获取分片统计信息 */ getChunkStats(fileId) { const chunks = this.chunkQueue.get(fileId) || []; const uploadedChunks = this.uploadedChunks.get(fileId) || new Set(); const pendingChunks = this.pendingChunks.get(fileId) || new Set(); // 总分片数需要从文件信息中获取,这里只能估算 const total = chunks.length + uploadedChunks.size + pendingChunks.size; return { total, completed: uploadedChunks.size, pending: pendingChunks.size, remaining: chunks.length, }; } } //# sourceMappingURL=ChunkManager.js.map