parallel-file-uploader
Version:
高性能并行文件上传工具,支持大文件分片上传、断点续传、Web Worker多线程处理
287 lines • 9.57 kB
JavaScript
import { v4 as uuid } from 'uuid';
import { UploadStepEnum, ErrorType, UploaderError } from '../type';
/**
* 文件大小格式化工具
*/
function formatFileSize(bytes) {
if (bytes === 0)
return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* 文件管理器
* 负责文件队列管理、文件验证、状态管理等
*/
export class FileManager {
constructor(options = {}) {
this.fileQueue = [];
this.activeFiles = new Map();
this.maxFileSize = options.maxFileSize;
// 🔧 处理文件类型配置,自动过滤无效配置
this.allowedFileTypes = this.processAllowedFileTypes(options.allowedFileTypes);
}
/**
* 🔧 处理和验证允许的文件类型配置
*/
processAllowedFileTypes(types) {
if (!types || types.length === 0) {
return undefined;
}
// 处理通配符和无效配置
const processedTypes = types.filter((type) => {
// 支持 "*" 通配符,表示允许所有文件类型
if (type === '*') {
console.log('📁 检测到通配符 "*",将允许所有文件类型');
return false; // 返回 undefined 表示不限制
}
// 过滤空字符串
if (!type || type.trim() === '') {
console.warn('⚠️ 检测到空的文件类型配置,已忽略');
return false;
}
return true;
});
// 如果包含 "*" 或处理后为空,表示不限制文件类型
if (types.includes('*') || processedTypes.length === 0) {
console.log('📁 文件类型验证已禁用,允许所有文件类型');
return undefined;
}
console.log('📁 有效的文件类型限制:', processedTypes);
return processedTypes;
}
/**
* 添加文件到队列
*/
addFiles(files) {
const fileArray = Array.from(files);
const addedFiles = [];
for (const file of fileArray) {
try {
this.validateFile(file);
const fileId = uuid();
const fileInfo = {
fileId,
fileName: file.name,
fileSize: file.size,
uploadedSize: 0,
progress: 0,
status: UploadStepEnum.beforeUpload,
file,
mimeType: file.type,
lastUpdated: Date.now(),
};
this.fileQueue.push(fileInfo);
addedFiles.push(fileInfo);
}
catch (error) {
// 🔧 增强错误信息,包含文件详情
if (error instanceof UploaderError) {
// 添加文件详情到错误信息
error.message = `文件 "${file.name}" (${formatFileSize(file.size)}) 验证失败: ${error.message}`;
}
throw error;
}
}
return addedFiles;
}
/**
* 🔧 增强的文件验证方法
*/
validateFile(file) {
// 🔧 基础文件验证
if (!file || !file.name) {
throw new UploaderError('无效的文件对象', ErrorType.FILE_TYPE_NOT_ALLOWED);
}
// 🔧 文件大小为0的检查
if (file.size === 0) {
throw new UploaderError('文件大小为0,无法上传空文件', ErrorType.FILE_TOO_LARGE);
}
// 验证文件类型
if (this.allowedFileTypes && this.allowedFileTypes.length > 0) {
const fileType = file.type || this.getFileExtension(file.name);
if (!this.isFileTypeAllowed(fileType, file.name)) {
const supportedTypes = this.getSupportedTypesDescription();
throw new UploaderError(`不支持的文件类型: ${fileType || '未知类型'}。支持的类型: ${supportedTypes}`, ErrorType.FILE_TYPE_NOT_ALLOWED);
}
}
// 验证文件大小
if (this.maxFileSize && file.size > this.maxFileSize) {
throw new UploaderError(`文件大小超出限制: ${formatFileSize(file.size)} > ${formatFileSize(this.maxFileSize)}`, ErrorType.FILE_TOO_LARGE);
}
}
/**
* 从队列中获取下一个文件
*/
getNextFile() {
return this.fileQueue.shift();
}
/**
* 添加文件到活动列表
*/
addToActive(fileInfo) {
this.activeFiles.set(fileInfo.fileId, fileInfo);
}
/**
* 从活动列表中移除文件
*/
removeFromActive(fileId) {
this.activeFiles.delete(fileId);
}
/**
* 获取活动文件
*/
getActiveFile(fileId) {
return this.activeFiles.get(fileId);
}
/**
* 获取所有活动文件
*/
getAllActiveFiles() {
return Array.from(this.activeFiles.values());
}
/**
* 更新文件状态
*/
updateFileStatus(fileId, status) {
const fileInfo = this.activeFiles.get(fileId);
if (fileInfo) {
fileInfo.status = status;
fileInfo.lastUpdated = Date.now();
}
}
/**
* 更新文件进度
*/
updateFileProgress(fileId, uploadedSize) {
const fileInfo = this.activeFiles.get(fileId);
if (fileInfo) {
fileInfo.uploadedSize = uploadedSize;
fileInfo.progress = Math.round((uploadedSize / fileInfo.fileSize) * 100);
fileInfo.lastUpdated = Date.now();
}
}
/**
* 获取队列长度
*/
getQueueLength() {
return this.fileQueue.length;
}
/**
* 获取活动文件数量
*/
getActiveCount() {
return this.activeFiles.size;
}
/**
* 清空队列
*/
clearQueue() {
this.fileQueue = [];
}
/**
* 清空活动文件
*/
clearActive() {
this.activeFiles.clear();
}
/**
* 获取统计信息
*/
getStats() {
const queued = this.fileQueue.length;
let active = 0;
let completed = 0;
let failed = 0;
let paused = 0;
for (const fileInfo of this.activeFiles.values()) {
switch (fileInfo.status) {
case UploadStepEnum.upload:
active++;
break;
case UploadStepEnum.complete:
completed++;
break;
case UploadStepEnum.error:
failed++;
break;
case UploadStepEnum.pause:
paused++;
break;
}
}
return { queued, active, completed, failed, paused };
}
/**
* 获取文件扩展名
*/
getFileExtension(filename) {
const lastDotIndex = filename.lastIndexOf('.');
return lastDotIndex !== -1 ? filename.substring(lastDotIndex + 1).toLowerCase() : '';
}
/**
* 🔧 增强的文件类型检查方法
*/
isFileTypeAllowed(fileType, fileName) {
if (!this.allowedFileTypes || this.allowedFileTypes.length === 0) {
return true;
}
// 获取文件扩展名
const extension = this.getFileExtension(fileName);
return this.allowedFileTypes.some((type) => {
// 🔧 支持 "*" 通配符
if (type === '*') {
return true;
}
// 处理MIME类型
if (type.includes('/')) {
// 完全匹配 (image/png)
if (type === fileType)
return true;
// 通配符匹配 (image/*)
if (type.endsWith('/*') && fileType.startsWith(type.split('/*')[0]))
return true;
return false;
}
// 处理扩展名 - 支持多种格式
const normalizedType = type.toLowerCase();
const normalizedExt = extension.toLowerCase();
// 支持 .pdf, pdf, PDF 等格式
if (normalizedType === `.${normalizedExt}` ||
normalizedType === normalizedExt ||
normalizedType === `.${normalizedExt.toLowerCase()}`) {
return true;
}
return false;
});
}
/**
* 🔧 获取支持的文件类型描述
*/
getSupportedTypesDescription() {
if (!this.allowedFileTypes || this.allowedFileTypes.length === 0) {
return '所有文件类型';
}
if (this.allowedFileTypes.includes('*')) {
return '所有文件类型';
}
// 限制显示的类型数量,避免过长
if (this.allowedFileTypes.length <= 5) {
return this.allowedFileTypes.join(', ');
}
return `${this.allowedFileTypes.slice(0, 3).join(', ')} 等 ${this.allowedFileTypes.length} 种类型`;
}
/**
* 🔧 获取当前配置信息(用于调试)
*/
getConfiguration() {
return {
maxFileSize: this.maxFileSize ? formatFileSize(this.maxFileSize) : undefined,
allowedFileTypes: this.allowedFileTypes,
supportedTypesDescription: this.getSupportedTypesDescription()
};
}
}
//# sourceMappingURL=FileManager.js.map