js-uploader
Version:
A JavaScript library for file upload
404 lines (350 loc) • 12.8 kB
text/typescript
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],
}
}
}