大文件分片上传、断点续传,MD5判断是否上传

安装依赖

pnpm install spark-md5
pnpm install @types/spark-md5 -D

分片上传、断点续传函数定义

CHUNK_SIZE = 10 * 1024 * 1024; // 10M
getFileMD5(file: File) {
    const fileReader = new FileReader();
    // 获取文件分片对象(注意它的兼容性,在不同浏览器的写法不同)
    const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
    // 取第一个分片计算
    const start = 0;
    const end = this.CHUNK_SIZE >= file.size ? file.size : this.CHUNK_SIZE;
    fileReader.readAsBinaryString(blobSlice.call(file, start, end));

    return new Promise((resolve, reject) => {
        fileReader.onload = (e: any) => {
            resolve(SparkMD5.hashBinary(e.target.result, false));
        };
        fileReader.onerror = function () {
            message.error('文件读取出错,请检查该文件!');
            reject(new Error('文件读取出错,请检查该文件!'));
        };
    });
}

checkUploadByMD5(md5: string) {
    return this.get(`xxxxxxxxxx/check-upload?identifier=${md5}`);
}

// 切片上传大文件
createBigFile(file: File, onProcess: any = null): any {
    return new Promise(async (resolve, reject) => {
        if (!file) {
            reject(new Error('文件为空,请检查!'));
        }
        const md5 = await this.getFileMD5(file);
        let uploadedChunks = []; // 已上传分片
        try {
            const checkResult: any = await this.checkUploadByMD5(`${md5}`);
            if (checkResult.uploaded && Object.keys(checkResult.commonFile || {}).length) {
                // 文件已上传
                resolve(checkResult.commonFile);
                return;
            }
            uploadedChunks = checkResult.uploadedChunks || []; // 已上传分片
        } catch (err) {
            // 检查失败,当作未上传
            console.error(err);
        }
        // 创建切片
        const fileChunks = [];
        let index = 0; // 切片序号
        for (let cur = 0; cur < file.size; cur += this.CHUNK_SIZE) {
            if (!uploadedChunks.includes(++index)) {
                // 未上传过此分片
                fileChunks.push({
                    chunkNumber: index,
                    chunk: file.slice(cur, cur + this.CHUNK_SIZE),
                });
            }
        }
        const totalChunks = index; // 总切片数
        // 控制并发和断点续传
        let success = uploadedChunks.length; // 成功的数量
        let percent = (success / totalChunks ?? 0) * 100;
        onProcess && onProcess({ percent: percent.toFixed(2) });
        const processDetail: number[] = [];
        let consecutiveFailure = 0; // 连续失败次数
        const uploadFileChunks = async (list: { [key: string]: any }[]) => {
            const pool: any[] = []; // 并发池
            const max = 3; // 最大并发量
            const failureMax = 3; // 可接受最大连续失败数
            let finish = 0; // 完成的数量
            const failList: { [key: string]: any }[] = []; // 失败的列表
            for (let i = 0; i < list.length; i++) {
            	if (consecutiveFailure >= failureMax) {
                     // 连续failureMax次upload失败,停止请求
                     message.error('文件上传失败,请稍后再试!');
                     reject(new Error('文件上传失败,请稍后再试!'));
                     return;
                }
                const item = list[i];
                const chunkData = {
                    chunkNumber: item.chunkNumber,
                    totalChunks,
                    chunkSize: item.chunk.size,
                    totalSize: file.size,
                    identifier: md5,
                    filename: file.name,
                    file: item.chunk,
                };
                // 上传切片
                const task = this.postFormLarge('xxxxxxx/upload', chunkData, {
                    onUploadProgress: (info) => {
                        if (onProcess) {
                            const poolIndex = pool.findIndex((item) => item === task);
                            const progress = info?.progress ?? 0;
                            processDetail[poolIndex] = progress === 1 ? 0.99 : progress; // 避免还未成功进度拉满
                            const percentNew = (processDetail.reduce((pre, cur) => pre + cur, success) / totalChunks) * 100;
                            if (percentNew > percent) {
                                percent = percentNew;
                                onProcess({ percent: percentNew.toFixed(2) });
                            }
                        }
                    },
                });
                task.then((taskResult: any) => {
                    if (taskResult && taskResult.uploadFlag) {
                        // 请求结束后将该Promise任务从并发池中移除
                        const poolIndex = pool.findIndex((item) => item === task);
                        pool.splice(poolIndex, 1);
                        success++;
                        consecutiveFailure = 0;
                        if (onProcess) {
                            try {
                                processDetail.splice(poolIndex, 1);
                                const percentNew = (processDetail.reduce((pre, cur) => pre + cur, success) / totalChunks) * 100;
                                if (percentNew > percent) {
                                    percent = percentNew;
                                    onProcess({ percent: percentNew.toFixed(2) });
                                }
                            } catch (err) {
                                // 在这catch避免重复上传已上传分片
                                console.error(err);
                            }
                        }
                    } else {
                    	consecutiveFailure++;
                        failList.push(item);
                    }
                    if (Object.keys(taskResult.commonFile || {}).length) {
                        // 全部分片都上传成功后后端返回文件信息
                        resolve(taskResult.commonFile);
                    }
                })
                    .catch(() => {
                    	consecutiveFailure++;
                        failList.push(item);
                    })
                    .finally(() => {
                        finish++;
                        // 所有请求都请求完成
                        if (finish === list.length && failList.length) {
                            uploadFileChunks(failList);
                        }
                    });
                pool.push(task);
                processDetail.push(0);
                if (pool.length === max) {
                    // 每当并发池跑完一个任务,就再塞入一个任务,避免内存泄漏
                    await Promise.race(pool);
                }
            }
        };
        uploadFileChunks(fileChunks);
    });
}

调用

const commonFile = await createBigFile(file, (e: any) => {
    uploadingObjs.value.percent = e?.percent || 0;
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值