qiniu-js
Version:
Javascript SDK for Qiniu Resource (Cloud) Storage AP
225 lines (189 loc) • 5.96 kB
text/typescript
import * as utils from '../utils'
import { Pool } from '../pool'
import { uploadChunk, uploadComplete, initUploadParts, UploadChunkData } from '../api'
import Base, { Progress, UploadInfo, Extra } from './base'
export interface UploadedChunkStorage extends UploadChunkData {
size: number
}
export interface ChunkLoaded {
mkFileProgress: 0 | 1
chunks: number[]
}
export interface ChunkInfo {
chunk: Blob
index: number
}
export interface LocalInfo {
data: UploadedChunkStorage[]
id: string
}
export interface ChunkPart {
etag: string
partNumber: number
}
export interface UploadChunkBody extends Extra {
parts: ChunkPart[]
}
/** 是否为正整数 */
function isPositiveInteger(n: number) {
var re = /^[1-9]\d*$/
return re.test(String(n))
}
export default class Resume extends Base {
private chunks: Blob[]
/** 当前上传过程中已完成的上传信息 */
private uploadedList: UploadedChunkStorage[]
/** 当前上传片进度信息 */
private loaded: ChunkLoaded
private uploadId: string
protected async run() {
if (!this.config.chunkSize || !isPositiveInteger(this.config.chunkSize)) {
throw new Error('chunkSize must be a positive integer')
}
if (this.config.chunkSize > 1024) {
throw new Error('chunkSize maximum value is 1024')
}
await this.initBeforeUploadChunks()
const pool = new Pool(
(chunkInfo: ChunkInfo) => this.uploadChunk(chunkInfo),
this.config.concurrentRequestLimit
)
const uploadChunks = this.chunks.map((chunk, index) => pool.enqueue({ chunk, index }))
const result = Promise.all(uploadChunks).then(() => this.mkFileReq())
result.then(
() => {
utils.removeLocalFileInfo(this.getLocalKey())
},
err => {
// uploadId 无效,上传参数有误(多由于本地存储信息的 uploadId 失效
if (err.code === 612 || err.code === 400) {
utils.removeLocalFileInfo(this.getLocalKey())
}
}
)
return result
}
private async uploadChunk(chunkInfo: ChunkInfo) {
const { index, chunk } = chunkInfo
const info = this.uploadedList[index]
const shouldCheckMD5 = this.config.checkByMD5
const reuseSaved = () => {
this.updateChunkProgress(chunk.size, index)
}
if (info && !shouldCheckMD5) {
reuseSaved()
return
}
const md5 = await utils.computeMd5(chunk)
if (info && md5 === info.md5) {
reuseSaved()
return
}
const onProgress = (data: Progress) => {
this.updateChunkProgress(data.loaded, index)
}
const requestOptions = {
body: chunk,
onProgress,
onCreate: (xhr: XMLHttpRequest) => this.addXhr(xhr)
}
const response = await uploadChunk(
this.token,
this.key,
chunkInfo.index + 1,
this.getUploadInfo(),
requestOptions
)
// 在某些浏览器环境下,xhr 的 progress 事件无法被触发,progress 为 null,这里在每次分片上传完成后都手动更新下 progress
onProgress({
loaded: chunk.size,
total: chunk.size
})
this.uploadedList[index] = {
etag: response.data.etag,
md5: response.data.md5,
size: chunk.size
}
utils.setLocalFileInfo(this.getLocalKey(), {
id: this.uploadId,
data: this.uploadedList
})
}
private async mkFileReq() {
const data: UploadChunkBody = {
parts: this.uploadedList.map((value, index) => ({
etag: value.etag,
partNumber: index + 1
})),
fname: this.putExtra.fname,
...this.putExtra.mimeType && { mimeType: this.putExtra.mimeType },
...this.putExtra.customVars && { customVars: this.putExtra.customVars },
...this.putExtra.metadata && { metadata: this.putExtra.metadata }
}
const result = await uploadComplete(
this.token,
this.key,
this.getUploadInfo(),
{
onCreate: xhr => this.addXhr(xhr),
body: JSON.stringify(data)
}
)
this.updateMkFileProgress(1)
return result
}
private async initBeforeUploadChunks() {
const localInfo = utils.getLocalFileInfo(this.getLocalKey())
// 分片必须和当时使用的 uploadId 配套,所以断点续传需要把本地存储的 uploadId 拿出来
// 假如没有 localInfo 本地信息并重新获取 uploadId
if (!localInfo) {
// 防止本地信息已被破坏,初始化时 clear 一下
utils.removeLocalFileInfo(this.getLocalKey())
const res = await initUploadParts(this.token, this.bucket, this.key, this.uploadUrl)
this.uploadId = res.data.uploadId
this.uploadedList = []
} else {
this.uploadId = localInfo.id
this.uploadedList = localInfo.data
}
this.chunks = utils.getChunks(this.file, this.config.chunkSize)
this.loaded = {
mkFileProgress: 0,
chunks: this.chunks.map(_ => 0)
}
this.notifyResumeProgress()
}
private getUploadInfo(): UploadInfo {
return {
id: this.uploadId,
url: this.uploadUrl
}
}
private getLocalKey() {
return utils.createLocalKey(this.file.name, this.key, this.file.size)
}
private updateChunkProgress(loaded: number, index: number) {
this.loaded.chunks[index] = loaded
this.notifyResumeProgress()
}
private updateMkFileProgress(progress: 0 | 1) {
this.loaded.mkFileProgress = progress
this.notifyResumeProgress()
}
private notifyResumeProgress() {
this.progress = {
total: this.getProgressInfoItem(
utils.sum(this.loaded.chunks) + this.loaded.mkFileProgress,
this.file.size + 1 // 防止在 complete 未调用的时候进度显示 100%
),
chunks: this.chunks.map((chunk, index) => (
this.getProgressInfoItem(this.loaded.chunks[index], chunk.size)
)),
uploadInfo: {
id: this.uploadId,
url: this.uploadUrl
}
}
this.onData(this.progress)
}
}