初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
import type OSS from 'ali-oss'
import {
getSTSToken,
getOssSignatureUrl,
checkOssSignatureUrlIsExist,
} from '@apis/oss'
import type { STSTokenResponse, UploadParams, UploadResult } from '../type'
import { OSSBucketType } from '../type'
import { StorageAdapter } from './base'
// 常量定义
const OSS_REGION = 'oss-cn-hangzhou'
export class OSSClient extends StorageAdapter<OSS> {
private static instance: OSSClient
// 获取默认实例键值
protected getDefaultInstanceKey(): string {
return 'bty-oss-token'
}
// 获取提供商前缀
protected getProviderPrefix(): string {
return 'custom-oss'
}
// 默认获取STS Token的方法
protected async defaultGetSTSToken(): Promise<STSTokenResponse> {
return getSTSToken()
}
// 获取默认OSSClient单例
public static getInstance(): OSSClient {
if (!OSSClient.instance) {
OSSClient.instance = new OSSClient()
}
return OSSClient.instance
}
// 创建自定义OSSClient实例
public static createCustomInstance(
getSTSTokenFn: () => Promise<STSTokenResponse>,
): OSSClient {
return new OSSClient(getSTSTokenFn)
}
// 刷新OSS客户端
protected async refreshClient(): Promise<void> {
const token = await this.getSTSToken()
const aliOssModule: any = await import('ali-oss')
const OSSSdk: typeof OSS = aliOssModule.default || (aliOssModule as any)
this.client = new OSSSdk({
region: token.region || OSS_REGION,
accessKeyId: token.access_key_id,
accessKeySecret: token.access_key_secret,
stsToken: token.security_token,
secure: true,
...(Reflect.has(token, 'secure') ? { secure: token.secure } : {}),
})
this.bucketMap = {
[OSSBucketType.PRIVATE]: token.data_bucket,
[OSSBucketType.PUBLIC]: token.resource_bucket,
} as Record<OSSBucketType, string>
}
// 上传文件
public async upload(params: UploadParams): Promise<UploadResult> {
try {
const client = await this.getClient()
const bucketName =
params.custom_bucket ||
this.getBucketName(params.bucketType || OSSBucketType.PRIVATE)
client!.useBucket(bucketName)
const result = await client.put(
params.filePath,
params.file,
params.options || {},
)
const url = (result.res as any)?.requestUrls?.[0]?.split('?')?.[0]
return { url, result }
} catch (error) {
console.error('File upload failed:', error)
throw new Error(`File upload failed: ${(error as Error).message}`)
}
}
public async multipartUpload(params: UploadParams): Promise<UploadResult> {
try {
const client = await this.getClient()
const bucketName =
params.custom_bucket ||
this.getBucketName(params.bucketType || OSSBucketType.PRIVATE)
client!.useBucket(bucketName)
const result = await client.multipartUpload(
params.filePath,
params.file,
params.options || {},
)
const url = (result.res as any)?.requestUrls?.[0]?.split('?')?.[0]
return { url, result }
} catch (error) {
console.error('File upload failed:', error)
throw new Error(`File upload failed: ${(error as Error).message}`)
}
}
public async getOssSignatureUrl(key: string, params?: Record<string, any>) {
return getOssSignatureUrl(key, params)
}
public async checkOssSignatureUrlIsExist(url: string) {
return checkOssSignatureUrlIsExist(url)
}
}

View File

