-
Notifications
You must be signed in to change notification settings - Fork 519
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
1,128 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
/* eslint-disable no-bitwise */ | ||
|
||
import { MB } from './helper' | ||
const MB = 1024 ** 2 | ||
|
||
/** | ||
* 以下 class 实现参考 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
import { urlSafeBase64Encode } from '../helper/base64' | ||
import { HttpAbort, HttpClient, HttpHeader, OnHttpProgress } from '../types/http' | ||
import { InnerTokenProvider, TokenProvider } from '../types/token' | ||
import { Result, isErrorResult } from '../types/types' | ||
import { HostProvider } from '../upload/common/host' | ||
|
||
interface BasicParams { | ||
abort?: HttpAbort | ||
onProgress?: OnHttpProgress | ||
} | ||
|
||
interface BasicWithAuthParams extends BasicParams { | ||
token: string | ||
} | ||
|
||
interface DirectUploadParams extends BasicWithAuthParams { | ||
|
||
} | ||
|
||
interface InitMultipartUploadParams extends BasicWithAuthParams { | ||
key?: string | ||
bucket: string | ||
} | ||
|
||
interface UploadPartParams extends BasicWithAuthParams { | ||
bucket: string | ||
uploadId: string | ||
part: IBlob | ||
partIndex: number | ||
md5?: string | ||
key?: string | ||
} | ||
|
||
interface ListMultipartUploadPartsParams extends BasicWithAuthParams { | ||
uploadId: string | ||
bucket: string | ||
key?: string | ||
} | ||
|
||
interface ListMultipartUploadPartsResponse { | ||
uploadId: string | ||
expireAt: number | ||
partNumberMarker: number | ||
parts: Array<{ | ||
Etag: string | ||
Size: number | ||
PutTime: number | ||
PartNumber: number | ||
}> | ||
} | ||
|
||
interface CompleteMultipartUploadParams extends BasicWithAuthParams { | ||
uploadId: string | ||
md5?: string | ||
key?: string | ||
} | ||
|
||
interface AbortMultipartUploadParams extends BasicWithAuthParams { | ||
uploadId: string | ||
key?: string | ||
} | ||
|
||
interface UploadChunkData { | ||
etag: string | ||
md5: string | ||
} | ||
|
||
interface InitPartsData { | ||
/** 该文件的上传 id, 后续该文件其他各个块的上传,已上传块的废弃,已上传块的合成文件,都需要该 id */ | ||
uploadId: string | ||
/** uploadId 的过期时间 */ | ||
expireAt: number | ||
} | ||
|
||
type UploadCompleteData<T = any> = T | ||
|
||
interface GetChunkRequestPathParams extends BasicWithAuthParams { | ||
key?: string | ||
} | ||
|
||
interface DirectUploadParams extends BasicWithAuthParams { | ||
file: IFile | ||
key?: string | ||
|
||
crc32?: string | ||
meta?: string[] | ||
accept?: string | ||
fileName?: string | ||
custom_name?: string | ||
custom_value?: string | ||
} | ||
|
||
export class UploadApis { | ||
constructor( | ||
/** http 请求客户端;通过实现不同的 HttpClient 来实现多环境支持 */ | ||
private httpClient: HttpClient, | ||
/** 上传 host 池;提供了获取和管理上传 host 的能力 */ | ||
private hostProvider: HostProvider, | ||
/** token 获取器;api 会自动通过该对象获取 token */ | ||
private tokenProvider: InnerTokenProvider | ||
) {} | ||
|
||
private generateAuthHeaders(token: string): HttpHeader { | ||
const auth = 'UpToken ' + token | ||
return { Authorization: auth } | ||
} | ||
|
||
private async getBaseRequestPath(params: GetChunkRequestPathParams): Promise<Result<string>> { | ||
const tokenResult = await this.tokenProvider.getUploadToken(params) | ||
if (isErrorResult(tokenResult)) return tokenResult | ||
const hostResult = await this.hostProvider.getUploadHost(tokenResult.result) | ||
if (isErrorResult(hostResult)) return hostResult | ||
const uploadHostUrl = hostResult.result.getUrl() | ||
|
||
const realKey = params.key != null ? urlSafeBase64Encode(params.key) : '~' | ||
const url = `${uploadHostUrl}/buckets/${tokenResult.result.bucket}/objects/${realKey}/uploads` | ||
return { result: url } | ||
} | ||
|
||
async initMultipartUpload(params: InitMultipartUploadParams) { | ||
const requestPathResult = await this.getBaseRequestPath(params) | ||
if (isErrorResult(requestPathResult)) return requestPathResult | ||
|
||
const headers = this.generateAuthHeaders(params.token) | ||
headers['content-type'] = 'application/json' | ||
return this.httpClient.post<InitPartsData>(requestPathResult.result, { | ||
headers, | ||
abort: params.abort, | ||
onProgress: params.onProgress | ||
}) | ||
} | ||
|
||
async uploadPart(params: UploadPartParams) { | ||
const requestPathResult = await this.getBaseRequestPath(params) | ||
if (isErrorResult(requestPathResult)) return requestPathResult | ||
|
||
const url = `${requestPathResult.result}/${params.uploadId}/${params.partIndex}` | ||
const headers = this.generateAuthHeaders(params.token) | ||
headers['content-type'] = 'application/json' | ||
if (params.md5) headers['Content-MD5'] = params.md5 | ||
return this.httpClient.put<UploadChunkData>(url, { | ||
onProgress: params.onProgress, | ||
abort: params.abort, | ||
body: params.part, | ||
headers | ||
}) | ||
} | ||
|
||
async listMultipartUploadParts(params: ListMultipartUploadPartsParams) { | ||
const requestPathResult = await this.getBaseRequestPath(params) | ||
if (isErrorResult(requestPathResult)) return requestPathResult | ||
|
||
const url = `${requestPathResult.result}/${params.uploadId}` | ||
const headers = this.generateAuthHeaders(params.token) | ||
headers['content-type'] = 'application/json' | ||
return this.httpClient.get<ListMultipartUploadPartsResponse>(url, { | ||
headers, | ||
abort: params.abort, | ||
onProgress: params.onProgress | ||
}) | ||
} | ||
|
||
async completeMultipartUpload<T = unknown>(params: CompleteMultipartUploadParams) { | ||
const requestPathResult = await this.getBaseRequestPath(params) | ||
if (isErrorResult(requestPathResult)) return requestPathResult | ||
|
||
const url = `${requestPathResult.result}/${params.uploadId}` | ||
const headers = this.generateAuthHeaders(params.token) | ||
headers['content-type'] = 'application/json' | ||
return this.httpClient.post<UploadCompleteData<T>>(url, { | ||
headers, | ||
abort: params.abort, | ||
onProgress: params.onProgress | ||
}) | ||
} | ||
|
||
async abortMultipartUpload(params: AbortMultipartUploadParams) { | ||
const requestPathResult = await this.getBaseRequestPath(params) | ||
if (isErrorResult(requestPathResult)) return requestPathResult | ||
|
||
const url = `${requestPathResult.result}/${params.uploadId}` | ||
const headers = this.generateAuthHeaders(params.token) | ||
headers['content-type'] = 'application/json' | ||
return this.httpClient.delete(url, { | ||
headers, | ||
abort: params.abort, | ||
onProgress: params.onProgress | ||
}) | ||
} | ||
|
||
async directUpload<T>(params: DirectUploadParams) { | ||
const tokenResult = await this.tokenProvider.getUploadToken(params) | ||
if (isErrorResult(tokenResult)) return tokenResult | ||
const hostResult = await this.hostProvider.getUploadHost(tokenResult.result) | ||
if (isErrorResult(hostResult)) return hostResult | ||
const uploadHostUrl = hostResult.result.getUrl() | ||
|
||
return this.httpClient.post<UploadCompleteData<T>>(uploadHostUrl, { | ||
abort: params.abort, | ||
onProgress: params.onProgress, | ||
headers: { 'content-type': 'multipart/form-data' }, | ||
body: { | ||
resource_key: params.key, | ||
upload_token: params.token, | ||
fileBinaryData: params.file, | ||
custom_name: params.custom_name, | ||
custom_value: params.custom_value, | ||
crc32: params.crc32, | ||
accept: params.accept, | ||
fileName: params.fileName, | ||
'x-qn-meta': params.meta | ||
} | ||
}) | ||
} | ||
} | ||
|
||
interface GetHostConfigParams { | ||
serverUrl: string | ||
accessKey: string | ||
bucket: string | ||
} | ||
|
||
interface HostConfig { | ||
hosts: Array<{ | ||
region: string | ||
ttl: number | ||
up: { | ||
domains: string[] | ||
old: string[] | ||
} | ||
io: { | ||
domains: string[] | ||
old: string[] | ||
} | ||
io_src: { | ||
domains: string[] | ||
} | ||
s3: { | ||
region_alias: string | ||
domains: string[] | ||
} | ||
}> | ||
} | ||
|
||
export class ConfigApis { | ||
constructor( | ||
/** http 请求客户端;通过实现不同的 HttpClient 来实现多环境支持 */ | ||
private httpClient: HttpClient | ||
) {} | ||
|
||
/** 从服务中心获取接口服务地址 */ | ||
async getHostConfig(params: GetHostConfigParams) { | ||
/** 从配置中心获取上传服务地址 */ | ||
const query = `ak=${encodeURIComponent(params.accessKey)}&bucket=${encodeURIComponent(params.bucket)}` | ||
// TODO: 支持设置,私有云自动获取上传地址 | ||
const url = `${params.serverUrl}/v4/query?${query}` | ||
return this.httpClient.get<HostConfig>(url) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import * as base64 from '.' | ||
|
||
// 测试用例来自以下地址 | ||
// https://github.com/LinusU/encode-utf8/blob/bd6c09b1c67baafc51853b1bea0e80bfe1e69ed0/test.js | ||
const testCases = [ | ||
['正', '5q2j'], | ||
['𝌆', '8J2Mhg'], | ||
['💩', '8J-SqQ'], | ||
['Hello, World!', 'SGVsbG8sIFdvcmxkIQ'], | ||
['🐵 🙈 🙉 🙊', '8J-QtSDwn5mIIPCfmYkg8J-Zig'], | ||
['åß∂ƒ©˙∆˚¬…æ', 'w6XDn-KIgsaSwqnLmeKIhsuawqzigKbDpg'], | ||
['사회과학원 어학연구소', '7IKs7ZqM6rO87ZWZ7JuQIOyWtO2VmeyXsOq1rOyGjA'], | ||
['゚・✿ヾ╲(。◕‿◕。)╱✿・゚', '776f772l4py_44O-4pWyKO-9oeKXleKAv-KXle-9oSnilbHinL_vvaXvvp8'], | ||
['Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗', 'UG93ZXLZhNmP2YTZj9i12ZHYqNmP2YTZj9mE2LXZkdio2Y_Ysdix2Ysg4KWjIOClo2gg4KWjIOClo-WGlw'], | ||
['𝕿𝖍𝖊 𝖖𝖚𝖎𝖈𝖐 𝖇𝖗𝖔𝖜𝖓 𝖋𝖔𝖝 𝖏𝖚𝖒𝖕𝖘 𝖔𝖛𝖊𝖗 𝖙𝖍𝖊 𝖑𝖆𝖟𝖞 𝖉𝖔𝖌', '8J2Vv_Cdlo3wnZaKIPCdlpbwnZaa8J2WjvCdlojwnZaQIPCdlofwnZaX8J2WlPCdlpzwnZaTIPCdlovwnZaU8J2WnSDwnZaP8J2WmvCdlpLwnZaV8J2WmCDwnZaU8J2Wm_CdlorwnZaXIPCdlpnwnZaN8J2WiiDwnZaR8J2WhvCdlp_wnZaeIPCdlonwnZaU8J2WjA'] | ||
] | ||
|
||
describe('test base64', () => { | ||
test('urlSafeBase64Encode', () => { | ||
for (const [input, expected] of testCases) { | ||
const actual = base64.urlSafeBase64Encode(input) | ||
expect(actual).toMatch(expected) | ||
} | ||
}) | ||
test('urlSafeBase64Decode', () => { | ||
for (const [expected, input] of testCases) { | ||
const actual = base64.urlSafeBase64Decode(input) | ||
expect(actual).toMatch(expected) | ||
} | ||
}) | ||
}) |
Oops, something went wrong.