Files
zyt/admin/src/utils/oss-direct-upload.ts
T
long 7d7a4aa4d2 oss 直传
分片上传
2026-05-08 10:49:16 +08:00

143 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}