@@ -0,0 +1,128 @@
import OssSingleton from '../OssSingleton'
import type {
STSTokenResponse,
OSSBucketType,
UploadParams,
UploadResult,
} from '../type'
// 常量定义
// 最大缓存时长:无论 token 实际有效期多久,最多只缓存 10 分钟
export const MAX_TOKEN_CACHE_DURATION = 10 * 60 * 1000 // 10 分钟
/**
* 根据 token 的 expiration 字段计算缓存时长
* @param tokenResponse STS Token 响应
* @returns 缓存时长(毫秒)
*/
export function calculateTokenCacheDuration(
tokenResponse: STSTokenResponse,
): number {
// 如果没有 expiration 字段,使用最大缓存时长
if (!tokenResponse.expiration) {
return MAX_TOKEN_CACHE_DURATION
}
try {
const expirationTime = new Date(tokenResponse.expiration).getTime()
const remainingTime = expirationTime - Date.now()
// token 已过期,不缓存
if (remainingTime <= 0) {
return 0
}
// 取服务端返回的时间和最大缓存时长中的较小值
const cacheDuration = Math.min(remainingTime, MAX_TOKEN_CACHE_DURATION)
console.log('[Token Cache] Cache duration:', cacheDuration, 'ms')
return cacheDuration
} catch (error) {
console.warn('[Token Cache] Parse error, using max duration:', error)
return MAX_TOKEN_CACHE_DURATION
}
}
// 存储适配器抽象基类
export abstract class StorageAdapter<C = any> {
protected client: C | null = null
protected instanceKey: string | null = null
protected bucketMap: Record<OSSBucketType, string> = {} as Record<
OSSBucketType,
string
>
protected getSTSTokenFn: () => Promise<STSTokenResponse>
constructor(getSTSTokenFn?: () => Promise<STSTokenResponse>) {
if (getSTSTokenFn) {
this.getSTSTokenFn = getSTSTokenFn
this.instanceKey = this.generateInstanceKey(getSTSTokenFn)
} else {
this.getSTSTokenFn = this.defaultGetSTSToken
this.instanceKey = this.getDefaultInstanceKey()
}
}
// 生成实例唯一键值
protected generateInstanceKey(fn: (...args: any[]) => any): string {
const fnString = fn.toString()
let hash = 0
for (let i = 0; i < fnString.length; i++) {
const char = fnString.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash
}
return `${this.getProviderPrefix()}-token-${hash}`
}
// 获取默认实例键值
protected abstract getDefaultInstanceKey(): string
// 获取提供商前缀
protected abstract getProviderPrefix(): string
// 默认获取STS Token的方法
protected abstract defaultGetSTSToken(): Promise<STSTokenResponse>
// 获取并可能刷新STS Token
protected async getSTSToken(): Promise<STSTokenResponse> {
return OssSingleton.getCachedData<STSTokenResponse>(
this.instanceKey!,
this.getSTSTokenFn,
// 传入函数来动态计算缓存时长
calculateTokenCacheDuration,
)
}
// 刷新客户端
protected abstract refreshClient(): Promise<void>
// 获取客户端
public async getClient(): Promise<C> {
// 检查 token 缓存是否过期
const tokenExpired = OssSingleton.isCacheExpired(this.instanceKey!)
// 如果 client 不存在或 token 已过期,则刷新 client
if (!this.client || tokenExpired) {
console.log('[Client] Refreshing client, token expired:', tokenExpired)
await this.refreshClient()
}
return this.client!
}
// 获取存储桶名称
protected getBucketName(bucketType: OSSBucketType): string {
return this.bucketMap[bucketType]
}
// 抽象方法,子类必须实现
public abstract upload(params: UploadParams): Promise<UploadResult>
public abstract multipartUpload(params: UploadParams): Promise<UploadResult>
public abstract getOssSignatureUrl(
key: string,
params?: Record<string, any>,
): Promise<string>
public abstract checkOssSignatureUrlIsExist(url: string): Promise<boolean>
}

View File

