665228a58f
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
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
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m49s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m36s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m37s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 54s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m53s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 3m38s
CI/CD Pipeline / Unit Tests (push) Failing after 14m29s
CI/CD Pipeline / Build Production API Image (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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Failing after 7m28s
CI/CD Pipeline / CI Gate (push) Has been skipped
前端: - 删除 uploadAsset 函数(upload.ts)和 export(index.ts) - 删除 UploadResult type export - CloneModal/useCloneSubmit 改用 uploadAssetDirect - DirectUploadCompleteResult 新增 url 字段 - 清理测试文件中 uploadAsset 引用 后端: - DirectUploadCompleteResponse 新增 url 字段 - complete_direct_upload 返回 OSS 文件 URL - 修复 FFmpeg 超时日志 300s → 900s
111 lines
3.5 KiB
TypeScript
111 lines
3.5 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
|
|
}
|
|
|
|
/** 直传上传(大文件推荐),支持可选进度回调 */
|
|
export const uploadAssetDirect = async (data: {
|
|
file: File
|
|
library_id: string
|
|
onProgress?: (percent: number) => void
|
|
}): Promise<DirectUploadCompleteResult> => {
|
|
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,
|
|
})
|
|
|
|
const directForm = new FormData()
|
|
Object.entries(prepared.fields).forEach(([key, value]) => directForm.append(key, value))
|
|
directForm.append("file", data.file)
|
|
|
|
// 使用 XMLHttpRequest 以获取上传进度 + 超时控制 + 详细错误诊断
|
|
await new Promise<void>((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest()
|
|
xhr.open(prepared.method, prepared.upload_url)
|
|
|
|
// 超时 10 分钟
|
|
xhr.timeout = 10 * 60 * 1000
|
|
|
|
xhr.upload.onprogress = (e) => {
|
|
if (e.lengthComputable && data.onProgress) {
|
|
data.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)
|
|
})
|
|
|
|
return completeDirectUpload({
|
|
project_id: project.id,
|
|
library_id: data.library_id,
|
|
storage_key: prepared.storage_key,
|
|
})
|
|
}
|