Files
xiaoxia-saas/apps/web/src/api/assets/upload.ts
T
xiaoxia 5c45629495
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
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 / 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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m15s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m37s
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 / Frontend Unit Tests (push) Successful in 10m20s
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 / ACR Image Cleanup (push) Successful in 2m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m10s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m44s
CI/CD Pipeline / Canary Release to Production (push) Failing after 405h11m24s
CI/CD Pipeline / Production Browser E2E (push) Failing after 405h13m22s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 405h18m50s
CI/CD Pipeline / Deploy Production (push) Failing after 405h13m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 405h18m56s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 405h13m23s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 405h19m0s
CI/CD Pipeline / Build Production Web Image (push) Failing after 405h13m24s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 405h19m0s
CI/CD Pipeline / CI Gate (push) Failing after 405h13m24s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 405h19m2s
CI/CD Pipeline / Build Production API Image (push) Failing after 405h13m24s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 405h19m4s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 405h18m52s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 405h18m54s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 405h20m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 405h20m57s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 405h21m0s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 405h21m3s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 405h21m5s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 405h20m37s
CI/CD Pipeline / Build Staging API Image (push) Failing after 405h21m5s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 405h24m44s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 405h24m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 405h20m59s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 405h25m53s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 405h25m57s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Failing after 405h28m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 405h28m51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 405h28m53s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 405h29m18s
CI/CD Pipeline / PR Build API Image (push) Failing after 405h29m22s
CI/CD Pipeline / Frontend Lint (push) Failing after 405h29m23s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 405h29m25s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 405h29m26s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 405h53m11s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 405h55m10s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 405h58m55s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 406h0m4s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Failing after 406h2m58s
CI/CD Pipeline / PR Build Web Image (push) Failing after 406h3m29s
feat(assets): 上传即入库——批量上传队列化(并发3)+真实进度+刷新不丢 (#1557)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-30 17:56:02 +08:00

145 lines
4.8 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.
/**
* 上传相关 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()
}