Skip to content

Commit

Permalink
添加支持多 host 自动切换 & 补全测试 (#505)
Browse files Browse the repository at this point in the history
* 添加高可用 & 补充测试
* 更新版本到 3.2.0
  • Loading branch information
yinxulai authored May 14, 2021
1 parent 671e54e commit c30a037
Show file tree
Hide file tree
Showing 26 changed files with 1,242 additions and 323 deletions.
189 changes: 102 additions & 87 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "qiniu-js",
"jsName": "qiniu",
"version": "3.1.4",
"version": "3.2.0",
"private": false,
"description": "Javascript SDK for Qiniu Resource (Cloud) Storage AP",
"main": "lib/index.js",
Expand Down
145 changes: 145 additions & 0 deletions src/api/index.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { QiniuNetworkError, QiniuRequestError } from '../errors'
import * as api from '.'

export const errorMap = {
networkError: new QiniuNetworkError('mock', 'message'), // 网络错误

invalidParams: new QiniuRequestError(400, 'mock', 'message'), // 无效的参数
expiredToken: new QiniuRequestError(401, 'mock', 'message'), // token 过期

gatewayUnavailable: new QiniuRequestError(502, 'mock', 'message'), // 网关不可用
serviceUnavailable: new QiniuRequestError(503, 'mock', 'message'), // 服务不可用
serviceTimeout: new QiniuRequestError(504, 'mock', 'message'), // 服务超时
serviceError: new QiniuRequestError(599, 'mock', 'message'), // 服务错误

invalidUploadId: new QiniuRequestError(612, 'mock', 'message'), // 无效的 upload id
}

export type ApiName =
| 'direct'
| 'getUpHosts'
| 'uploadChunk'
| 'uploadComplete'
| 'initUploadParts'
| 'deleteUploadedChunks'

export class MockApi {
constructor() {
this.direct = this.direct.bind(this)
this.getUpHosts = this.getUpHosts.bind(this)
this.uploadChunk = this.uploadChunk.bind(this)
this.uploadComplete = this.uploadComplete.bind(this)
this.initUploadParts = this.initUploadParts.bind(this)
this.deleteUploadedChunks = this.deleteUploadedChunks.bind(this)
}

private interceptorMap = new Map<ApiName, any>()
public clearInterceptor() {
this.interceptorMap.clear()
}

public setInterceptor(name: 'direct', interceptor: typeof api.direct): void
public setInterceptor(name: 'getUpHosts', interceptor: typeof api.getUpHosts): void
public setInterceptor(name: 'uploadChunk', interceptor: typeof api.uploadChunk): void
public setInterceptor(name: 'uploadComplete', interceptor: typeof api.uploadComplete): void
public setInterceptor(name: 'initUploadParts', interceptor: typeof api.initUploadParts): void
public setInterceptor(name: 'deleteUploadedChunks', interceptor: typeof api.deleteUploadedChunks): void
public setInterceptor(name: ApiName, interceptor: any): void
public setInterceptor(name: any, interceptor: any): void {
this.interceptorMap.set(name, interceptor)
}

private callInterceptor(name: ApiName, defaultValue: any): any {
const interceptor = this.interceptorMap.get(name)
if (interceptor != null) {
return interceptor()
}

return defaultValue
}

public direct(): ReturnType<typeof api.direct> {
const defaultData: ReturnType<typeof api.direct> = Promise.resolve({
reqId: 'req-id',
data: {
fsize: 270316,
bucket: 'test2222222222',
hash: 'Fs_k3kh7tT5RaFXVx3z1sfCyoa2Y',
name: '84575bc9e34412d47cf3367b46b23bc7e394912a',
key: '84575bc9e34412d47cf3367b46b23bc7e394912a.html'
}
})

return this.callInterceptor('direct', defaultData)
}

public getUpHosts(): ReturnType<typeof api.getUpHosts> {
const defaultData: ReturnType<typeof api.getUpHosts> = Promise.resolve({
reqId: 'req-id',
data: {
ttl: 86400,
io: { src: { main: ['iovip-z2.qbox.me'] } },
up: {
acc: {
main: ['upload-z2.qiniup.com'],
backup: ['upload-dg.qiniup.com', 'upload-fs.qiniup.com']
},
old_acc: { main: ['upload-z2.qbox.me'], info: 'compatible to non-SNI device' },
old_src: { main: ['up-z2.qbox.me'], info: 'compatible to non-SNI device' },
src: { main: ['up-z2.qiniup.com'], backup: ['up-dg.qiniup.com', 'up-fs.qiniup.com'] }
},
uc: { acc: { main: ['uc.qbox.me'] } },
rs: { acc: { main: ['rs-z2.qbox.me'] } },
rsf: { acc: { main: ['rsf-z2.qbox.me'] } },
api: { acc: { main: ['api-z2.qiniu.com'] } }
}
})

return this.callInterceptor('getUpHosts', defaultData)
}

public uploadChunk(): ReturnType<typeof api.uploadChunk> {
const defaultData: ReturnType<typeof api.uploadChunk> = Promise.resolve({
reqId: 'req-id',
data: {
etag: 'FuYYVJ1gmVCoGk5C5r5ftrLXxE6m',
md5: '491309eddd8e7233e14eaa25216594b4'
}
})

return this.callInterceptor('uploadChunk', defaultData)
}

public uploadComplete(): ReturnType<typeof api.uploadComplete> {
const defaultData: ReturnType<typeof api.uploadComplete> = Promise.resolve({
reqId: 'req-id',
data: {
key: 'test.zip',
hash: 'lsril688bAmXn7kiiOe9fL4mpc39',
fsize: 11009649,
bucket: 'test',
name: 'test'
}
})

return this.callInterceptor('uploadComplete', defaultData)
}

public initUploadParts(): ReturnType<typeof api.initUploadParts> {
const defaultData: ReturnType<typeof api.initUploadParts> = Promise.resolve({
reqId: 'req-id',
data: { uploadId: '60878b9408bc044043f5d74f', expireAt: 1620100628 }
})

return this.callInterceptor('initUploadParts', defaultData)
}

public deleteUploadedChunks(): ReturnType<typeof api.deleteUploadedChunks> {
const defaultData: ReturnType<typeof api.deleteUploadedChunks> = Promise.resolve({
reqId: 'req-id',
data: undefined
})

return this.callInterceptor('deleteUploadedChunks', defaultData)
}
}
12 changes: 11 additions & 1 deletion src/api/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { region } from '../config'
import { getUploadUrl } from '.'

jest.mock('../utils', () => ({
...jest.requireActual('../utils') as any,

request: () => Promise.resolve({
data: {
up: {
Expand All @@ -27,7 +29,7 @@ describe('api function test', () => {
retryCount: 3,
checkByMD5: false,
uphost: '',
upprotocol: 'https:',
upprotocol: 'https',
forceDirect: false,
chunkSize: DEFAULT_CHUNK_SIZE,
concurrentRequestLimit: 3
Expand All @@ -43,6 +45,14 @@ describe('api function test', () => {
url = await getUploadUrl(config, token)
expect(url).toBe('https://upload.qiniup.com')

config.upprotocol = 'https'
url = await getUploadUrl(config, token)
expect(url).toBe('https://upload.qiniup.com')

config.upprotocol = 'http'
url = await getUploadUrl(config, token)
expect(url).toBe('http://upload.qiniup.com')

config.upprotocol = 'https:'
url = await getUploadUrl(config, token)
expect(url).toBe('https://upload.qiniup.com')
Expand Down
80 changes: 51 additions & 29 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,26 @@
import { stringify } from 'querystring'

import { normalizeUploadConfig } from '../utils'
import { Config, InternalConfig, UploadInfo } from '../upload'
import * as utils from '../utils'
import { regionUphostMap } from '../config'
import { Config, UploadInfo } from '../upload'

interface UpHosts {
data: {
up: {
acc: {
main: string[]
backup: string[]
}
}
}
}

export async function getUpHosts(token: string, protocol: 'https:' | 'http:'): Promise<UpHosts> {
const putPolicy = utils.getPutPolicy(token)
const url = protocol + '//api.qiniu.com/v2/query?ak=' + putPolicy.ak + '&bucket=' + putPolicy.bucket
export async function getUpHosts(accessKey: string, bucketName: string, protocol: InternalConfig['upprotocol']): Promise<UpHosts> {
const params = stringify({ ak: accessKey, bucket: bucketName })
const url = `${protocol}://api.qiniu.com/v2/query?${params}`
return utils.request(url, { method: 'GET' })
}

export type UploadUrlConfig = Partial<Pick<Config, 'upprotocol' | 'uphost' | 'region' | 'useCdnDomain'>>

/** 获取上传url */
export async function getUploadUrl(config: UploadUrlConfig, token: string): Promise<string> {
const protocol = config.upprotocol || 'https:'

if (config.uphost) {
return `${protocol}//${config.uphost}`
}

if (config.region) {
const upHosts = regionUphostMap[config.region]
const host = config.useCdnDomain ? upHosts.cdnUphost : upHosts.srcUphost
return `${protocol}//${host}`
}

const res = await getUpHosts(token, protocol)
const hosts = res.data.up.acc.main
return `${protocol}//${hosts[0]}`
}

/**
* @param bucket 空间名
* @param key 目标文件名
Expand Down Expand Up @@ -96,7 +78,7 @@ export function uploadChunk(
uploadInfo: UploadInfo,
options: Partial<utils.RequestOptions>
): utils.Response<UploadChunkData> {
const bucket = utils.getPutPolicy(token).bucket
const bucket = utils.getPutPolicy(token).bucketName
const url = getBaseUrl(bucket, key, uploadInfo) + `/${index}`
return utils.request<UploadChunkData>(url, {
...options,
Expand All @@ -119,7 +101,7 @@ export function uploadComplete(
uploadInfo: UploadInfo,
options: Partial<utils.RequestOptions>
): utils.Response<UploadCompleteData> {
const bucket = utils.getPutPolicy(token).bucket
const bucket = utils.getPutPolicy(token).bucketName
const url = getBaseUrl(bucket, key, uploadInfo)
return utils.request<UploadCompleteData>(url, {
...options,
Expand All @@ -138,7 +120,7 @@ export function deleteUploadedChunks(
key: string | null | undefined,
uploadinfo: UploadInfo
): utils.Response<void> {
const bucket = utils.getPutPolicy(token).bucket
const bucket = utils.getPutPolicy(token).bucketName
const url = getBaseUrl(bucket, key, uploadinfo)
return utils.request(
url,
Expand All @@ -148,3 +130,43 @@ export function deleteUploadedChunks(
}
)
}

/**
* @param {string} url
* @param {FormData} data
* @param {Partial<utils.RequestOptions>} options
* @returns Promise
* @description 直传接口
*/
export function direct(
url: string,
data: FormData,
options: Partial<utils.RequestOptions>
): Promise<UploadCompleteData> {
return utils.request<UploadCompleteData>(url, {
method: 'POST',
body: data,
...options
})
}

export type UploadUrlConfig = Partial<Pick<Config, 'upprotocol' | 'uphost' | 'region' | 'useCdnDomain'>>

/**
* @param {UploadUrlConfig} config
* @param {string} token
* @returns Promise
* @description 获取上传 url
*/
export async function getUploadUrl(_config: UploadUrlConfig, token: string): Promise<string> {
const config = normalizeUploadConfig(_config)
const protocol = config.upprotocol

if (config.uphost.length > 0) {
return `${protocol}://${config.uphost[0]}`
}
const putPolicy = utils.getPutPolicy(token)
const res = await getUpHosts(putPolicy.assessKey, putPolicy.bucketName, protocol)
const hosts = res.data.up.acc.main
return `${protocol}://${hosts[0]}`
}
33 changes: 1 addition & 32 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1 @@
/** 上传区域 */
export const region = {
z0: 'z0',
z1: 'z1',
z2: 'z2',
na0: 'na0',
as0: 'as0'
} as const

/** 上传区域对应的 host */
export const regionUphostMap = {
[region.z0]: {
srcUphost: 'up.qiniup.com',
cdnUphost: 'upload.qiniup.com'
},
[region.z1]: {
srcUphost: 'up-z1.qiniup.com',
cdnUphost: 'upload-z1.qiniup.com'
},
[region.z2]: {
srcUphost: 'up-z2.qiniup.com',
cdnUphost: 'upload-z2.qiniup.com'
},
[region.na0]: {
srcUphost: 'up-na0.qiniup.com',
cdnUphost: 'upload-na0.qiniup.com'
},
[region.as0]: {
srcUphost: 'up-as0.qiniup.com',
cdnUphost: 'upload-as0.qiniup.com'
}
} as const
export * from './region'
37 changes: 37 additions & 0 deletions src/config/region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/** 上传区域 */
export const region = {
z0: 'z0',
z1: 'z1',
z2: 'z2',
na0: 'na0',
as0: 'as0',
cnEast2: 'cn-east-2'
} as const

/** 上传区域对应的 host */
export const regionUphostMap = {
[region.z0]: {
srcUphost: ['up.qiniup.com'],
cdnUphost: ['upload.qiniup.com']
},
[region.z1]: {
srcUphost: ['up-z1.qiniup.com'],
cdnUphost: ['upload-z1.qiniup.com']
},
[region.z2]: {
srcUphost: ['up-z2.qiniup.com'],
cdnUphost: ['upload-z2.qiniup.com']
},
[region.na0]: {
srcUphost: ['up-na0.qiniup.com'],
cdnUphost: ['upload-na0.qiniup.com']
},
[region.as0]: {
srcUphost: ['up-as0.qiniup.com'],
cdnUphost: ['upload-as0.qiniup.com']
},
[region.cnEast2]: {
srcUphost: ['up-cn-east-2.qiniup.com'],
cdnUphost: ['upload-cn-east-2.qiniup.com']
}
} as const
Loading

0 comments on commit c30a037

Please sign in to comment.