UNPKG

js-uploader

Version:
404 lines (350 loc) 12.8 kB
import { CommonsTaskHandler } from './CommonsTaskHandler' import { UploadTask, UploaderOptions, OSSProvider, FileChunk, UploadFile, Obj, AjaxResponse, StatusCode, RequestMethod, Protocol, } from '../../interface' import { of, from, Observable, Subscriber } from 'rxjs' import { tap, map, catchError, mapTo, concatMap, filter, mergeMap } from 'rxjs/operators' import { ajax } from 'rxjs/ajax' import { Logger } from '../../shared' import { urlSafeBase64Encode, urlSafeBase64Decode } from '../../shared/base64' interface FileExtraInfo { bucket?: string key?: string uploadId?: string upToken?: string baseUrl?: string } interface CompletedPart { etag: string partNumber: number } interface PutPolicy { ak: string scope: string } interface UpHosts { up: { acc: { main: string[] } } } export class QiniuOSSTaskHandler_v2 extends CommonsTaskHandler { private static HOST_MAP = new Map<string, UpHosts>() private static _overwrite = false constructor(task: UploadTask, uploaderOptions: UploaderOptions) { super(task, uploaderOptions) Logger.info('🚀 ~ QiniuOSSTaskHandler_v2 ~ constructor :', task, uploaderOptions) if (!QiniuOSSTaskHandler_v2._overwrite) { this.processUploaderOptions() } } private enable(task: UploadTask) { const { ossOptions } = this.uploaderOptions return ossOptions?.enable(task) && ossOptions.provider === OSSProvider.Qiniu_v2 } abort(): this { this.abortTaskFiles() super.abort() return this } private abortTaskFiles() { let sub = of(...this.task.fileList) .pipe( filter((file) => { const { key, uploadId } = this.getFileExtraInfo(file) return !!key && !!uploadId && file.status !== StatusCode.Complete }), mergeMap((file) => { const { key, uploadId } = this.getFileExtraInfo(file) return this.abortMultipartUpload(file, key!, uploadId!) }, 10), ) .subscribe({ complete: () => { sub.unsubscribe() sub = null as any }, }) } private processUploaderOptions() { const { uploaderOptions } = this const { ossOptions, beforeFileUploadComplete, beforeFileUploadStart, beforeUploadResponseProcess } = uploaderOptions if (!ossOptions?.enable || ossOptions?.provider !== OSSProvider.Qiniu_v2) { throw new Error('ossOptions配置错误!') } const { chunkSize, chunked, requestBodyProcessFn, requestOptions } = uploaderOptions const { url, headers, method } = requestOptions uploaderOptions.chunkSize = chunked ? Math.max(chunkSize || 0, 1024 ** 2 * 4) : chunkSize uploaderOptions.requestOptions.method = (task: UploadTask, upfile: UploadFile, chunk: FileChunk) => { if (!this.enable(task)) { return typeof method === 'function' ? method(task, upfile, chunk) : (method as RequestMethod) } return 'PUT' } uploaderOptions.requestOptions.url = (task: UploadTask, upfile: UploadFile, chunk: FileChunk) => { if (!this.enable(task)) { return this.createObserverble(url, task, upfile, chunk).toPromise() } const baseURL = this.getRequestBaseURL(upfile) let { key, uploadId } = this.getFileExtraInfo(upfile) key = urlSafeBase64Encode(key) return `${baseURL}/objects/${key}/uploads/${uploadId}/${chunk.index + 1}` } uploaderOptions.requestOptions.headers = (task: UploadTask, upfile: UploadFile, chunk: FileChunk) => { if (!this.enable(task)) { return this.createObserverble(headers, task, upfile, chunk).toPromise() } const authHeaders = this.getAuthHeaders(upfile) return { 'Content-Type': 'application/octet-stream', ...authHeaders, } } uploaderOptions.requestBodyProcessFn = (task: UploadTask, upfile: UploadFile, chunk: FileChunk, params: Obj) => { if (this.enable(task)) { return params.file } return requestBodyProcessFn?.(task, upfile, chunk, params) } const overwriteFns = this.getOverwriteFns() if (beforeFileUploadComplete?.name !== overwriteFns.overwriteBeforeFileUploadComplete.name) { uploaderOptions.beforeFileUploadComplete = overwriteFns.overwriteBeforeFileUploadComplete } if (beforeFileUploadStart?.name !== overwriteFns.overwriteBeforeFileUploadStart.name) { uploaderOptions.beforeFileUploadStart = overwriteFns.overwriteBeforeFileUploadStart } if (beforeUploadResponseProcess?.name !== overwriteFns.overwriteBeforeUploadResponseProcess.name) { uploaderOptions.beforeUploadResponseProcess = overwriteFns.overwriteBeforeUploadResponseProcess } QiniuOSSTaskHandler_v2._overwrite = true } private getOverwriteFns() { const { uploaderOptions } = this const { beforeFileUploadComplete, beforeFileUploadStart, beforeUploadResponseProcess } = uploaderOptions return { overwriteBeforeFileUploadStart: (task: UploadTask, upFile: UploadFile) => { const extraInfo: FileExtraInfo = this.getFileExtraInfo(upFile) const beforeUpload = () => { return beforeFileUploadStart?.(task, upFile) || Promise.resolve() } if (!this.enable(task)) { return beforeUpload() } const getObjectKey = () => { const objectKey = uploaderOptions.ossOptions?.keyGenerator?.(upFile, task) || Promise.resolve('') return this.toObserverble(objectKey).pipe( map((key) => { return (extraInfo.key = key) }), ) } const getUpToken = () => { const upToken = uploaderOptions.ossOptions?.uptokenGenerator?.(upFile, task) || Promise.resolve('') return this.toObserverble(upToken).pipe( map((token) => { return (extraInfo.upToken = token) }), ) } const getUploadUrlFn = (token: string) => { return from(this.getUploadUrl(token)).pipe( map((baseUrl) => { return (extraInfo.baseUrl = baseUrl) }), ) } const pre = () => { if (extraInfo.uploadId) { return of(extraInfo) } return of(null).pipe( concatMap(getUpToken), concatMap(getUploadUrlFn), concatMap(getObjectKey), concatMap((key) => this.createMultipartUpload(upFile, key)), tap(({ uploadId, bucket }) => Object.assign(extraInfo, { uploadId, bucket })), ) } return of(null).pipe(concatMap(pre), concatMap(beforeUpload)).toPromise() }, overwriteBeforeFileUploadComplete: (task: UploadTask, file: UploadFile) => { const beforeFileComplete = () => beforeFileUploadComplete?.(task, file) || Promise.resolve() if (!this.enable(task)) { return beforeFileComplete() } const completeMultipartUpload = () => { if (file.response.key) { return of(file.response) } return new Observable((subscriber: Subscriber<Obj>) => { const { key, uploadId } = this.getFileExtraInfo(file) const parts: CompletedPart[] = file.chunkList?.map((ck: FileChunk) => ({ etag: ck.response.etag, partNumber: ck.index + 1, })) this.completeMultipartUpload(file, key!, uploadId!, parts) .pipe( tap((res: Obj) => { file.response = res }), ) .subscribe(subscriber) }) } return of(null).pipe(concatMap(completeMultipartUpload), concatMap(beforeFileComplete)).toPromise() }, overwriteBeforeUploadResponseProcess: ( task: UploadTask, file: UploadFile, chunk: FileChunk, response: AjaxResponse, ) => { if (!this.enable(task)) { return Promise.resolve() } try { response.response = JSON.parse(response.response) } catch (error) { console.error(error) } return beforeUploadResponseProcess?.(task, file, chunk, response) || Promise.resolve() }, } } private createMultipartUpload(upfile: UploadFile, key: string) { const authHeaders = this.getAuthHeaders(upfile) const baseURL = this.getRequestBaseURL(upfile) key = urlSafeBase64Encode(key) return ajax({ url: `${baseURL}/objects/${key}/uploads`, method: 'POST', headers: { ...authHeaders, }, }).pipe( map((res: AjaxResponse) => { const { uploadId } = res?.response const extraInfo = this.getFileExtraInfo(upfile) const policy = this.getPutPolicy(extraInfo.upToken!) return { bucket: policy.bucket, key, uploadId } }), ) } protected uploadPart(upfile: UploadFile, key: string, partNumber: number, uploadId: string, body: any) { const authHeaders = this.getAuthHeaders(upfile) const baseURL = this.getRequestBaseURL(upfile) key = urlSafeBase64Encode(key) return ajax({ url: `${baseURL}/objects/${key}/uploads/${uploadId}/partNumber`, method: 'PUT', body, headers: { 'Content-Type': 'application/octet-stream', ...authHeaders, }, }).pipe( map((res) => { const { etag, md5 } = res.response return { uploadId, key, partNumber, etag, md5 } }), ) } private completeMultipartUpload(upfile: UploadFile, key: string, uploadId: string, parts: CompletedPart[]) { const authHeaders = this.getAuthHeaders(upfile) const baseURL = this.getRequestBaseURL(upfile) key = urlSafeBase64Encode(key) return ajax({ url: `${baseURL}/objects/${key}/uploads/${uploadId}`, method: 'POST', body: { parts: parts.sort((a, b) => a.partNumber - b.partNumber), }, headers: { 'Content-Type': 'application/json', ...authHeaders, }, }).pipe( map((res: AjaxResponse) => { let response = res.response || {} try { response = typeof response === 'string' ? JSON.parse(response) : response } catch (error) {} return { uploadId, ...response } }), ) } private abortMultipartUpload(upfile: UploadFile, key: string, uploadId: string) { const authHeaders = this.getAuthHeaders(upfile) const baseURL = this.getRequestBaseURL(upfile) key = urlSafeBase64Encode(key) return ajax({ url: `${baseURL}/objects/${key}/uploads/${uploadId}`, method: 'DELETE', headers: { ...authHeaders, }, responseType: 'text', }).pipe( mapTo(true), catchError(() => of(false)), ) } private getAuthHeaders(upfile: UploadFile) { const extraInfo = this.getFileExtraInfo(upfile) return { Authorization: `UpToken ${extraInfo.upToken}`, } } private getRequestBaseURL(upfile: UploadFile) { const extraInfo = this.getFileExtraInfo(upfile) return extraInfo.baseUrl! } private getFileExtraInfo(file: UploadFile): FileExtraInfo { file.extraInfo = file.extraInfo || {} return file.extraInfo as FileExtraInfo } private async getUploadUrl(token: string): Promise<string> { const reg = /^https?:/ let protocol: Protocol = location.protocol as Protocol if (!reg.test(protocol)) { const res = reg.exec(location.origin) protocol = (res?.length ? res[0] : 'http:') as Protocol } const putPolicy = this.getPutPolicy(token) const data = await this.getUpHosts(token, protocol) const hosts = data.up.acc.main return `${protocol}//${hosts[0]}/buckets/${putPolicy.bucket}` } private async getUpHosts(token: string, protocol: Protocol): Promise<UpHosts> { const putPolicy = this.getPutPolicy(token) const k = `${putPolicy.ak}--${putPolicy.bucket}` let hosts = QiniuOSSTaskHandler_v2.HOST_MAP.get(k) if (!hosts) { const url = `${protocol}//api.qiniu.com/v2/query?ak=${putPolicy.ak}&bucket=${putPolicy.bucket}` const ob$: Observable<UpHosts> = ajax.getJSON(url) hosts = await ob$.toPromise() QiniuOSSTaskHandler_v2.HOST_MAP.set(k, hosts) } return hosts } private getPutPolicy(token: string) { const segments = token.split(':') // token 构造的差异参考:https://github.com/qbox/product/blob/master/kodo/auths/UpToken.md#admin-uptoken-authorization const ak = segments.length > 3 ? segments[1] : segments[0] const putPolicy: PutPolicy = JSON.parse(urlSafeBase64Decode(segments[segments.length - 1])) return { ak, bucket: putPolicy.scope.split(':')[0], } } }