feat: report desktop release upload completion progress
This commit is contained in:
@@ -91,6 +91,7 @@ export interface DesktopClientReleaseUploadProgress {
|
||||
total?: number
|
||||
percent?: number
|
||||
stage?: 'hashing' | 'signing' | 'uploading' | 'verifying' | 'complete'
|
||||
message?: string
|
||||
chunkIndex?: number
|
||||
chunkCount?: number
|
||||
}
|
||||
@@ -128,6 +129,19 @@ interface DesktopClientReleaseFileHashes {
|
||||
sha256: string
|
||||
}
|
||||
|
||||
interface DesktopClientReleaseCompleteJob {
|
||||
job_id: string
|
||||
status: 'running' | 'succeeded' | 'failed'
|
||||
stage: string
|
||||
message: string
|
||||
percent: number
|
||||
result?: DesktopClientReleaseUploadResult
|
||||
error_code?: string
|
||||
error?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export const platformOptions = [
|
||||
{ label: 'macOS', value: 'darwin' },
|
||||
{ label: 'Windows', value: 'win32' },
|
||||
@@ -298,14 +312,8 @@ async function completeDirectUpload(
|
||||
session: DesktopClientReleaseDirectUploadSession,
|
||||
target: DesktopClientReleaseUploadTarget,
|
||||
): Promise<DesktopClientReleaseUploadResult> {
|
||||
target.onProgress?.({
|
||||
loaded: session.file_size_bytes,
|
||||
total: session.file_size_bytes,
|
||||
percent: 100,
|
||||
stage: 'verifying',
|
||||
})
|
||||
const result = await http.post<DesktopClientReleaseUploadResult>(
|
||||
'/desktop-client/releases/direct-upload/complete',
|
||||
const job = await http.post<DesktopClientReleaseCompleteJob>(
|
||||
'/desktop-client/releases/direct-upload/complete-jobs',
|
||||
{
|
||||
platform: target.platform,
|
||||
arch: target.arch,
|
||||
@@ -318,13 +326,7 @@ async function completeDirectUpload(
|
||||
sha256: session.sha256,
|
||||
},
|
||||
)
|
||||
target.onProgress?.({
|
||||
loaded: result.file_size_bytes,
|
||||
total: result.file_size_bytes,
|
||||
percent: 100,
|
||||
stage: 'complete',
|
||||
})
|
||||
return result
|
||||
return pollCompleteJob(job.job_id, session.file_size_bytes, target)
|
||||
}
|
||||
|
||||
function isDirectUploadNotFoundError(error: unknown): boolean {
|
||||
@@ -343,6 +345,45 @@ function shouldFallbackToChunkedUpload(error: unknown): boolean {
|
||||
return error instanceof Error && error.message === 'missing_direct_upload_url'
|
||||
}
|
||||
|
||||
async function pollCompleteJob(
|
||||
jobID: string,
|
||||
totalBytes: number,
|
||||
target: DesktopClientReleaseUploadTarget,
|
||||
chunkCount?: number,
|
||||
): Promise<DesktopClientReleaseUploadResult> {
|
||||
for (;;) {
|
||||
const job = await http.get<DesktopClientReleaseCompleteJob>(
|
||||
`/desktop-client/release-upload-complete-jobs/${jobID}`,
|
||||
)
|
||||
target.onProgress?.({
|
||||
loaded: totalBytes,
|
||||
total: totalBytes,
|
||||
percent: Math.min(99, Math.max(1, job.percent)),
|
||||
stage: 'verifying',
|
||||
message: job.message,
|
||||
chunkCount,
|
||||
})
|
||||
if (job.status === 'succeeded' && job.result) {
|
||||
target.onProgress?.({
|
||||
loaded: job.result.file_size_bytes,
|
||||
total: job.result.file_size_bytes,
|
||||
percent: 100,
|
||||
stage: 'complete',
|
||||
message: job.message || '上传完成',
|
||||
chunkCount,
|
||||
})
|
||||
return job.result
|
||||
}
|
||||
if (job.status === 'failed') {
|
||||
throw new OpsApiError({
|
||||
message: job.error_code || 'desktop_client_release_upload_failed',
|
||||
detail: job.error || job.message || '上传确认失败',
|
||||
})
|
||||
}
|
||||
await delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
async function hashFile(
|
||||
file: File,
|
||||
onProgress?: (loaded: number) => void,
|
||||
@@ -404,18 +445,12 @@ async function uploadOSSInChunks(
|
||||
emitChunkProgress(target, confirmedBytes, file.size, chunkCount, index)
|
||||
}
|
||||
|
||||
target.onProgress?.({
|
||||
loaded: file.size,
|
||||
total: file.size,
|
||||
percent: 100,
|
||||
stage: 'complete',
|
||||
chunkCount,
|
||||
})
|
||||
return await http.post<DesktopClientReleaseUploadResult>(
|
||||
`/desktop-client/release-uploads/${session.upload_id}/complete`,
|
||||
const job = await http.post<DesktopClientReleaseCompleteJob>(
|
||||
`/desktop-client/release-uploads/${session.upload_id}/complete-jobs`,
|
||||
undefined,
|
||||
{ timeout: DESKTOP_CLIENT_RELEASE_COMPLETE_TIMEOUT_MS },
|
||||
)
|
||||
return await pollCompleteJob(job.job_id, file.size, target, chunkCount)
|
||||
} catch (error) {
|
||||
void http.delete(`/desktop-client/release-uploads/${session.upload_id}`).catch(() => undefined)
|
||||
throw error
|
||||
|
||||
@@ -205,6 +205,14 @@
|
||||
{{ uploadButtonText('installer') }}
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div v-if="uploading" class="upload-progress">
|
||||
<a-progress
|
||||
:percent="uploadProgress ?? 0"
|
||||
:status="uploadStage === 'verifying' ? 'active' : 'normal'"
|
||||
size="small"
|
||||
/>
|
||||
<span>{{ uploadProgressText('installer') }}</span>
|
||||
</div>
|
||||
<div v-if="form.file_name" class="upload-result">
|
||||
<strong>{{ form.file_name }}</strong>
|
||||
<span>{{ formatBytes(form.file_size_bytes) }}</span>
|
||||
@@ -271,6 +279,14 @@
|
||||
{{ uploadButtonText('updater') }}
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div v-if="updaterUploading" class="upload-progress">
|
||||
<a-progress
|
||||
:percent="updaterUploadProgress ?? 0"
|
||||
:status="updaterUploadStage === 'verifying' ? 'active' : 'normal'"
|
||||
size="small"
|
||||
/>
|
||||
<span>{{ uploadProgressText('updater') }}</span>
|
||||
</div>
|
||||
<div v-if="form.updater_file_name" class="upload-result">
|
||||
<strong>{{ form.updater_file_name }}</strong>
|
||||
<span>{{ formatBytes(form.updater_file_size_bytes) }}</span>
|
||||
@@ -403,6 +419,8 @@ const uploadProgress = ref<number | null>(null)
|
||||
const updaterUploadProgress = ref<number | null>(null)
|
||||
const uploadStage = ref<DesktopClientReleaseUploadProgress['stage'] | null>(null)
|
||||
const updaterUploadStage = ref<DesktopClientReleaseUploadProgress['stage'] | null>(null)
|
||||
const uploadMessage = ref<string | null>(null)
|
||||
const updaterUploadMessage = ref<string | null>(null)
|
||||
|
||||
const enabledRows = computed(() => rows.value.filter((row) => row.enabled).length)
|
||||
const forceRows = computed(() => rows.value.filter((row) => row.force_update).length)
|
||||
@@ -573,10 +591,12 @@ async function beforeUploadReleasePackage(file: File, kind: 'installer' | 'updat
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
uploadStage.value = 'hashing'
|
||||
uploadMessage.value = null
|
||||
} else {
|
||||
updaterUploading.value = true
|
||||
updaterUploadProgress.value = 0
|
||||
updaterUploadStage.value = 'hashing'
|
||||
updaterUploadMessage.value = null
|
||||
}
|
||||
try {
|
||||
const result = await desktopClientReleaseApi.uploadOSS(file, {
|
||||
@@ -590,9 +610,11 @@ async function beforeUploadReleasePackage(file: File, kind: 'installer' | 'updat
|
||||
if (kind === 'installer') {
|
||||
uploadProgress.value = progress.percent
|
||||
uploadStage.value = progress.stage ?? null
|
||||
uploadMessage.value = progress.message ?? null
|
||||
} else {
|
||||
updaterUploadProgress.value = progress.percent
|
||||
updaterUploadStage.value = progress.stage ?? null
|
||||
updaterUploadMessage.value = progress.message ?? null
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -620,6 +642,8 @@ async function beforeUploadReleasePackage(file: File, kind: 'installer' | 'updat
|
||||
updaterUploadProgress.value = null
|
||||
uploadStage.value = null
|
||||
updaterUploadStage.value = null
|
||||
uploadMessage.value = null
|
||||
updaterUploadMessage.value = null
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -648,10 +672,21 @@ function uploadButtonText(kind: 'installer' | 'updater') {
|
||||
return hasUploaded ? '重新上传' : '选择文件并上传'
|
||||
}
|
||||
|
||||
function uploadButtonLoading(kind: 'installer' | 'updater') {
|
||||
const isUploading = kind === 'installer' ? uploading.value : updaterUploading.value
|
||||
function uploadProgressText(kind: 'installer' | 'updater') {
|
||||
const progress = kind === 'installer' ? uploadProgress.value : updaterUploadProgress.value
|
||||
return isUploading && progress !== 100
|
||||
const stage = kind === 'installer' ? uploadStage.value : updaterUploadStage.value
|
||||
const message = kind === 'installer' ? uploadMessage.value : updaterUploadMessage.value
|
||||
if (message) return `${message} ${progress ?? 0}%`
|
||||
if (stage === 'hashing') return `正在计算 SHA256 ${progress ?? 0}%`
|
||||
if (stage === 'signing') return '正在获取 OSS 直传地址'
|
||||
if (stage === 'uploading') return `正在上传 ${progress ?? 0}%`
|
||||
if (stage === 'verifying') return '服务端正在确认文件完整性'
|
||||
if (stage === 'complete') return '上传完成'
|
||||
return '准备上传'
|
||||
}
|
||||
|
||||
function uploadButtonLoading(kind: 'installer' | 'updater') {
|
||||
return kind === 'installer' ? uploading.value : updaterUploading.value
|
||||
}
|
||||
|
||||
function beforeUploadInstallerPackage(file: File) {
|
||||
@@ -1011,6 +1046,18 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
display: grid;
|
||||
max-width: 360px;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.upload-progress span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.upload-result strong {
|
||||
color: #0f172a;
|
||||
font-weight: 750;
|
||||
|
||||
Reference in New Issue
Block a user