qiniu-js
Version:
Javascript SDK for Qiniu Resource (Cloud) Storage AP
236 lines (200 loc) • 6.01 kB
text/typescript
import * as utils from '../utils'
import { getUploadUrl, UploadCompleteData } from '../api'
import StatisticsLogger from '../statisticsLog'
import { region } from '../config'
export const DEFAULT_CHUNK_SIZE = 4 // 单位 MB
/** 上传文件的资源信息配置 */
export interface Extra {
/** 文件原文件名 */
fname: string
/** 用来放置自定义变量 */
customVars?: { [key: string]: string }
/** 自定义元信息 */
metadata?: { [key: string]: string }
/** 文件类型设置 */
mimeType?: string //
}
/** 上传任务的配置信息 */
export interface Config {
/** 是否开启 cdn 加速 */
useCdnDomain: boolean
/** 是否对分片进行 md5校验 */
checkByMD5: boolean
/** 强制直传 */
forceDirect: boolean
/** 上传失败后重试次数 */
retryCount: number
/** 自定义上传域名 */
uphost: string
/** 自定义分片上传并发请求量 */
concurrentRequestLimit: number
/** 是否禁止静态日志上报 */
disableStatisticsReport: boolean
/** 分片大小,单位为 MB */
chunkSize: number
/** 上传区域 */
region?: typeof region[keyof typeof region]
}
export interface UploadOptions {
file: File
key: string
token: string
putExtra?: Partial<Extra>
config?: Partial<Config>
}
export interface UploadInfo {
id: string
url: string
}
/** 传递给外部的上传进度信息 */
export interface UploadProgress {
total: ProgressCompose
uploadInfo?: UploadInfo
chunks?: ProgressCompose[]
}
export interface UploadHandler {
onData: (data: UploadProgress) => void
onError: (err: utils.CustomError) => void
onComplete: (res: any) => void
}
export interface Progress {
loaded: number
total: number
}
export interface ProgressCompose {
loaded: number
size: number
percent: number
}
export type XHRHandler = (xhr: XMLHttpRequest) => void
const GB = 1024 ** 3
export default abstract class Base {
protected config: Config
protected putExtra: Extra
protected xhrList: XMLHttpRequest[] = []
protected file: File
protected key: string
protected aborted = false
protected retryCount = 0
protected token: string
protected uploadUrl: string
protected bucket: string
protected uploadAt: number
protected progress: UploadProgress
protected onData: (data: UploadProgress) => void
protected onError: (err: utils.CustomError) => void
protected onComplete: (res: any) => void
protected abstract run(): utils.Response<any>
constructor(options: UploadOptions, handlers: UploadHandler, private statisticsLogger: StatisticsLogger) {
this.config = {
useCdnDomain: true,
disableStatisticsReport: false,
retryCount: 3,
checkByMD5: false,
uphost: '',
forceDirect: false,
chunkSize: DEFAULT_CHUNK_SIZE,
concurrentRequestLimit: 3,
...options.config
}
this.putExtra = {
fname: '',
...options.putExtra
}
this.file = options.file
this.key = options.key
this.token = options.token
this.onData = handlers.onData
this.onError = handlers.onError
this.onComplete = handlers.onComplete
try {
this.bucket = utils.getPutPolicy(this.token).bucket
} catch (e) {
this.onError(e)
}
}
public async putFile(): Promise<utils.ResponseSuccess<UploadCompleteData>> {
this.aborted = false
if (!this.putExtra.fname) {
this.putExtra.fname = this.file.name
}
if (this.file.size > 10000 * GB) {
const err = new Error('file size exceed maximum value 10000G')
this.onError(err)
throw err
}
if (this.putExtra.customVars) {
if (!utils.isCustomVarsValid(this.putExtra.customVars)) {
const err = new Error('customVars key should start width x:')
this.onError(err)
throw err
}
}
if (this.putExtra.metadata) {
if (!utils.isMetaDataValid(this.putExtra.metadata)) {
const err = new Error('metadata key should start with x-qn-meta-')
this.onError(err)
throw err
}
}
try {
this.uploadUrl = await getUploadUrl(this.config, this.token)
this.uploadAt = new Date().getTime()
const result = await this.run()
this.onComplete(result.data)
if (!this.config.disableStatisticsReport) {
this.sendLog(result.reqId, 200)
}
return result
} catch (err) {
this.clear()
if (err.isRequestError && !this.config.disableStatisticsReport) {
const reqId = this.aborted ? '' : err.reqId
const code = this.aborted ? -2 : err.code
this.sendLog(reqId, code)
}
const needRetry = err.isRequestError && err.code === 0 && !this.aborted
const notReachRetryCount = ++this.retryCount <= this.config.retryCount
// 以下条件满足其中之一则会进行重新上传:
// 1. 满足 needRetry 的条件且 retryCount 不为 0
// 2. uploadId 无效时在 resume 里会清除本地数据,并且这里触发重新上传
if (needRetry && notReachRetryCount || err.code === 612) {
return this.putFile()
}
this.onError(err)
throw err
}
}
private clear() {
this.xhrList.forEach(xhr => xhr.abort())
this.xhrList = []
}
public stop() {
this.clear()
this.aborted = true
}
public addXhr(xhr: XMLHttpRequest) {
this.xhrList.push(xhr)
}
private sendLog(reqId: string, code: number) {
this.statisticsLogger.log({
code,
reqId,
host: utils.getDomainFromUrl(this.uploadUrl),
remoteIp: '',
port: utils.getPortFromUrl(this.uploadUrl),
duration: (new Date().getTime() - this.uploadAt) / 1000,
time: Math.floor(this.uploadAt / 1000),
bytesSent: this.progress ? this.progress.total.loaded : 0,
upType: 'jssdk-h5',
size: this.file.size
}, this.token)
}
public getProgressInfoItem(loaded: number, size: number) {
return {
loaded,
size,
percent: loaded / size * 100
}
}
}