Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

添加支持多 host 自动切换 & 补全测试 #505

Merged
merged 28 commits into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.1.5",
lzfee0227 marked this conversation as resolved.
Show resolved Hide resolved
"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 '.'
yinxulai marked this conversation as resolved.
Show resolved Hide resolved

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'
lzfee0227 marked this conversation as resolved.
Show resolved Hide resolved
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> {
Copy link
Collaborator Author

@yinxulai yinxulai Apr 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

挪进来是为了方便统一 mock 测试

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 = {
lzfee0227 marked this conversation as resolved.
Show resolved Hide resolved
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