143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import COS from 'cos-js-sdk-v5'
|
||
|
||
import {
|
||
confirmOssUpload,
|
||
getOssCredentials,
|
||
type OssCredentialsResponse
|
||
} from '@/api/file'
|
||
|
||
export type DirectUploadType = 'video'
|
||
|
||
export interface DirectUploadProgress {
|
||
/** 0-100 */
|
||
percent: number
|
||
loaded: number
|
||
total: number
|
||
speed: number
|
||
}
|
||
|
||
export interface DirectUploadResult {
|
||
id: number
|
||
cid: number
|
||
type: number
|
||
name: string
|
||
uri: string
|
||
url: string
|
||
}
|
||
|
||
export interface DirectUploadOptions {
|
||
file: File
|
||
type: DirectUploadType
|
||
cid?: number
|
||
onProgress?: (progress: DirectUploadProgress) => void
|
||
/** 返回 false 时由调用方自行降级到老链路 */
|
||
onFallback?: (provider: string) => boolean | void
|
||
}
|
||
|
||
const SLICE_SIZE = 5 * 1024 * 1024 // 5MB
|
||
const ASYNC_LIMIT = 3
|
||
|
||
function buildKey(prefix: string, file: File): string {
|
||
const ext = (file.name.split('.').pop() || 'mp4').toLowerCase()
|
||
const ts = Date.now()
|
||
const rand = Math.random().toString(36).slice(2, 10)
|
||
return `${prefix}${ts}-${rand}.${ext}`
|
||
}
|
||
|
||
/**
|
||
* 直传到腾讯云 COS(含 STS 凭证申请、分片上传、回执)
|
||
* 不支持降级 / fallback=true 时抛错,由调用方决定走老链路。
|
||
*/
|
||
export async function uploadVideoDirectToCos(options: DirectUploadOptions): Promise<DirectUploadResult> {
|
||
const credentials: OssCredentialsResponse = await getOssCredentials({ type: options.type })
|
||
|
||
if (credentials.fallback) {
|
||
const handled = options.onFallback?.(credentials.provider) ?? false
|
||
if (!handled) {
|
||
throw new DirectUploadFallbackError(credentials.provider)
|
||
}
|
||
// 调用方自行处理,本函数早退
|
||
throw new DirectUploadFallbackError(credentials.provider)
|
||
}
|
||
|
||
if (!credentials.credentials || !credentials.bucket || !credentials.region || !credentials.key_prefix) {
|
||
throw new Error('STS 凭证返回缺失字段')
|
||
}
|
||
|
||
if (credentials.max_size && options.file.size > credentials.max_size) {
|
||
const mb = Math.round(credentials.max_size / 1024 / 1024)
|
||
throw new Error(`视频体积超出上限(${mb}MB)`)
|
||
}
|
||
|
||
const cred = credentials.credentials
|
||
const expiredTime = credentials.expired_time ?? 0
|
||
const startTime = credentials.start_time ?? Math.floor(Date.now() / 1000)
|
||
|
||
const cos = new COS({
|
||
getAuthorization(_options, callback) {
|
||
callback({
|
||
TmpSecretId: cred.tmpSecretId,
|
||
TmpSecretKey: cred.tmpSecretKey,
|
||
SecurityToken: cred.sessionToken,
|
||
StartTime: startTime,
|
||
ExpiredTime: expiredTime
|
||
})
|
||
}
|
||
})
|
||
|
||
const key = buildKey(credentials.key_prefix, options.file)
|
||
|
||
await new Promise<void>((resolve, reject) => {
|
||
cos.uploadFile(
|
||
{
|
||
Bucket: credentials.bucket as string,
|
||
Region: credentials.region as string,
|
||
Key: key,
|
||
Body: options.file,
|
||
SliceSize: SLICE_SIZE,
|
||
AsyncLimit: ASYNC_LIMIT,
|
||
onProgress(info) {
|
||
if (!options.onProgress) return
|
||
const total = info.total || options.file.size
|
||
options.onProgress({
|
||
percent: Math.min(99, Math.round((info.percent || 0) * 100)),
|
||
loaded: info.loaded || 0,
|
||
total,
|
||
speed: info.speed || 0
|
||
})
|
||
}
|
||
},
|
||
(err) => {
|
||
if (err) {
|
||
reject(err)
|
||
return
|
||
}
|
||
resolve()
|
||
}
|
||
)
|
||
})
|
||
|
||
const confirmed = await confirmOssUpload({
|
||
type: options.type,
|
||
key,
|
||
name: options.file.name,
|
||
size: options.file.size,
|
||
content_type: options.file.type || '',
|
||
cid: options.cid ?? 0
|
||
})
|
||
|
||
options.onProgress?.({ percent: 100, loaded: options.file.size, total: options.file.size, speed: 0 })
|
||
|
||
return confirmed
|
||
}
|
||
|
||
/** 当后端 fallback=true 时抛出,调用方据此降级到老链路 */
|
||
export class DirectUploadFallbackError extends Error {
|
||
readonly provider: string
|
||
constructor(provider: string) {
|
||
super(`COS 直传不可用,当前驱动: ${provider}`)
|
||
this.name = 'DirectUploadFallbackError'
|
||
this.provider = provider
|
||
}
|
||
}
|