import {QiniuErrorName, QiniuError, QiniuRequestError} from '../errors' import Logger, {LogLevel} from '../logger' import * as utils from '../utils' import {S3Client} from "@aws-sdk/client-s3"; import {Host} from './hosts' export const DEFAULT_CHUNK_SIZE = 5 // 单位 MB(AWS最小5M) // code 信息地址 https://developer.qiniu.com/kodo/3928/error-responses export const FREEZE_CODE_LIST = [0, 502, 503, 504, 599] // 将会冻结当前 host 的 code // 408/504 超时 export const RETRY_CODE_LIST = [...FREEZE_CODE_LIST, 612, 408, 504] // 会进行重试的 code /** 上传文件的资源信息配置 */ export interface Extra { /** 文件原文件名 */ fname: string /** 用来放置自定义变量 */ customVars?: { [key: string]: string } /** 自定义元信息 */ metadata?: { [key: string]: string } /** 文件类型设置 */ mimeType?: string // } export interface InternalConfig { /** 是否开启 cdn 加速 */ useCdnDomain: boolean /** 是否对分片进行 md5校验 */ checkByMD5: boolean /** 强制直传 */ forceDirect: boolean /** 上传失败后重试次数 */ retryCount: number /** 自定义上传域名 */ uphost: string[] /** 自定义分片上传并发请求量 */ concurrentRequestLimit: number /** 分片大小,单位为 MB */ chunkSize: number /** 上传域名协议 */ upprotocol: 'https' | 'http' /** 上传区域 */ region: string /** 是否禁止统计日志上报 */ disableStatisticsReport: boolean /** 设置调试日志输出模式,默认 `OFF`,不输出任何日志 */ debugLogLevel?: LogLevel /** ASW添加 biming **/ /** bucketName **/ bucketName: string endpoint: string } /** 上传任务的配置信息 */ export interface Config extends Partial> { /** 上传域名协议 */ upprotocol?: InternalConfig['upprotocol'] | 'https:' | 'http:' /** 自定义上传域名 */ uphost?: InternalConfig['uphost'] | string } export interface UploadOptions { file: File key: string | undefined token: string config: InternalConfig putExtra?: Partial } export interface UploadInfo { id: string | undefined url: string } /** 传递给外部的上传进度信息 */ export interface UploadProgress { total: ProgressCompose uploadInfo?: UploadInfo chunks?: ProgressCompose[] } export interface UploadHandlers { onData: (data: UploadProgress) => void onError: (err: QiniuError) => void onComplete: (res: any) => void } export interface Progress { total: number loaded: number } export interface ProgressCompose { size: number loaded: number percent: number fromCache?: boolean } export type XHRHandler = (xhr: XMLHttpRequest) => void const GB = 1024 ** 3 export default abstract class Base { protected s3Client: S3Client protected config: InternalConfig protected putExtra: Extra protected aborted = false protected retryCount = 0 protected uploadHost?: Host protected xhrList: XMLHttpRequest[] = [] protected file: File protected key: string | undefined protected token: string protected assessKey: string protected secretAccessKey: string // added by aws protected bucketName: string protected endpoint: string protected region: string protected uploadAt: number protected progress: UploadProgress protected onData: (data: UploadProgress) => void protected onError: (err: QiniuError) => void protected onComplete: (res: any) => void constructor( options: UploadOptions, handlers: UploadHandlers, protected logger: Logger ) { this.config = options.config logger.info('config inited.', this.config) this.bucketName = this.config.bucketName this.endpoint = this.config.endpoint this.region = this.config.region 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 { const putPolicy = utils.getPutPolicy(this.token) this.assessKey = putPolicy.accessKeyId this.secretAccessKey = putPolicy.secretAccessKey this.token = putPolicy.sessionToken this.s3Client = new S3Client({ endpoint: `https://${this.endpoint}/`, region: 'us-west-1', credentials: { accessKeyId: this.assessKey, secretAccessKey: this.secretAccessKey, sessionToken: this.token } }); } catch (error) { logger.error('get putPolicy from token failed.', error) this.onError(error) } } /** * @returns Promise 返回结果与上传最终状态无关,状态信息请通过 [Subscriber] 获取。 * @description 上传文件,状态信息请通过 [Subscriber] 获取。 */ public async putFile(): Promise { this.aborted = false if (this.file.size > 100 * GB) { this.handleError(new QiniuError( QiniuErrorName.InvalidFile, 'file size exceed maximum value 100G' )) return } try { this.uploadAt = new Date().getTime() await this.run() const data = { key: this.key, location: `https://${this.bucketName}.${this.endpoint}/${this.key}` } this.onComplete(data) return } catch (err) { this.logger.error(err) this.clear() if (err) { const notReachRetryCount = ++this.retryCount <= this.config.retryCount const needRetry = true; // const needRetry = !this.aborted && RETRY_CODE_LIST.includes(code) // 以下条件满足其中之一则会进行重新上传: // 1. 满足 needRetry 的条件且 retryCount 不为 0 // 2. uploadId 无效时在 resume 里会清除本地数据,并且这里触发重新上传 if (needRetry && notReachRetryCount) { this.logger.warn(`error auto retry: ${this.retryCount}/${this.config.retryCount}.`) this.putFile() return } } this.onError(err) } } public stop() { this.logger.info('stop.') this.clear() this.aborted = true } public addXhr(xhr: XMLHttpRequest) { this.xhrList.push(xhr) } public getProgressInfoItem(loaded: number, size: number, fromCache?: boolean): ProgressCompose { return { size, loaded, percent: loaded / size * 100, ...(fromCache == null ? {} : {fromCache}) } } /** * @description 子类通过该方法实现具体的任务处理 */ // protected abstract run(): Promise | Promise protected abstract run(): void private handleError(error: QiniuError) { this.logger.error(error.message) this.onError(error) } // fixme 现在没用到xhrList,xhr放在了S3ClientHttpRequestHandler生成 private clear() { this.logger.info('start cleaning all xhr.') this.xhrList.forEach(xhr => { xhr.onreadystatechange = null xhr.onload = null xhr.abort() }) this.logger.info('cleanup completed.') this.xhrList = [] } }