@@ -0,0 +1,207 @@
import {
S3Client,
PutObjectCommand,
CreateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
} from '@aws-sdk/client-s3'
import {
getOssSignatureUrl as apiGetOssSignatureUrl,
checkOssSignatureUrlIsExist as apiCheckOssSignatureUrlIsExist,
getSTSToken,
} from '@apis/oss'
import type { STSTokenResponse, UploadParams, UploadResult } from '../type'
import { OSSBucketType } from '../type'
import { StorageAdapter } from './base'
// 添加常量
const PART_SIZE = 5 * 1024 * 1024 // 5MB
export class MinIOAdapter extends StorageAdapter<S3Client> {
private static instance: MinIOAdapter
private config: Record<string, any> = {}
// 获取默认实例键值
protected getDefaultInstanceKey(): string {
return 'minio-sts-token'
}
// 获取提供商前缀
protected getProviderPrefix(): string {
return 'custom-minio'
}
// 默认获取STS Token的方法
protected async defaultGetSTSToken(): Promise<STSTokenResponse> {
const token = await getSTSToken()
return token
}
public static getInstance(): MinIOAdapter {
if (!MinIOAdapter.instance) {
MinIOAdapter.instance = new MinIOAdapter()
}
return MinIOAdapter.instance
}
// 添加创建自定义实例的静态方法
public static createCustomInstance(
getSTSTokenFn: () => Promise<STSTokenResponse>,
): MinIOAdapter {
return new MinIOAdapter(getSTSTokenFn)
}
// 刷新S3客户端
protected async refreshClient(): Promise<void> {
try {
// 获取最新的token
const token = await this.getSTSToken()
this.config = {
...token,
endpoint: token.server_endpoint,
}
this.client = new S3Client({
region: token.region,
endpoint: token.server_endpoint,
credentials: {
accessKeyId: token.access_key_id,
secretAccessKey: token.access_key_secret,
...(token.security_token
? { sessionToken: token.security_token }
: {}),
},
forcePathStyle: true,
})
this.bucketMap = {
[OSSBucketType.PRIVATE]: token.data_bucket!,
[OSSBucketType.PUBLIC]: token.resource_bucket!,
}
} catch (error) {
throw new Error(`MinIO客户端初始化失败: ${(error as Error).message}`)
}
}
// 优化上传方法,提取公共逻辑
private getUploadBucketAndUrl(params: UploadParams): {
bucketName: string
urlBase: string
} {
const bucketName =
params.custom_bucket ||
this.getBucketName(params.bucketType || OSSBucketType.PRIVATE)
const urlBase = `${this.config.endpoint}/${bucketName}/${params.filePath}`
return { bucketName, urlBase }
}
async upload(params: UploadParams): Promise<UploadResult> {
try {
const client = await this.getClient()
const { bucketName, urlBase } = this.getUploadBucketAndUrl(params)
const body = new Uint8Array(await params.file.arrayBuffer())
const command = new PutObjectCommand({
Bucket: bucketName,
Key: params.filePath,
Body: body,
ContentType: params.file.type,
...params.options,
})
const result = await client.send(command)
const url = urlBase
return {
url,
result: {
res: {
requestUrls: [url],
...result,
},
},
}
} catch (error) {
console.error('文件上传失败:', error)
throw new Error(`文件上传失败: ${(error as Error).message}`)
}
}
async multipartUpload(params: UploadParams): Promise<UploadResult> {
try {
const client = await this.getClient()
const { bucketName, urlBase } = this.getUploadBucketAndUrl(params)
const createCommand = new CreateMultipartUploadCommand({
Bucket: bucketName,
Key: params.filePath,
ContentType: params.file.type,
...params.options,
})
const createResponse = await client.send(createCommand)
const uploadId = createResponse.UploadId
if (!uploadId) {
throw new Error('初始化分片上传失败')
}
const fileBuffer = await params.file.arrayBuffer()
const totalParts = Math.ceil(fileBuffer.byteLength / PART_SIZE)
const uploadPromises = []
for (let partNumber = 1; partNumber <= totalParts; partNumber++) {
const start = (partNumber - 1) * PART_SIZE
const end = Math.min(start + PART_SIZE, fileBuffer.byteLength)
const partBuffer = fileBuffer.slice(start, end)
const uploadPartCommand = new UploadPartCommand({
Bucket: bucketName,
Key: params.filePath,
PartNumber: partNumber,
UploadId: uploadId,
Body: new Uint8Array(partBuffer),
})
uploadPromises.push(client.send(uploadPartCommand))
}
const uploadResults = await Promise.all(uploadPromises)
const completeCommand = new CompleteMultipartUploadCommand({
Bucket: bucketName,
Key: params.filePath,
UploadId: uploadId,
MultipartUpload: {
Parts: uploadResults.map((result, index) => ({
ETag: result.ETag,
PartNumber: index + 1,
})),
},
})
const completeResponse = await client.send(completeCommand)
const url = urlBase
return {
url,
result: {
res: {
requestUrls: [url],
...completeResponse,
},
},
}
} catch (error) {
console.error('分片上传失败:', error)
throw new Error(`分片上传失败: ${(error as Error).message}`)
}
}
async getOssSignatureUrl(
key: string,
params?: Record<string, any>,
): Promise<string> {
return apiGetOssSignatureUrl(key, params)
}
async checkOssSignatureUrlIsExist(): Promise<boolean> {
return apiCheckOssSignatureUrlIsExist()
}
}