Skip to content

Commit c30a037

Browse files
authored
添加支持多 host 自动切换 & 补全测试 (#505)
* 添加高可用 & 补充测试 * 更新版本到 3.2.0
1 parent 671e54e commit c30a037

26 files changed

+1242
-323
lines changed

README.md

+102-87
Large diffs are not rendered by default.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "qiniu-js",
33
"jsName": "qiniu",
4-
"version": "3.1.4",
4+
"version": "3.2.0",
55
"private": false,
66
"description": "Javascript SDK for Qiniu Resource (Cloud) Storage AP",
77
"main": "lib/index.js",

src/api/index.mock.ts

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { QiniuNetworkError, QiniuRequestError } from '../errors'
2+
import * as api from '.'
3+
4+
export const errorMap = {
5+
networkError: new QiniuNetworkError('mock', 'message'), // 网络错误
6+
7+
invalidParams: new QiniuRequestError(400, 'mock', 'message'), // 无效的参数
8+
expiredToken: new QiniuRequestError(401, 'mock', 'message'), // token 过期
9+
10+
gatewayUnavailable: new QiniuRequestError(502, 'mock', 'message'), // 网关不可用
11+
serviceUnavailable: new QiniuRequestError(503, 'mock', 'message'), // 服务不可用
12+
serviceTimeout: new QiniuRequestError(504, 'mock', 'message'), // 服务超时
13+
serviceError: new QiniuRequestError(599, 'mock', 'message'), // 服务错误
14+
15+
invalidUploadId: new QiniuRequestError(612, 'mock', 'message'), // 无效的 upload id
16+
}
17+
18+
export type ApiName =
19+
| 'direct'
20+
| 'getUpHosts'
21+
| 'uploadChunk'
22+
| 'uploadComplete'
23+
| 'initUploadParts'
24+
| 'deleteUploadedChunks'
25+
26+
export class MockApi {
27+
constructor() {
28+
this.direct = this.direct.bind(this)
29+
this.getUpHosts = this.getUpHosts.bind(this)
30+
this.uploadChunk = this.uploadChunk.bind(this)
31+
this.uploadComplete = this.uploadComplete.bind(this)
32+
this.initUploadParts = this.initUploadParts.bind(this)
33+
this.deleteUploadedChunks = this.deleteUploadedChunks.bind(this)
34+
}
35+
36+
private interceptorMap = new Map<ApiName, any>()
37+
public clearInterceptor() {
38+
this.interceptorMap.clear()
39+
}
40+
41+
public setInterceptor(name: 'direct', interceptor: typeof api.direct): void
42+
public setInterceptor(name: 'getUpHosts', interceptor: typeof api.getUpHosts): void
43+
public setInterceptor(name: 'uploadChunk', interceptor: typeof api.uploadChunk): void
44+
public setInterceptor(name: 'uploadComplete', interceptor: typeof api.uploadComplete): void
45+
public setInterceptor(name: 'initUploadParts', interceptor: typeof api.initUploadParts): void
46+
public setInterceptor(name: 'deleteUploadedChunks', interceptor: typeof api.deleteUploadedChunks): void
47+
public setInterceptor(name: ApiName, interceptor: any): void
48+
public setInterceptor(name: any, interceptor: any): void {
49+
this.interceptorMap.set(name, interceptor)
50+
}
51+
52+
private callInterceptor(name: ApiName, defaultValue: any): any {
53+
const interceptor = this.interceptorMap.get(name)
54+
if (interceptor != null) {
55+
return interceptor()
56+
}
57+
58+
return defaultValue
59+
}
60+
61+
public direct(): ReturnType<typeof api.direct> {
62+
const defaultData: ReturnType<typeof api.direct> = Promise.resolve({
63+
reqId: 'req-id',
64+
data: {
65+
fsize: 270316,
66+
bucket: 'test2222222222',
67+
hash: 'Fs_k3kh7tT5RaFXVx3z1sfCyoa2Y',
68+
name: '84575bc9e34412d47cf3367b46b23bc7e394912a',
69+
key: '84575bc9e34412d47cf3367b46b23bc7e394912a.html'
70+
}
71+
})
72+
73+
return this.callInterceptor('direct', defaultData)
74+
}
75+
76+
public getUpHosts(): ReturnType<typeof api.getUpHosts> {
77+
const defaultData: ReturnType<typeof api.getUpHosts> = Promise.resolve({
78+
reqId: 'req-id',
79+
data: {
80+
ttl: 86400,
81+
io: { src: { main: ['iovip-z2.qbox.me'] } },
82+
up: {
83+
acc: {
84+
main: ['upload-z2.qiniup.com'],
85+
backup: ['upload-dg.qiniup.com', 'upload-fs.qiniup.com']
86+
},
87+
old_acc: { main: ['upload-z2.qbox.me'], info: 'compatible to non-SNI device' },
88+
old_src: { main: ['up-z2.qbox.me'], info: 'compatible to non-SNI device' },
89+
src: { main: ['up-z2.qiniup.com'], backup: ['up-dg.qiniup.com', 'up-fs.qiniup.com'] }
90+
},
91+
uc: { acc: { main: ['uc.qbox.me'] } },
92+
rs: { acc: { main: ['rs-z2.qbox.me'] } },
93+
rsf: { acc: { main: ['rsf-z2.qbox.me'] } },
94+
api: { acc: { main: ['api-z2.qiniu.com'] } }
95+
}
96+
})
97+
98+
return this.callInterceptor('getUpHosts', defaultData)
99+
}
100+
101+
public uploadChunk(): ReturnType<typeof api.uploadChunk> {
102+
const defaultData: ReturnType<typeof api.uploadChunk> = Promise.resolve({
103+
reqId: 'req-id',
104+
data: {
105+
etag: 'FuYYVJ1gmVCoGk5C5r5ftrLXxE6m',
106+
md5: '491309eddd8e7233e14eaa25216594b4'
107+
}
108+
})
109+
110+
return this.callInterceptor('uploadChunk', defaultData)
111+
}
112+
113+
public uploadComplete(): ReturnType<typeof api.uploadComplete> {
114+
const defaultData: ReturnType<typeof api.uploadComplete> = Promise.resolve({
115+
reqId: 'req-id',
116+
data: {
117+
key: 'test.zip',
118+
hash: 'lsril688bAmXn7kiiOe9fL4mpc39',
119+
fsize: 11009649,
120+
bucket: 'test',
121+
name: 'test'
122+
}
123+
})
124+
125+
return this.callInterceptor('uploadComplete', defaultData)
126+
}
127+
128+
public initUploadParts(): ReturnType<typeof api.initUploadParts> {
129+
const defaultData: ReturnType<typeof api.initUploadParts> = Promise.resolve({
130+
reqId: 'req-id',
131+
data: { uploadId: '60878b9408bc044043f5d74f', expireAt: 1620100628 }
132+
})
133+
134+
return this.callInterceptor('initUploadParts', defaultData)
135+
}
136+
137+
public deleteUploadedChunks(): ReturnType<typeof api.deleteUploadedChunks> {
138+
const defaultData: ReturnType<typeof api.deleteUploadedChunks> = Promise.resolve({
139+
reqId: 'req-id',
140+
data: undefined
141+
})
142+
143+
return this.callInterceptor('deleteUploadedChunks', defaultData)
144+
}
145+
}

src/api/index.test.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { region } from '../config'
44
import { getUploadUrl } from '.'
55

66
jest.mock('../utils', () => ({
7+
...jest.requireActual('../utils') as any,
8+
79
request: () => Promise.resolve({
810
data: {
911
up: {
@@ -27,7 +29,7 @@ describe('api function test', () => {
2729
retryCount: 3,
2830
checkByMD5: false,
2931
uphost: '',
30-
upprotocol: 'https:',
32+
upprotocol: 'https',
3133
forceDirect: false,
3234
chunkSize: DEFAULT_CHUNK_SIZE,
3335
concurrentRequestLimit: 3
@@ -43,6 +45,14 @@ describe('api function test', () => {
4345
url = await getUploadUrl(config, token)
4446
expect(url).toBe('https://upload.qiniup.com')
4547

48+
config.upprotocol = 'https'
49+
url = await getUploadUrl(config, token)
50+
expect(url).toBe('https://upload.qiniup.com')
51+
52+
config.upprotocol = 'http'
53+
url = await getUploadUrl(config, token)
54+
expect(url).toBe('http://upload.qiniup.com')
55+
4656
config.upprotocol = 'https:'
4757
url = await getUploadUrl(config, token)
4858
expect(url).toBe('https://upload.qiniup.com')

src/api/index.ts

+51-29
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,26 @@
1+
import { stringify } from 'querystring'
2+
3+
import { normalizeUploadConfig } from '../utils'
4+
import { Config, InternalConfig, UploadInfo } from '../upload'
15
import * as utils from '../utils'
2-
import { regionUphostMap } from '../config'
3-
import { Config, UploadInfo } from '../upload'
46

57
interface UpHosts {
68
data: {
79
up: {
810
acc: {
911
main: string[]
12+
backup: string[]
1013
}
1114
}
1215
}
1316
}
1417

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

21-
export type UploadUrlConfig = Partial<Pick<Config, 'upprotocol' | 'uphost' | 'region' | 'useCdnDomain'>>
22-
23-
/** 获取上传url */
24-
export async function getUploadUrl(config: UploadUrlConfig, token: string): Promise<string> {
25-
const protocol = config.upprotocol || 'https:'
26-
27-
if (config.uphost) {
28-
return `${protocol}//${config.uphost}`
29-
}
30-
31-
if (config.region) {
32-
const upHosts = regionUphostMap[config.region]
33-
const host = config.useCdnDomain ? upHosts.cdnUphost : upHosts.srcUphost
34-
return `${protocol}//${host}`
35-
}
36-
37-
const res = await getUpHosts(token, protocol)
38-
const hosts = res.data.up.acc.main
39-
return `${protocol}//${hosts[0]}`
40-
}
41-
4224
/**
4325
* @param bucket 空间名
4426
* @param key 目标文件名
@@ -96,7 +78,7 @@ export function uploadChunk(
9678
uploadInfo: UploadInfo,
9779
options: Partial<utils.RequestOptions>
9880
): utils.Response<UploadChunkData> {
99-
const bucket = utils.getPutPolicy(token).bucket
81+
const bucket = utils.getPutPolicy(token).bucketName
10082
const url = getBaseUrl(bucket, key, uploadInfo) + `/${index}`
10183
return utils.request<UploadChunkData>(url, {
10284
...options,
@@ -119,7 +101,7 @@ export function uploadComplete(
119101
uploadInfo: UploadInfo,
120102
options: Partial<utils.RequestOptions>
121103
): utils.Response<UploadCompleteData> {
122-
const bucket = utils.getPutPolicy(token).bucket
104+
const bucket = utils.getPutPolicy(token).bucketName
123105
const url = getBaseUrl(bucket, key, uploadInfo)
124106
return utils.request<UploadCompleteData>(url, {
125107
...options,
@@ -138,7 +120,7 @@ export function deleteUploadedChunks(
138120
key: string | null | undefined,
139121
uploadinfo: UploadInfo
140122
): utils.Response<void> {
141-
const bucket = utils.getPutPolicy(token).bucket
123+
const bucket = utils.getPutPolicy(token).bucketName
142124
const url = getBaseUrl(bucket, key, uploadinfo)
143125
return utils.request(
144126
url,
@@ -148,3 +130,43 @@ export function deleteUploadedChunks(
148130
}
149131
)
150132
}
133+
134+
/**
135+
* @param {string} url
136+
* @param {FormData} data
137+
* @param {Partial<utils.RequestOptions>} options
138+
* @returns Promise
139+
* @description 直传接口
140+
*/
141+
export function direct(
142+
url: string,
143+
data: FormData,
144+
options: Partial<utils.RequestOptions>
145+
): Promise<UploadCompleteData> {
146+
return utils.request<UploadCompleteData>(url, {
147+
method: 'POST',
148+
body: data,
149+
...options
150+
})
151+
}
152+
153+
export type UploadUrlConfig = Partial<Pick<Config, 'upprotocol' | 'uphost' | 'region' | 'useCdnDomain'>>
154+
155+
/**
156+
* @param {UploadUrlConfig} config
157+
* @param {string} token
158+
* @returns Promise
159+
* @description 获取上传 url
160+
*/
161+
export async function getUploadUrl(_config: UploadUrlConfig, token: string): Promise<string> {
162+
const config = normalizeUploadConfig(_config)
163+
const protocol = config.upprotocol
164+
165+
if (config.uphost.length > 0) {
166+
return `${protocol}://${config.uphost[0]}`
167+
}
168+
const putPolicy = utils.getPutPolicy(token)
169+
const res = await getUpHosts(putPolicy.assessKey, putPolicy.bucketName, protocol)
170+
const hosts = res.data.up.acc.main
171+
return `${protocol}://${hosts[0]}`
172+
}

src/config/index.ts

+1-32
Original file line numberDiff line numberDiff line change
@@ -1,32 +1 @@
1-
/** 上传区域 */
2-
export const region = {
3-
z0: 'z0',
4-
z1: 'z1',
5-
z2: 'z2',
6-
na0: 'na0',
7-
as0: 'as0'
8-
} as const
9-
10-
/** 上传区域对应的 host */
11-
export const regionUphostMap = {
12-
[region.z0]: {
13-
srcUphost: 'up.qiniup.com',
14-
cdnUphost: 'upload.qiniup.com'
15-
},
16-
[region.z1]: {
17-
srcUphost: 'up-z1.qiniup.com',
18-
cdnUphost: 'upload-z1.qiniup.com'
19-
},
20-
[region.z2]: {
21-
srcUphost: 'up-z2.qiniup.com',
22-
cdnUphost: 'upload-z2.qiniup.com'
23-
},
24-
[region.na0]: {
25-
srcUphost: 'up-na0.qiniup.com',
26-
cdnUphost: 'upload-na0.qiniup.com'
27-
},
28-
[region.as0]: {
29-
srcUphost: 'up-as0.qiniup.com',
30-
cdnUphost: 'upload-as0.qiniup.com'
31-
}
32-
} as const
1+
export * from './region'

src/config/region.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/** 上传区域 */
2+
export const region = {
3+
z0: 'z0',
4+
z1: 'z1',
5+
z2: 'z2',
6+
na0: 'na0',
7+
as0: 'as0',
8+
cnEast2: 'cn-east-2'
9+
} as const
10+
11+
/** 上传区域对应的 host */
12+
export const regionUphostMap = {
13+
[region.z0]: {
14+
srcUphost: ['up.qiniup.com'],
15+
cdnUphost: ['upload.qiniup.com']
16+
},
17+
[region.z1]: {
18+
srcUphost: ['up-z1.qiniup.com'],
19+
cdnUphost: ['upload-z1.qiniup.com']
20+
},
21+
[region.z2]: {
22+
srcUphost: ['up-z2.qiniup.com'],
23+
cdnUphost: ['upload-z2.qiniup.com']
24+
},
25+
[region.na0]: {
26+
srcUphost: ['up-na0.qiniup.com'],
27+
cdnUphost: ['upload-na0.qiniup.com']
28+
},
29+
[region.as0]: {
30+
srcUphost: ['up-as0.qiniup.com'],
31+
cdnUphost: ['upload-as0.qiniup.com']
32+
},
33+
[region.cnEast2]: {
34+
srcUphost: ['up-cn-east-2.qiniup.com'],
35+
cdnUphost: ['upload-cn-east-2.qiniup.com']
36+
}
37+
} as const

0 commit comments

Comments
 (0)