初始化模版工程
This commit is contained in:
84
package/uploader/src/OssSingleton.ts
Normal file
84
package/uploader/src/OssSingleton.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Token 缓存和并发请求防重工具类
|
||||
* 1. 确保同一个 key 的并发请求只会真正执行一次
|
||||
* 2. 缓存 token 并管理过期时间
|
||||
*/
|
||||
|
||||
import { MAX_TOKEN_CACHE_DURATION } from './adapter/base'
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T
|
||||
expirationTime: number
|
||||
}
|
||||
|
||||
export default class OssSingleton {
|
||||
private static pendingRequests: Map<string, Promise<any>> = new Map()
|
||||
private static cache: Map<string, CacheEntry<any>> = new Map()
|
||||
|
||||
/**
|
||||
* 获取或创建带缓存的请求
|
||||
* @param key 请求的唯一标识
|
||||
* @param callback 实际的请求函数
|
||||
* @param getCacheDuration 计算缓存时长的函数,接收 callback 返回的数据,返回缓存时长(毫秒)
|
||||
* @returns Promise<T>
|
||||
*/
|
||||
public static async getCachedData<T>(
|
||||
key: string,
|
||||
callback: () => Promise<T>,
|
||||
getCacheDuration?: number | ((data: T) => number),
|
||||
): Promise<T> {
|
||||
const currentTime = Date.now()
|
||||
|
||||
// 检查缓存是否有效
|
||||
const cachedEntry = OssSingleton.cache.get(key)
|
||||
if (cachedEntry && currentTime < cachedEntry.expirationTime) {
|
||||
return cachedEntry.data as T
|
||||
}
|
||||
|
||||
// 如果已经有相同的请求在进行中,直接返回该 Promise
|
||||
if (OssSingleton.pendingRequests.has(key)) {
|
||||
return OssSingleton.pendingRequests.get(key)! as Promise<T>
|
||||
}
|
||||
|
||||
// 创建新的请求 Promise
|
||||
const promise = callback()
|
||||
.then(data => {
|
||||
// 计算缓存时长
|
||||
let cacheDuration: number
|
||||
if (typeof getCacheDuration === 'function') {
|
||||
cacheDuration = getCacheDuration(data)
|
||||
} else if (typeof getCacheDuration === 'number') {
|
||||
cacheDuration = getCacheDuration
|
||||
} else {
|
||||
cacheDuration = MAX_TOKEN_CACHE_DURATION
|
||||
}
|
||||
|
||||
// 缓存数据并设置过期时间
|
||||
OssSingleton.cache.set(key, {
|
||||
data,
|
||||
expirationTime: Date.now() + cacheDuration,
|
||||
})
|
||||
return data
|
||||
})
|
||||
.finally(() => {
|
||||
// 请求完成后清理 pending 状态
|
||||
OssSingleton.pendingRequests.delete(key)
|
||||
})
|
||||
|
||||
OssSingleton.pendingRequests.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否已过期
|
||||
* @param key 缓存键
|
||||
* @returns 如果缓存不存在或已过期返回 true,否则返回 false
|
||||
*/
|
||||
public static isCacheExpired(key: string): boolean {
|
||||
const cachedEntry = OssSingleton.cache.get(key)
|
||||
if (!cachedEntry) {
|
||||
return true
|
||||
}
|
||||
return Date.now() >= cachedEntry.expirationTime
|
||||
}
|
||||
}
|
||||
115
package/uploader/src/adapter/alioss.ts
Normal file
115
package/uploader/src/adapter/alioss.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
128
package/uploader/src/adapter/base.ts
Normal file
128
package/uploader/src/adapter/base.ts
Normal 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>
|
||||
}
|
||||
207
package/uploader/src/adapter/minio.ts
Normal file
207
package/uploader/src/adapter/minio.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
53
package/uploader/src/index.ts
Normal file
53
package/uploader/src/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { PrivateOSSType } from '@bty/constant'
|
||||
import { OSSClient } from './adapter/alioss'
|
||||
import { MinIOAdapter } from './adapter/minio'
|
||||
import type { STSTokenResponse } from './type'
|
||||
|
||||
export * from './type'
|
||||
|
||||
const StorageType = {
|
||||
AliOSS: PrivateOSSType.ALIYUN,
|
||||
MinIO: PrivateOSSType.MINIO,
|
||||
} as const
|
||||
|
||||
type StorageType = (typeof StorageType)[keyof typeof StorageType]
|
||||
|
||||
function checkIsMinIOVersion() {
|
||||
try {
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isMinIOVersion = checkIsMinIOVersion()
|
||||
|
||||
const STORAGE_TYPE: StorageType = isMinIOVersion
|
||||
? StorageType.MinIO
|
||||
: StorageType.AliOSS
|
||||
|
||||
// 根据环境变量选择存储适配器
|
||||
function getAdapterClass() {
|
||||
switch (STORAGE_TYPE) {
|
||||
case StorageType.MinIO:
|
||||
return MinIOAdapter
|
||||
case StorageType.AliOSS:
|
||||
default:
|
||||
return OSSClient
|
||||
}
|
||||
}
|
||||
|
||||
// 获取适配器类
|
||||
const AdapterClass = getAdapterClass()
|
||||
|
||||
// 导出工厂方法,根据环境变量创建相应的适配器实例
|
||||
export function createCustomOSSUploader(
|
||||
tokenFn?: () => Promise<STSTokenResponse>,
|
||||
) {
|
||||
return tokenFn
|
||||
? AdapterClass.createCustomInstance(tokenFn)
|
||||
: AdapterClass.getInstance()
|
||||
}
|
||||
|
||||
// 默认导出实例,根据环境变量选择适当的实现
|
||||
export const ossUploader = AdapterClass.getInstance()
|
||||
35
package/uploader/src/type.ts
Normal file
35
package/uploader/src/type.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// 存储桶类型
|
||||
export const OSSBucketType = {
|
||||
PRIVATE: 'PRIVATE',
|
||||
PUBLIC: 'PUBLIC',
|
||||
} as const
|
||||
|
||||
export type OSSBucketType = (typeof OSSBucketType)[keyof typeof OSSBucketType]
|
||||
|
||||
// 统一的上传参数接口
|
||||
export interface UploadParams {
|
||||
file: File
|
||||
filePath: string
|
||||
bucketType?: OSSBucketType
|
||||
options?: Record<string, any>
|
||||
custom_bucket?: string
|
||||
}
|
||||
|
||||
// 统一的上传结果接口
|
||||
export interface UploadResult {
|
||||
url: string
|
||||
result: any
|
||||
}
|
||||
|
||||
export interface STSTokenResponse {
|
||||
server_endpoint?: string | undefined
|
||||
access_key_id: string
|
||||
access_key_secret: string
|
||||
security_token?: string
|
||||
expiration?: string
|
||||
data_bucket?: string
|
||||
resource_bucket?: string
|
||||
region?: string
|
||||
secure?: boolean
|
||||
endpoint?: string
|
||||
}
|
||||
Reference in New Issue
Block a user