src/tunnel.js
import isEmpty from 'lodash/isEmpty'
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import isNumber from 'lodash/isNumber'
import isInteger from 'lodash/isInteger'
import isFunction from 'lodash/isFunction'
import isPlainObject from 'lodash/isPlainObject'
import map from 'lodash/map'
import times from 'lodash/times'
import forEach from 'lodash/forEach'
import assign from 'lodash/assign'
import sortBy from 'lodash/sortBy'
import defaultsDeep from 'lodash/defaultsDeep'
import waterfall from 'async/waterfall'
import parallel from 'async/parallel'
import * as http from './request'
import * as CONFIG from './config'
import { File } from './file'
import { QiniupEvent } from './event'
import { isNumeric } from './utils'
/**
* 七牛通道类
* 支持普通文件上传,适合图片文本等小文件上传
* 支持 Base64 文件上传,Base64 字符串长度并不等于文件大小,可参考:https://en.wikipedia.org/wiki/Base64
* 支持断点续传,缓存上传了的块与片保存在本地缓存中,若清除本地缓存则不能保证能继续上次的断点
* 块大小,每块均为4MB(1024*1024*4),最后一块大小不超过4MB
* 所有接口均参考七牛官方文档,一切均以七牛官方文档为准
* @class
*/
export class Tunnel {
/**
* 七牛通道类默认配置
* @type {Object}
* @property {Boolean} defaultSettings.useHttps 是否使用 Https 进行上传
* @property {Boolean} defaultSettings.cache 是否缓存
* @property {Integer} defaultSettings.maxConnect 最大连接数
* @property {Integer} defaultSettings.blockSize 分块大小
* @property {Integer} defaultSettings.blockSize 分片大小
* @property {Integer} defaultSettings.maxBlockTasks 最大分块任务数, 若文件巨大, 可能分块的时候会卡死浏览器, 因此设置最大分块数
*/
static defaultSettings = {
useHttps: typeof window === 'undefined' ? false : window.location.protocol,
cache: false,
maxConnect: 4,
blockSize: 4 * CONFIG.M,
chunkSize: 1 * CONFIG.M,
maxFileSize: 1 * CONFIG.G,
maxBlockTasks: 2000
}
/**
* 创建通道类对象
* @param {Object} [options] 配置,可以参考{@link Tunnel.defaultSettings}
* @param {Object} [options.useHttps=true] 是否使用 Https 进行上传
* @param {Boolean} [options.cache=false] 是否缓存
* @param {Integer} [options.maxConnect=4] 最大连接数
* @param {Integer} [options.blockSize=4 * M] 分块大小
* @param {Integer} [options.chunkSize=1 * M] 分片大小
* @return {Tunnel}
*/
constructor (options, request = http) {
/**
* 配置
* @type {Object}
*/
this.settings = defaultsDeep({}, options, this.constructor.defaultSettings)
/**
* 令牌
* @type {String}
*/
this.token = ''
/**
* 令牌过期时间
* @type {Integer}
*/
this.tokenExpire = 0
/**
* 设置 request
* @type {Object}
*/
this.request = request
}
_execTokenGetter (getter, callback) {
if (!isFunction(getter)) {
throw new TypeError('Getter is not a fucntion')
}
if (this.tokenExpire > Date.now() && this.token) {
return callback(null, this.token)
}
getter((error, token) => {
if (error) {
return callback(error)
}
if (isPlainObject(token)) {
this.token = token.token
this.tokenExpire = isNumeric(token.expire) ? token.expire * 1 : 0
} else {
this.token = token
this.tokenExpire = 0
}
return callback(null, this.token)
})
}
/**
* 第三方资源抓取
*
* @see https://developer.qiniu.com/kodo/api/1263/fetch
*
* @param {String} file 远程文件
* @param {Object} [params={}] 上传参数
* @param {Object} params.token 七牛令牌
* @param {Object} [params.key] 如果没有指定则: 如果 uptoken.SaveKey 存在则基于 SaveKey 生产 key,否则用 hash 值作 key。EncodedKey 需要经过 base64 编码
* @param {Object} [params.bucket] 指定的存储区域 https://developer.qiniu.com/kodo/api/3966/bucket-image-source
* @param {Object} [options={}] 配置
* @param {Function} [options.tokenGetter] 获取 Token 拦截器
* @param {Boolean} [options.useHttps] 是否使用 Https 进行上传
* @param {String} [options.host] 七牛HOST https://developer.qiniu.com/kodo/manual/1671/region-endpoint
* @param {String} [options.tokenPrefix] 令牌前缀
* @param {Function} callback 回调函数
* @memberof Tunnel
*/
fetch (file, params = {}, options = {}, callback) {
if (!isFunction(callback)) {
throw new TypeError('Callback is not provied or not be a function')
}
if (!CONFIG.REMOTE_FILE_URL_REGEXP.test(file)) {
callback(new TypeError('File is not provided or invalid remote source url'))
return
}
let { token } = params
let { tokenGetter } = options
if (!(isString(token) && token)) {
if (!isFunction(tokenGetter)) {
callback(new TypeError('Token is not provided'))
return
}
return this._execTokenGetter(tokenGetter, (error, token) => {
if (error) {
callback(error)
return
}
return this.fetch(file, assign({ token }, params), options, callback)
})
}
options = defaultsDeep(options, this.settings)
let { host, useHttps, tokenPrefix } = options
host = host || (useHttps ? CONFIG.QINIU_UPLOAD_HTTPS_URL : CONFIG.QINIU_UPLOAD_HTTP_URL)
let { bucket, key } = params
let url = `${useHttps ? 'https:' : 'http:'}//${host}/fetch/${window.btoa(file)}/to/${bucket}:${key}`
let headers = {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `${tokenPrefix || 'UpToken'} ${token}`
}
return this.request.post(url, null, assign({ headers }, options), callback)
}
/**
* 上传文件
* 普通文件上传,适合小文件
*
* @param {File|Blob} file 文件
* @param {Object} [params={}] 上传参数
* @param {Object} params.token 七牛令牌
* @param {Object} [params.key] 如果没有指定则:如果 uptoken.SaveKey 存在则基于 SaveKey 生产 key,否则用 hash 值作 key。EncodedKey 需要经过 base64 编码
* @param {Object} [options={}] 上传配置
* @param {Function} [options.tokenGetter] 获取 Token 拦截器
* @param {Boolean} [options.useHttps] 是否使用 Https 进行上传
* @param {String} [options.host] 七牛HOST https://developer.qiniu.com/kodo/manual/1671/region-endpoint
* @param {String} [options.tokenPrefix] 令牌前缀
* @param {Function} [options.progress] 上传进度
* @param {Function} callback 回调
* @returns {Object} state
* @returns {XMLHttpsRequest} state.xhr AJAX 对象
* @returns {Function} state.cancel 取消函数
*/
upload (file, params = {}, options = {}, callback) {
if (!isFunction(callback)) {
throw new TypeError('Callback is not provied or not be a function')
}
if (!(file instanceof File || file instanceof window.Blob)) {
callback(new TypeError('File is not provided or not instanceof File'))
return
}
let { token } = params
let { tokenGetter } = options
if (!(isString(token) && token)) {
if (!isFunction(tokenGetter)) {
callback(new TypeError('Token is not provided'))
return
}
return this._execTokenGetter(tokenGetter, (error, token) => {
if (error) {
callback(error)
return
}
return this.upload(file, assign({ token }, params), options, callback)
})
}
options = defaultsDeep(options, this.settings)
let { host, useHttps, tokenPrefix } = options
host = host || (useHttps ? CONFIG.QINIU_UPLOAD_HTTPS_URL : CONFIG.QINIU_UPLOAD_HTTP_URL)
let url = `${useHttps ? 'https:' : 'http:'}//${host}`
let datas = assign({ file: file.file }, params)
let headers = {
Authorization: `${tokenPrefix || 'UpToken'} ${token}`
}
return this.request.upload(url, datas, assign({ headers }, options), callback)
}
/**
* 上传 base64 资源
* @see https://developer.qiniu.com/kodo/kb/1326/how-to-upload-photos-to-seven-niuyun-base64-code
*
* @param {string} content base64文件数据
* @param {Object} params 上传参数
* @param {Object} params.token 七牛令牌
* @param {Integer} [params.size=-1] 文件大小,-1为自动获取
* @param {Object} [params.key] 如果没有指定则:如果 uptoken.SaveKey 存在则基于 SaveKey 生产 key,否则用 hash 值作 key。EncodedKey 需要经过 base64 编码
* @param {Object} [params.mimeType] 文件的 MIME 类型,默认是 application/octet-stream
* @param {Object} [params.crc32] 文件内容的 crc32 校验值,不指定则不进行校验
* @param {Object} [params.userVars]
* @param {Object} [options={}] 上传配置
* @param {Function} [options.tokenGetter] 获取 Token 拦截器
* @param {Boolean} [options.useHttps] 是否使用 Https 进行上传
* @param {String} [options.host] 七牛HOST https://developer.qiniu.com/kodo/manual/1671/region-endpoint
* @param {String} [options.tokenPrefix] 令牌前缀
* @param {Function} [options.progress] 上传进度
* @param {Function} callback 回调
* @returns {Object} state
* @returns {XMLHttpsRequest} state.xhr AJAX 对象
* @returns {Function} state.cancel 取消函数
*/
upb64 (content, params = { size: -1 }, options = {}, callback) {
if (!isFunction(callback)) {
throw new TypeError('Callback is not provied or not be a function')
}
if (isEmpty(content) || !CONFIG.BASE64_REGEXP.exec(content)) {
callback(new TypeError('Content is not provided or not a valid base64 string'))
return
}
let { token } = params
let { tokenGetter } = options
if (!(isString(token) && token)) {
if (!isFunction(tokenGetter)) {
callback(new TypeError('Token is not provided'))
return
}
return this._execTokenGetter(tokenGetter, (error, token) => {
if (error) {
callback(error)
return
}
return this.upb64(content, assign({ token }, params), options, callback)
})
}
if (!(isNumber(params.size) && isInteger(params.size) && params.size > 0)) {
params.size = -1
}
options = defaultsDeep(options, this.settings)
let { host, useHttps, tokenPrefix } = options
host = host || (useHttps ? CONFIG.QINIU_UPLOAD_HTTPS_URL : CONFIG.QINIU_UPLOAD_HTTP_URL)
let { size, key, mimeType, crc32, userVars } = params
let url = `${useHttps ? 'https:' : 'http:'}//${host}/${size}`
if (isString(key) && key) {
url += `/key/${encodeURIComponent(key)}`
}
if (isString(mimeType) && mimeType) {
url += `/mimeType/${encodeURIComponent(mimeType)}`
}
if (isString(crc32) && crc32) {
url += `/crc32/${encodeURIComponent(crc32)}`
}
if (isString(userVars) && userVars) {
url += `/x:user-var/${encodeURIComponent(userVars)}`
}
let datas = content.replace(CONFIG.BASE64_REGEXP, '')
let headers = {
'Content-Type': 'application/octet-stream',
Authorization: `${tokenPrefix || 'UpToken'} ${token}`
}
return this.request.upload(url, datas, assign({ headers }, options), callback)
}
/**
* 上传块:
* 块只是一个虚拟的概念,块表示多个分片的集合的一个统称
* 1. 将文件分成若干块,可以并发进行上传,而块中拥有多个分片
* 每个块上传的开始必须将第一个分片同时上传
* 2. 上传完之后会返回第一个分片的哈希值(ctx),第二个分片必
* 须同时上传第一个分片的哈希值
*
* @see https://developer.qiniu.com/kodo/api/1286/mkblk
*
* @param {Blob} block 块
* @param {Object} params 上传参数
* @param {Object} params.token 七牛令牌
* @param {Object} [options={}] 上传配置
* @param {Function} [options.tokenGetter] 获取 Token 拦截器
* @param {Boolean} [options.useHttps] 是否使用 Https 进行上传
* @param {String} [options.host] 七牛HOST https://developer.qiniu.com/kodo/manual/1671/region-endpoint
* @param {String} [options.tokenPrefix] 令牌前缀
* @param {number} [options.chunkSize] 设置每个分片的大小
* @param {Function} [options.progress] 上传进度
* @param {mkblkCallback} callback 上传之后执行的回调函数
* @returns {Object} state
* @returns {XMLHttpsRequest} state.xhr AJAX 对象
* @returns {Function} state.cancel 取消函数
*/
mkblk (block, params = {}, options = {}, callback) {
if (!isFunction(callback)) {
throw new TypeError('Callback is not provied or not be a function')
}
if (!block || !(block instanceof window.Blob)) {
callback(new TypeError('Block is not provided or not instanceof Blob'))
return
}
let { token } = params
let { tokenGetter } = options
if (!(isString(token) && token)) {
if (!isFunction(tokenGetter)) {
callback(new TypeError('Token is not provided'))
return
}
return this._execTokenGetter(tokenGetter, (error, token) => {
if (error) {
callback(error)
return
}
return this.mkblk(block, assign({ token }, params), options, callback)
})
}
options = defaultsDeep(options, this.settings)
let { chunkSize, useHttps, host, tokenPrefix } = options
host = host || (useHttps ? CONFIG.QINIU_UPLOAD_HTTPS_URL : CONFIG.QINIU_UPLOAD_HTTP_URL)
let url = `${useHttps ? 'https:' : 'http:'}//${host}/mkblk/${block.size}`
let headers = {
'Content-Type': 'application/octet-stream',
Authorization: `${tokenPrefix || 'UpToken'} ${token}`
}
let chunk = block.slice(0, chunkSize, block.type)
return this.request.upload(url, chunk, assign({ headers }, options), callback)
}
/**
* 上传分片
* 1. 多个分片可以组成一个块,每一个分片的开始与结尾都必须
* 在创建的时候并定义好,且第一个分片在上传块的时候必须
* 一并上传
* 2. 七牛会返回一个哈希值(ctx),上传下一个分片的时候必须
* 将前一个分片的哈希值同时上传给服务器,第二个分片拿创建
* 块时上传的第一个分片范围的哈希值
* 3. 最后一个分片值代表该块的结束,必须记录好哈希值(ctx);
* 在合并文件的时候可以通过这些最后的哈希值进行合成文件
*
* @see https://developer.qiniu.com/kodo/api/1251/bput
*
* @param {Blob} chunk 片
* @param {Object} params 参数
* @param {String} params.ctx 前一次上传返回的块级上传控制信息
* @param {String} params.offset 当前片在整个块中的起始偏移
* @param {String} params.token 七牛令牌
* @param {Object} [options={}] 上传配置
* @param {Function} [options.tokenGetter] 获取 Token 拦截器
* @param {Boolean} [options.useHttps] 是否使用 Https 进行上传
* @param {String} [options.host] 七牛HOST https://developer.qiniu.com/kodo/manual/1671/region-endpoint
* @param {String} [options.tokenPrefix] 令牌前缀
* @param {Function} [options.progress] 上传进度
* @param {Function} callback 回调
* @returns {Object} state
* @returns {XMLHttpsRequest} state.xhr AJAX 对象
* @returns {Function} state.cancel 取消函数
*/
bput (chunk, params = {}, options = {}, callback) {
if (!isFunction(callback)) {
throw new TypeError('Callback is not provied or not be a function')
}
if (!chunk || !(chunk instanceof window.Blob)) {
callback(new TypeError('Block is not provided or not instanceof Blob'))
return
}
let { token } = params
let { tokenGetter } = options
if (!(isString(token) && token)) {
if (!isFunction(tokenGetter)) {
callback(new TypeError('Token is not provided'))
return
}
return this._execTokenGetter(tokenGetter, (error, token) => {
if (error) {
callback(error)
return
}
return this.bput(chunk, assign({ token }, params), options, callback)
})
}
let { ctx, offset } = params
if (!isString(params.ctx)) {
callback(new TypeError('Params.ctx is not provided or not be a valid string'))
return
}
if (!(isNumber(offset) && isInteger(offset) && offset > 0)) {
callback(new TypeError('Params.offset is not provided or not be a valid interger'))
return
}
options = defaultsDeep(options, this.settings)
let { host, useHttps, tokenPrefix } = options
host = host || (useHttps ? CONFIG.QINIU_UPLOAD_HTTPS_URL : CONFIG.QINIU_UPLOAD_HTTP_URL)
let url = `${useHttps ? 'https:' : 'http:'}//${host}/bput/${ctx}/${offset}`
let headers = {
'Content-Type': 'application/octet-stream',
Authorization: `${tokenPrefix || 'UpToken'} ${token}`
}
return this.request.upload(url, chunk, assign({ headers }, options), callback)
}
/**
* 提交组合文件,将所有块与分片组合起来并生成文件
* 当所有块与分片都上传了,将所有块的返回
*
* @see https://developer.qiniu.com/kodo/api/1287/mkfile
*
* @param {Array|String} ctxs 文件
* @param {Object} params 参数
* @param {Integer} params.size 文件大小
* @param {Object} [params.key] 如果没有指定则:如果 uptoken.SaveKey 存在则基于 SaveKey 生产 key,否则用 hash 值作 key。EncodedKey 需要经过 base64 编码
* @param {Object} [params.mimeType] 文件的 MIME 类型,默认是 application/octet-stream
* @param {Object} [params.crc32] 文件内容的 crc32 校验值,不指定则不进行校验
* @param {Object} [options={}] 上传配置
* @param {Function} [options.tokenGetter] 获取 Token 拦截器
* @param {Boolean} [options.useHttps] 是否使用 Https 进行上传
* @param {String} [options.host] 七牛HOST https://developer.qiniu.com/kodo/manual/1671/region-endpoint
* @param {String} [options.tokenPrefix] 令牌前缀
* @param {Function} [options.progress] 上传进度
* @param {Function} callback 回调
* @returns {Object} state
* @returns {XMLHttpsRequest} state.xhr AJAX 对象
* @returns {Function} state.cancel 取消函数
*/
mkfile (ctxs, params = {}, options = {}, callback) {
if (!isFunction(callback)) {
throw new TypeError('Callback is not provied or not be a function')
}
if (isEmpty(ctxs) || !(isArray(ctxs) || isString(ctxs))) {
callback(new TypeError('Ctxs is not provided or not be a valid value'))
return
}
let { token } = params
let { tokenGetter } = options
if (!(isString(token) && token)) {
if (!isFunction(tokenGetter)) {
callback(new TypeError('Token is not provided'))
return
}
return this._execTokenGetter(tokenGetter, (error, token) => {
if (error) {
callback(error)
return
}
return this.mkfile(ctxs, assign({ token }, params), options, callback)
})
}
let { size } = params
if (!(isNumber(size) && isInteger(size) && size > 0)) {
callback(new TypeError('Param.size is not provided or not be a valid integer'))
return
}
let { host, useHttps, tokenPrefix } = options
host = host || (useHttps ? CONFIG.QINIU_UPLOAD_HTTPS_URL : CONFIG.QINIU_UPLOAD_HTTP_URL)
let url = `${useHttps ? 'https:' : 'http:'}//${host}/mkfile/${size}`
let { key, mimeType, crc32, userVars } = params
if (isString(key) && key) {
url += `/key/${encodeURIComponent(key)}`
}
if (isString(mimeType) && mimeType) {
url += `/mimeType/${encodeURIComponent(mimeType)}`
}
if (isString(crc32) && crc32) {
url += `/crc32/${encodeURIComponent(crc32)}`
}
if (isString(userVars) && userVars) {
url += `/x:user-var/${encodeURIComponent(userVars)}`
}
let data = isArray(ctxs) ? ctxs.join(',') : ctxs
let headers = {
'Content-Type': 'application/octet-stream',
Authorization: `${tokenPrefix || 'UpToken'} ${token}`
}
return this.request.upload(url, data, assign({ headers }, options), callback)
}
/**
* 分割文件并上传
* 一次过将文件分成多个,并进行并发上传
* 上传的快慢并不代表分个数的大小, 我们应该尽量
* 创建适当多个块(Block), 因为没上传的块只是阻塞
* 在任务队列中
*
* @param {File|Blob} file 文件
* @param {Object} params 上传参数
* @param {Object} params.token 七牛令牌
* @param {Object} [params.key] 如果没有指定则:如果 uptoken.SaveKey 存在则基于 SaveKey 生产 key,否则用 hash 值作 key。EncodedKey 需要经过 base64 编码
* @param {Object} [params.mimeType] 文件的 MIME 类型,默认是 application/octet-stream
* @param {Object} [params.crc32] 文件内容的 crc32 校验值,不指定则不进行校验
* @param {Object} [options={}] 上传配置
* @param {Function} [options.tokenGetter] 获取 Token 拦截器
* @param {Boolean} [options.useHttps] 是否使用 Https 进行上传
* @param {String} [options.host] 七牛HOST https://developer.qiniu.com/kodo/manual/1671/region-endpoint
* @param {String} [options.tokenPrefix] 令牌前缀
* @param {Boolean} [options.cache=true] 设置本地缓存
* @param {Boolean} [options.override=false] 无论是否已经上传都进行重新上传
* @param {Integer} [options.maxConnect=4] 最大连接数,设置最大上传分块(Block)的数量,其余分块(Block)将会插入队列中
* @param {Function} [options.progress] 上传进度
* @param {Function} callback 回调
* @returns {Object} state
* @returns {XMLHttpsRequest} state.xhr AJAX 对象
* @returns {Function} state.cancel 取消函数
*/
resuming (file, params, options, callback) {
if (!isFunction(callback)) {
throw new TypeError('Callback is not provied or not be a function')
}
if (!(file instanceof File)) {
callback(new TypeError('File is not provided or not instanceof File (QiniuUploader.File)'))
return
}
options = defaultsDeep(options, { cache: true, override: false }, this.settings)
let { maxConnect } = options
if (!(isInteger(maxConnect) && maxConnect > 0)) {
callback(new TypeError('Options.maxConnect is invalid or not a integer'))
return
}
let {
blockSize: perBlockSize,
chunkSize: perChunkSize
} = options
if (!isInteger(perBlockSize)) {
callback(new TypeError('Block size is not a integer'))
return
}
if (!isInteger(perChunkSize)) {
callback(new TypeError('Chunk size is not a integer'))
return
}
if (perBlockSize < perChunkSize) {
callback(new Error('Chunk size must less than block size'))
return
}
let { maxFileSize } = options
if (file.size > maxFileSize) {
callback(new Error(`File size must less than ${maxFileSize}`))
return
}
if (!isInteger(maxFileSize)) {
callback(new TypeError('MaxFileSize is not a integer'))
return
}
let _resumingProgressHandle = options.progress
options.progress = undefined
let processes = []
let listenProgress = isFunction(_resumingProgressHandle)
let registerRequest = function (type, request, progressRelativeData) {
if (!(request && request.xhr && request.xhr instanceof window.XMLHttpRequest)) {
return
}
let { xhr } = request
/* eslint standard/object-curly-even-spacing:0 */
let process = { request, xhr /** , size, beginOffset, endOffset */ }
if (!isEmpty(progressRelativeData)) {
assign(process, progressRelativeData)
if (listenProgress) {
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
process.loaded = event.loaded
process.total = event.total
}
triggerRequestProgress(type, xhr, process)
}, false)
}
}
type === 'bput' && processes.push(process)
}
let triggerRequestProgress = function (type, xhr, process) {
let uploadSize = 0
forEach(processes, function ({ size, loaded, total, beginPos, endPos }) {
if (isInteger(size) && isInteger(loaded) && isInteger(total)) {
uploadSize += size * loaded / total
}
})
let event = new QiniupEvent(type)
event.processes = processes
event.process = process
event.loaded = uploadSize
event.total = file.size
let nowDatetime = Date.now()
let spendTime = nowDatetime - startDatetime
let size = event.loaded
let time = spendTime / 1000
let speed = size / time || 0
let description = `${speed.toFixed(2)}Byte/s`
if (speed > CONFIG.G) {
description = `${(speed / CONFIG.G).toFixed(2)}Gb/s`
} else if (speed > CONFIG.M) {
description = `${(speed / CONFIG.M).toFixed(2)}Mb/s`
} else if (speed > CONFIG.K) {
description = `${(speed / CONFIG.K).toFixed(2)}Kb/s`
}
event.during = time
event.speed = speed
event.speedDescription = description
_resumingProgressHandle.call(xhr, event)
}
let abortRequest = function () {
forEach(processes, ({ request }) => request.cancel())
}
/**
* 创建分块任务
* @param {File} file 文件对象
* @param {Integer} beginPos 起始位置
* @param {Integer} endPos 结束位置
* @param {Function} callback 回调函数
*/
let mkblk = (file, beginPos, endPos, info, callback) => {
/**
* 如果该段已经被上传则执行下一个切割任务
* 每次切割任务都必须判断分片(Chunk)是否上传完成
*/
let state = file.getState(beginPos, endPos)
if (info.options.override === false && state) {
callback(null, state)
return
}
/**
* 当到该段上传的时候才进行切割,否则大型文件在切割的情况下会变得好卡
* 这样也能减少资源与内存的消耗
*/
let block = file.slice(beginPos, endPos)
let request = this.mkblk(block, info.params, info.options, (error, response) => {
if (error) {
callback(error)
return
}
let state = assign({ status: 'uploaded', beginPos, endPos }, response)
file.setState(beginPos, endPos, state, info.options.cache)
callback(null, assign({ file, block, state }, info))
})
/**
* 这里是返回的是分块(block)中的第一个分片(chunk)
* 与上面的末位置不同(endPos)
*/
let size = block.size > perChunkSize ? perChunkSize : block.size
registerRequest('mkblk', request, { size: block.size, beginOffset: beginPos, endOffset: endPos })
registerRequest('bput', request, { size, beginOffset: beginPos, endOffset: beginPos + size })
}
/**
* 创建分片任务
* @param {Blob} block 分块
* @param {File} file 文件对象
* @param {String} ctx 七牛创建分块并上传第一个分片完成后返回哈希值
* @param {Integer} beginPos 起始位置
* @param {Integer} endPos 结束位置
* @param {Function} callback 回调函数
*/
let mkchk = (block, beginPos, endPos, info, callback) => {
/**
* 如果该段已经被上传则执行下一个切割任务
* 每次切割任务都必须判断分片(Chunk)是否上传完成
*/
let state = info.file.getState(info.beginOffset, info.endOffset)
if (info.options.override === false && state) {
callback(null, state)
return
}
let chunk = block.slice(beginPos, endPos, block.type)
let params = assign({ ctx: info.ctx, offset: beginPos }, info.params)
let request = this.bput(chunk, params, info.options, (error, response) => {
if (error) {
callback(error)
return
}
let state = assign({ status: 'uploaded', beginPos, endPos }, response)
file.setState(beginPos, endPos, state, info.options.cache)
callback(null, assign({ state, chunk, block }, info))
})
let datas = {
size: chunk.size,
beginOffset: info.beginOffset,
endOffset: info.endOffset
}
registerRequest('bput', request, datas)
}
let totalBlockNo = Math.ceil(file.size / perBlockSize)
if (totalBlockNo > options.maxBlockTasks) {
callback(new Error(`Block total number (${totalBlockNo}) is too large, it must be less than ${options.maxBlockTasks}, please check uploader options`))
return
}
let tasks = times(totalBlockNo, (blockNo) => {
let tasks = []
let blockOffset = perBlockSize * blockNo
let blockBeginPos = blockOffset
let blockEndPos = blockOffset + perBlockSize
if (blockEndPos > file.size) {
blockEndPos = file.size
}
/**
* 因为上传块(Block)的时候必须同时上传第一个切割片(Chunk)
* 因此我们可以直接判断当前块的第一个切片(Chunk)是否已经上传
* 不用额外将块(Block)上传信息另外保存起来
*/
let task = (callback) => {
let info = { params, options }
return mkblk(file, blockBeginPos, blockEndPos, info, callback)
}
tasks.push(task)
/**
* 上传片(Chunk)
* 每个块都由许多片(Chunk)组成
* 因此要先预设每个快(Block)中的片(Chunk)的起始位置(offset)
* 这样就预先定义好上传的任务队列
*
* 注意:
* 因为切割块(Block)是比较浪费资源,而且保存多个片(Chunk)会导致
* 内存大幅增加,因此我们必须在每个任务上传之前先给定相应的配置(起始位置与片大小等)
* 来进行定义任务,而非切割多个片(Chunk)资源,而且上传完必须销毁
*/
let blockSize = blockEndPos - blockBeginPos
let totalChunkNo = Math.ceil(blockSize / perChunkSize)
/**
* 因为上传分块(Block)已经上传了第一个分片(Chunk)
* 所以可以忽略第一个分片(Chunk),而分片(Chunk)的总数也减一
*/
times(totalChunkNo - 1, (chunkNo) => {
let chunkOffset = perChunkSize * (chunkNo + 1)
let chunkBeginPos = chunkOffset
let chunkEndPos = chunkOffset + perChunkSize
if (chunkEndPos > blockSize) {
chunkEndPos = blockSize
}
let task = ({ state, block, file }, callback) => {
let info = {
file,
ctx: state.ctx,
params,
options,
beginOffset: blockBeginPos + chunkBeginPos,
endOffset: blockBeginPos + chunkEndPos
}
return mkchk(block, chunkBeginPos, chunkEndPos, info, callback)
}
tasks.push(task)
})
return (callback) => waterfall(tasks, callback)
})
let startDatetime = Date.now()
/**
* 当所有块(Block)都全部上传完
* 则执行合并文件操作
*/
parallel(tasks, (error, responses) => {
if (error) {
callback(error)
return
}
/**
* 合并文件的时候必须要注意的是上传的 ctxs 值必须
* 是分割的顺序的,所以可以根据起始位置(beginPos)或者
* 结束位置(endPos)进行排序
*/
responses = sortBy(responses, 'state.beginPos')
/**
* 获取所有分块中最后分片上传完成返回的哈希值(ctx),
* 并组成数组提交创建文件接口
*/
let ctxs = map(responses, 'state.ctx')
let size = file.size
let request = this.mkfile(ctxs, assign({ size }, params), options, callback)
registerRequest('mkfile', request, { size })
})
return { cancel: abortRequest, xhr: null }
}
}