5c45629495
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4m37s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m37s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m50s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 5m5s
AI Code Review / AI Code Review (pull_request) Successful in 7m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m40s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m15s
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m37s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m48s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m55s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m20s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m1s
CI/CD Pipeline / Retag skipped Staging API Image (push) Successful in 2m18s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Successful in 2m16s
CI/CD Pipeline / Integration Tests (push) Successful in 4m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / Unit Tests (push) Successful in 15m57s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m10s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m44s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
/**
|
||
* 上传相关 API(表单上传 + OSS 直传)
|
||
*/
|
||
import apiClient from "../client"
|
||
import { getOrCreateDefaultProject } from "../projects"
|
||
import type { DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types"
|
||
|
||
/** 预签名直传准备 */
|
||
export const prepareDirectUpload = async (data: {
|
||
project_id: string
|
||
library_id: string
|
||
filename: string
|
||
content_type: string
|
||
file_size: number
|
||
}): Promise<DirectUploadPrepareResult> => {
|
||
const response = await apiClient.post("/upload/direct/prepare", data)
|
||
return response.data
|
||
}
|
||
|
||
/** 直传完成确认 */
|
||
export const completeDirectUpload = async (data: {
|
||
project_id: string
|
||
library_id: string
|
||
storage_key: string
|
||
}): Promise<DirectUploadCompleteResult> => {
|
||
const response = await apiClient.post("/upload/direct/complete", data)
|
||
return response.data
|
||
}
|
||
|
||
/** 直传 OSS 的底层传输(POST 表单到 OSS),带进度回调 */
|
||
const putToOSS = (
|
||
prepared: DirectUploadPrepareResult,
|
||
file: File,
|
||
onProgress?: (percent: number) => void,
|
||
): Promise<void> =>
|
||
new Promise<void>((resolve, reject) => {
|
||
const directForm = new FormData()
|
||
Object.entries(prepared.fields).forEach(([key, value]) => directForm.append(key, value))
|
||
directForm.append("file", file)
|
||
|
||
// 使用 XMLHttpRequest 以获取上传进度 + 超时控制 + 详细错误诊断
|
||
const xhr = new XMLHttpRequest()
|
||
xhr.open(prepared.method, prepared.upload_url)
|
||
|
||
// 超时 10 分钟
|
||
xhr.timeout = 10 * 60 * 1000
|
||
|
||
xhr.upload.onprogress = (e) => {
|
||
if (e.lengthComputable && onProgress) {
|
||
onProgress(Math.round((e.loaded / e.total) * 100))
|
||
}
|
||
}
|
||
xhr.onload = () => {
|
||
if (xhr.status >= 200 && xhr.status < 300) {
|
||
resolve()
|
||
} else {
|
||
// 解析 OSS 返回的 XML 错误信息
|
||
let ossError = ""
|
||
try {
|
||
const codeMatch = xhr.responseText.match(/<Code>([^<]+)<\/Code>/)
|
||
const msgMatch = xhr.responseText.match(/<Message>([^<]+)<\/Message>/)
|
||
if (codeMatch || msgMatch) {
|
||
ossError = ` [OSS: ${codeMatch?.[1] || "unknown"} - ${msgMatch?.[1] || "unknown"}]`
|
||
}
|
||
} catch {
|
||
// 无法解析响应体
|
||
}
|
||
const detail = `OSS 直传失败: HTTP ${xhr.status} ${xhr.statusText}${ossError}`
|
||
console.error("[OSS Upload] 直传失败:", {
|
||
url: prepared.upload_url,
|
||
storage_key: prepared.storage_key,
|
||
status: xhr.status,
|
||
statusText: xhr.statusText,
|
||
})
|
||
reject(new Error(detail))
|
||
}
|
||
}
|
||
xhr.onerror = () => {
|
||
console.error("[OSS Upload] 网络错误:", {
|
||
url: prepared.upload_url,
|
||
storage_key: prepared.storage_key,
|
||
})
|
||
reject(new Error("OSS 上传网络错误,请检查网络连接"))
|
||
}
|
||
xhr.ontimeout = () => {
|
||
console.error("[OSS Upload] 上传超时:", {
|
||
url: prepared.upload_url,
|
||
storage_key: prepared.storage_key,
|
||
})
|
||
reject(new Error("OSS 上传超时(10分钟),请检查网络或尝试更小的文件"))
|
||
}
|
||
xhr.send(directForm)
|
||
})
|
||
|
||
/** 单个文件的上传阶段信息(供批量上传队列做状态绑定) */
|
||
export interface DirectUploadHandle {
|
||
/** prepare 返回(含可能的预建 asset_id) */
|
||
prepared: DirectUploadPrepareResult
|
||
/** 直传 OSS(可重复调用用于重试) */
|
||
transfer: (onProgress?: (percent: number) => void) => Promise<void>
|
||
/** 直传完成后调用 complete 确认入库 */
|
||
complete: () => Promise<DirectUploadCompleteResult>
|
||
}
|
||
|
||
/**
|
||
* 准备一次直传:调 prepare 拿到签名表单(后端可能同时预建 uploading 态 asset),
|
||
* 返回分段执行的 handle,调用方自行控制 transfer/complete 时机(便于队列并发与重试)。
|
||
*/
|
||
export const prepareDirectUploadHandle = async (data: {
|
||
file: File
|
||
library_id: string
|
||
}): Promise<DirectUploadHandle> => {
|
||
const project = await getOrCreateDefaultProject()
|
||
|
||
const prepared = await prepareDirectUpload({
|
||
project_id: project.id,
|
||
library_id: data.library_id,
|
||
filename: data.file.name,
|
||
content_type: data.file.type || "application/octet-stream",
|
||
file_size: data.file.size,
|
||
})
|
||
|
||
return {
|
||
prepared,
|
||
transfer: (onProgress) => putToOSS(prepared, data.file, onProgress),
|
||
complete: () =>
|
||
completeDirectUpload({
|
||
project_id: project.id,
|
||
library_id: data.library_id,
|
||
storage_key: prepared.storage_key,
|
||
}),
|
||
}
|
||
}
|
||
|
||
/** 直传上传(大文件推荐),支持可选进度回调;一次性完成 prepare→transfer→complete */
|
||
export const uploadAssetDirect = async (data: {
|
||
file: File
|
||
library_id: string
|
||
onProgress?: (percent: number) => void
|
||
}): Promise<DirectUploadCompleteResult> => {
|
||
const handle = await prepareDirectUploadHandle({ file: data.file, library_id: data.library_id })
|
||
await handle.transfer(data.onProgress)
|
||
return handle.complete()
|
||
}
|