Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import { region } from '../config'
import { getUploadUrl } from '../api'
import Logger, { LogLevel } from '../logger'
import * as utils from '../utils'
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
/** 分片大小,单位为 MB */
chunkSize: number
/** 上传域名协议 */
upprotocol: 'http:' | 'https:'
/** 上传区域 */
region?: typeof region[keyof typeof region]
/** 是否禁止统计日志上报 */
disableStatisticsReport: boolean
/** 设置调试日志输出模式,默认 `OFF`,不输出任何日志 */
debugLogLevel?: LogLevel
}
export interface UploadOptions {
file: File
key: string | null | undefined
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 | null | undefined
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, protected logger: Logger) {
this.config = {
useCdnDomain: true,
disableStatisticsReport: false,
retryCount: 3,
checkByMD5: false,
uphost: '',
upprotocol: 'https:',
forceDirect: false,
chunkSize: DEFAULT_CHUNK_SIZE,
concurrentRequestLimit: 3,
...options.config
}
logger.info('config inited.', this.config)
this.putExtra = {
fname: '',
...options.putExtra
}
logger.info('putExtra inited.', this.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) {
logger.error('get bucket from token failed.', e)
this.onError(e)
}
}
private handleError(message: string) {
const err = new Error(message)
this.logger.error(message)
this.onError(err)
}
/**
* @returns Promise 返回结果与上传最终状态无关,状态信息请通过 [Subscriber] 获取。
* @description 上传文件,状态信息请通过 [Subscriber] 获取。
*/
public async putFile(): Promise<void> {
this.aborted = false
if (!this.putExtra.fname) {
this.logger.info('use file.name as fname.')
this.putExtra.fname = this.file.name
}
if (this.file.size > 10000 * GB) {
this.handleError('file size exceed maximum value 10000G.')
return
}
if (this.putExtra.customVars) {
if (!utils.isCustomVarsValid(this.putExtra.customVars)) {
this.handleError('customVars key should start width x:.')
return
}
}
if (this.putExtra.metadata) {
if (!utils.isMetaDataValid(this.putExtra.metadata)) {
this.handleError('metadata key should start with x-qn-meta-.')
return
}
}
try {
this.uploadUrl = await getUploadUrl(this.config, this.token)
this.logger.info('get uploadUrl from api.', this.uploadUrl)
this.uploadAt = new Date().getTime()
const result = await this.run()
this.onComplete(result.data)
this.sendLog(result.reqId, 200)
return
} catch (err) {
this.logger.error(err)
this.clear()
if (err.isRequestError) {
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) {
this.logger.warn(`error auto retry: ${this.retryCount}/${this.config.retryCount}.`)
this.putFile()
return
}
this.onError(err)
}
}
private clear() {
this.logger.info('start cleaning all xhr.')
this.xhrList.forEach(xhr => {
xhr.onreadystatechange = null
xhr.abort()
})
this.logger.info('cleanup completed.')
this.xhrList = []
}
public stop() {
this.logger.info('stop.')
this.clear()
this.aborted = true
}
public addXhr(xhr: XMLHttpRequest) {
this.xhrList.push(xhr)
}
private sendLog(reqId: string, code: number) {
this.logger.report({
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
})
}
public getProgressInfoItem(loaded: number, size: number) {
return {
loaded,
size,
percent: loaded / size * 100
}
}
}
|