Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
yinxulai committed Dec 15, 2023
1 parent cb09f28 commit ccb7afc
Show file tree
Hide file tree
Showing 19 changed files with 1,128 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/browser/src/utils/crc32.ts
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 实现参考
Expand Down
259 changes: 259 additions & 0 deletions packages/common/src/api/index.ts
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)
}
}
31 changes: 31 additions & 0 deletions packages/common/src/helper/base64/index.test.ts
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)
}
})
})
Loading

0 comments on commit ccb7afc

Please sign in to comment.