UNPKG

file-chunk-uploader

Version:

大文件分片上传

226 lines (218 loc) 7.72 kB
const SparkMd5 = require('spark-md5') /** * * @author lhl * @param {File} file 源文件 * @param {Number} chunkSize 切片的尺寸 * @description 大文件切片上传-上传方法在拿到切片文件后自行处理(整个文件的hash计算需要切片完成) * @example const target = new FileSliceUploader(File); target.init(handler, 'serviceIndexList').then(res => console.log('result:',res)) */ class FileUploader { constructor(file, chunkSize = 2, fixMax = 3) { if (!file) { throw 'the file must be require' return } this.source = file this.sourceSize = file.size this.chunkSize = chunkSize this.chunkBytes = chunkSize * 1024 * 1024 this.chunkCount = this.getSliceNum() this.chunksObj = {} this.fixMax = fixMax // 上传完毕后重新补充上传次数 避免系统异常导致浏览器卡死 this.fixCount = 0 // 当前重传次数 } /** * * @author lhl * @description 获取切片数量 */ getSliceNum () { const countNum = this.source.size / 1024 / 1024 / this.chunkSize const _numAdd = this.source.size % this.chunkSize === 0 ? 1 : 0 return Math.floor(countNum) + _numAdd } /** * @author lhl * @param {Promise} handler 切片的上传方法 * @param {String} serviceListKey 服务器响应的已存在文件的索引组成的列表的key * @param {Number} retryMax 切片上传重试次数 * @description 初始化文件切片 */ async init (handler, serviceListKey = 'indexList', retryMax = 5) { const baseChunks = [] for (let i = 0; i <= this.sourceSize; i += this.chunkBytes) { const chunk = this.source.slice(i, i + this.chunkBytes) baseChunks.push(chunk) } try { const result = await this.createHashChunks(baseChunks) this.chunksObj = result const target = await this.deepChunks(handler, serviceListKey, retryMax) return target } catch (err) { throw err } } /** * * @author lhl * @param {Blob[]} baseChunks 文件blob切片数组 * @description 创建文件切片 */ createHashChunks (baseChunks) { const _that = this return new Promise((resolve) => { const spark = new SparkMd5() const chunks = [] const total = this.chunkCount let chunkIndex = 0 const _read = () => { if (chunkIndex >= total) { resolve({ chunks, total, size: this.sourceSize, hash: spark.end() }) return } const reader = new FileReader() const blob = baseChunks[chunkIndex] reader.onload = function (e) { const bytes = e.target.result chunks.push({ file: bytes, index: chunkIndex }) spark.append(bytes) chunkIndex++ _read(chunkIndex) } reader.readAsArrayBuffer(blob) } _read(0) }) } /** * * @author lhl * @param {Function} handler 回调 要求必须是promise 建议包含成功和失败的处理 需要接收上传文件参数 * @param {String} serviceListKey 服务器响应的已存在文件的索引组成的列表的key * @param {Number} retryMax 回调报错重试次数上限 * @description 内置递归调用回调函数的方法 */ deepChunks (handler, serviceListKey, retryMax) { return new Promise((resolve, reject) => { const _that = this if (!this.chunksObj) { reject('this.chunksObj -- This parameter is necessary') return } if (!this.chunksObj.chunks || !this.chunksObj.chunks[0] || !Object.prototype.toString.call(this.chunksObj.chunks[0].file).includes('ArrayBuffer')) { reject('chunks -- the param type must be ArrayBuffer[]') return } if (!handler) { reject('handler -- the function type must return a Promise') return } let deepIndex = 0 // 当前递归次数 let retryCount = 0 // 回调报错重试次数 const _deep = (chunksObj = this.chunksObj) => { let chunkIndex = chunksObj.chunks[deepIndex].index // 当前分片索引 console.log('开始上传第' + chunkIndex + '个切片,递归次数:' + deepIndex) let lastChunk = deepIndex == chunksObj.chunks.length - 1 handler({ chunk: chunksObj.chunks[deepIndex], hash: chunksObj.hash, total: chunksObj.total }).then(res => { if (!res) { reject('handler: Promise未返回任何有效信息' + `response: ${res}`) return } if (!res[serviceListKey]) { reject(`响应结果未返回已上传成功的索引数组,example: new FileSliceUploader(handler, "${serviceListKey}")`) return } retryCount = 0 // 文件存在前端不处理 后台直接跳过 if (lastChunk) { if (res[serviceListKey].length !== _that.chunkCount) { if (_that.fixCount < _that.fixMax) { console.log('需要检查完整性,正在重新上传。当前重传次数:' + _that.fixCount) _that.fixCount++ const newChunksObj = _that.getChunksObjByServiceIndexList(res[serviceListKey]) deepIndex = 0 _deep(newChunksObj) } else { reject(`检查文件完整性出现问题(超出重传次数上限${_that.fixCount}),请稍后再试`) return } } else { // 不需要检查完整性 console.log('不需要检查完整性' + lastChunk ? '最终项' : '不是最终项') resolve({ message: '上传成功', status: 'success', fileInfo: { hash: chunksObj.hash, chunkTotal: chunksObj.total, chunkSize: chunksObj.size } }) } } else { deepIndex++ _deep(chunksObj) } }).catch(error => { retryCount++ if (retryCount >= retryMax) { throw `上传失败,超出上传重试次数上限(${retryMax})` } console.log(`第${retryCount}次重试,重试上限:${retryMax}`) _deep(chunksObj) }) } _deep() }) } /** * * @author lhl * @param {Number[]} serviceIndexList 服务器返回的分片索引列表 * @description 根据分片索引的连续性获取新的分片文件对象 */ getChunksObjByServiceIndexList (serviceIndexList) { const result = { hash: this.chunksObj.hash, total: this.chunksObj.total, size: this.chunksObj.size, chunks: [] } const lostList = this.getArrayLostItems(serviceIndexList, this.chunkCount) for (let n of lostList) { result.chunks.push(this.chunksObj.chunks[n]) } return result } /** * * @param {Number[]} list 当前数组 * @param {Number} orginSize 目标数组长度 * @returns Number[] 缺失项 * @description 判断从0开始自增的有序数组的连续性并取出对应的缺失项数组 */ getArrayLostItems (list, orginSize) { const result = [] for (let n of [...new Array(orginSize).keys()]) { if (!list.includes(n)) { result.push(n) } } return result } } module.exports = FileUploader