file-chunk-uploader
Version:
大文件分片上传
226 lines (218 loc) • 7.72 kB
JavaScript
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