fix: harden desktop release OSS uploads

This commit is contained in:
2026-06-22 23:49:24 +08:00
parent dcbab28e69
commit f0de364063
7 changed files with 218 additions and 63 deletions
+66 -31
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import { createMD5, createSHA256 } from 'hash-wasm'
import { createSHA256 } from 'hash-wasm'
import { http } from '@/lib/http'
import { OpsApiError, http } from '@/lib/http'
export type DesktopReleasePlatform = 'darwin' | 'win32' | 'linux'
export type DesktopReleaseArch = 'universal' | 'x64' | 'arm64' | 'ia32'
@@ -126,7 +126,6 @@ interface DesktopClientReleaseDirectUploadSession {
interface DesktopClientReleaseFileHashes {
sha256: string
contentMD5: string
}
export const platformOptions = [
@@ -175,7 +174,20 @@ export const desktopClientReleaseApi = {
file: File,
target: DesktopClientReleaseUploadTarget,
) {
return uploadOSSDirect(file, target)
try {
return await uploadOSSDirect(file, target)
} catch (error) {
if (!shouldFallbackToChunkedUpload(error)) {
throw error
}
target.onProgress?.({
loaded: 0,
total: file.size,
percent: 0,
stage: 'uploading',
})
return uploadOSSInChunks(file, target)
}
},
async uploadOSSLegacy(
file: File,
@@ -246,7 +258,6 @@ async function uploadOSSDirect(
file_name: file.name,
file_size_bytes: file.size,
sha256: hashes.sha256,
content_md5: hashes.contentMD5,
},
)
@@ -254,24 +265,42 @@ async function uploadOSSDirect(
if (!session.upload_url) {
throw new Error('missing_direct_upload_url')
}
await axios.put(session.upload_url, file, {
headers: session.headers,
timeout: DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS,
onUploadProgress(event) {
const total = event.total ?? file.size
target.onProgress?.({
loaded: event.loaded,
total,
percent: total ? Math.min(99, Math.round((event.loaded / total) * 100)) : undefined,
stage: 'uploading',
})
},
})
try {
await axios.put(session.upload_url, file, {
headers: directUploadHeaders(session.headers),
timeout: DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS,
onUploadProgress(event) {
const total = event.total ?? file.size
target.onProgress?.({
loaded: event.loaded,
total,
percent: total ? Math.min(99, Math.round((event.loaded / total) * 100)) : undefined,
stage: 'uploading',
})
},
})
} catch (uploadError) {
try {
return await completeDirectUpload(session, target)
} catch (completeError) {
if (isDirectUploadNotFoundError(completeError)) {
throw uploadError
}
throw completeError
}
}
}
return completeDirectUpload(session, target)
}
async function completeDirectUpload(
session: DesktopClientReleaseDirectUploadSession,
target: DesktopClientReleaseUploadTarget,
): Promise<DesktopClientReleaseUploadResult> {
target.onProgress?.({
loaded: file.size,
total: file.size,
loaded: session.file_size_bytes,
total: session.file_size_bytes,
percent: 100,
stage: 'verifying',
})
@@ -298,34 +327,40 @@ async function uploadOSSDirect(
return result
}
function isDirectUploadNotFoundError(error: unknown): boolean {
return error instanceof OpsApiError && error.code === 40078
}
function directUploadHeaders(headers: Record<string, string> | undefined): Record<string, string> {
if (!headers) return {}
return Object.fromEntries(
Object.entries(headers).filter(([key]) => key.toLowerCase() !== 'content-md5'),
)
}
function shouldFallbackToChunkedUpload(error: unknown): boolean {
if (axios.isAxiosError(error)) return true
return error instanceof Error && error.message === 'missing_direct_upload_url'
}
async function hashFile(
file: File,
onProgress?: (loaded: number) => void,
): Promise<DesktopClientReleaseFileHashes> {
const [sha256Hasher, md5Hasher] = await Promise.all([createSHA256(), createMD5()])
const sha256Hasher = await createSHA256()
let loaded = 0
while (loaded < file.size) {
const end = Math.min(loaded + DESKTOP_CLIENT_RELEASE_HASH_CHUNK_SIZE_BYTES, file.size)
const chunk = new Uint8Array(await file.slice(loaded, end).arrayBuffer())
sha256Hasher.update(chunk)
md5Hasher.update(chunk)
loaded = end
onProgress?.(loaded)
}
return {
sha256: sha256Hasher.digest('hex') as string,
contentMD5: uint8ArrayToBase64(md5Hasher.digest('binary') as Uint8Array),
}
}
function uint8ArrayToBase64(value: Uint8Array): string {
let binary = ''
for (let index = 0; index < value.length; index += 1) {
binary += String.fromCharCode(value[index])
}
return window.btoa(binary)
}
async function uploadOSSInChunks(
file: File,
target: DesktopClientReleaseUploadTarget,