Show upload progress percent on queue rows during POST.

Share percentFromAxiosProgress for axios upload and ExifTool WASM download; update queue entries from upload helpers via queue key.
This commit is contained in:
2026-11-03 17:41:33 +01:00
parent 2290040242
commit b5e50f786b
6 changed files with 103 additions and 24 deletions
@@ -27,7 +27,7 @@
{{ formatMetadataPreviewMessage(row.metadata) }}
</span>
<span class="min-w-0 truncate" :class="uploadStatusClass(row.upload)">
{{ row.upload.message }}
{{ formatUploadQueueMessage(row.upload) }}
</span>
</div>
</div>
@@ -97,10 +97,24 @@ const formatMetadataPreviewMessage = (entry: MetadataPreviewEntry): string => {
const percent = wasmDownloadPercent.value;
if (percent === null) {
return 'Downloading ExifTool…';
return entry.message;
}
return `Downloading ExifTool… ${percent}%`;
return `${entry.message} ${percent}%`;
}
return entry.message;
};
const formatUploadQueueMessage = (entry: UploadQueueEntry): string => {
if (entry.status === 'uploading') {
const percent = entry.uploadPercent;
if (percent === null || percent === undefined) {
return entry.message;
}
return `${entry.message} ${percent}%`;
}
return entry.message;
+43 -14
View File
@@ -16,6 +16,8 @@ import { prepareHotlinkUploadRequest } from '@/utils/upload/prepareHotlinkUpload
import { prepareZkUploadRequest } from '@/utils/upload/prepareZkUploadRequest';
import type { PreparedZkUploadRequest } from '@/types/upload/PreparedZkUploadRequest';
import type { PreparedHotlinkUploadRequest } from '@/types/upload/PreparedHotlinkUploadRequest';
import { percentFromAxiosProgress } from '@/utils/percentFromAxiosProgress';
import type { AxiosProgressEvent } from 'axios';
const UPLOAD_IDLE_MESSAGE = 'Waiting to upload...';
const UPLOAD_QUEUED_MESSAGE = 'Upload queued';
@@ -39,10 +41,11 @@ const UPLOAD_QUEUED_ENTRY: UploadQueueEntry = {
message: UPLOAD_QUEUED_MESSAGE
};
const UPLOAD_UPLOADING_ENTRY: UploadQueueEntry = {
const buildUploadingEntry = (uploadPercent: number | null = null): UploadQueueEntry => ({
status: 'uploading',
message: UPLOAD_UPLOADING_MESSAGE
};
message: UPLOAD_UPLOADING_MESSAGE,
uploadPercent
});
const UPLOAD_FAILED_ENTRY: UploadQueueEntry = {
status: 'failed',
@@ -80,17 +83,41 @@ export const useUploadStore = defineStore('upload', () => {
uploadByKey.value = {};
};
const uploadZk = async (body: CreateZkUploadRequest, altcha?: string): Promise<CreatedUploadResult> => {
const getUploadRequestHeaders = (altcha?: string) => (altcha ? { [ALTCHA_REQUEST_HEADER]: altcha } : undefined);
const onUploadProgress = (queueKey: string, progressEvent: AxiosProgressEvent): void => {
const percent = percentFromAxiosProgress(progressEvent);
if (percent !== null) {
const entry = buildUploadingEntry(percent);
setUploadEntry(queueKey, entry);
}
};
const uploadZk = async (
body: CreateZkUploadRequest,
altcha: string | undefined,
queueKey: string
): Promise<CreatedUploadResult> => {
const { data } = await api.post<CreatedUploadResult>('/uploads/zk', body, {
headers: getUploadRequestHeaders(altcha)
headers: getUploadRequestHeaders(altcha),
onUploadProgress: progressEvent => onUploadProgress(queueKey, progressEvent)
});
return data;
};
const uploadHotlink = async (body: CreateHotlinkUploadRequest, altcha?: string): Promise<CreatedUploadResult> => {
const uploadHotlink = async (
body: CreateHotlinkUploadRequest,
altcha: string | undefined,
queueKey: string
): Promise<CreatedUploadResult> => {
const { data } = await api.post<CreatedUploadResult>('/uploads/hotlink', body, {
headers: getUploadRequestHeaders(altcha)
headers: getUploadRequestHeaders(altcha),
onUploadProgress: progressEvent => {
onUploadProgress(queueKey, progressEvent);
}
});
return data;
@@ -102,8 +129,6 @@ export const useUploadStore = defineStore('upload', () => {
});
};
const getUploadRequestHeaders = (altcha?: string) => (altcha ? { [ALTCHA_REQUEST_HEADER]: altcha } : undefined);
const uploadBatch = async (
files: readonly File[],
settings: UploadBatchSettingsForm,
@@ -176,11 +201,14 @@ export const useUploadStore = defineStore('upload', () => {
continue;
}
setUploadEntry(key, UPLOAD_UPLOADING_ENTRY);
const entry = buildUploadingEntry();
setUploadEntry(key, entry);
try {
const { request, fragmentKey } = prepared;
const result = await uploadZk(request, altcha);
const result = await uploadZk(request, altcha, key);
setUploadEntry(key, {
status: 'uploaded',
@@ -209,11 +237,13 @@ export const useUploadStore = defineStore('upload', () => {
continue;
}
setUploadEntry(key, UPLOAD_UPLOADING_ENTRY);
const entry = buildUploadingEntry();
setUploadEntry(key, entry);
try {
const { request, secret } = prepared;
const result = await uploadHotlink(request, altcha);
const result = await uploadHotlink(request, altcha, key);
setUploadEntry(key, {
status: 'uploaded',
@@ -253,7 +283,6 @@ export const useUploadStore = defineStore('upload', () => {
} else {
setUploadEntry(key, { status: 'skipped', message: UPLOAD_SKIPPED_METADATA_MESSAGE });
}
}
};
@@ -4,7 +4,7 @@ export type UploadQueueEntry =
| { status: 'idle'; message: string }
| { status: 'queued'; message: string }
| { status: 'preparing'; message: string }
| { status: 'uploading'; message: string }
| { status: 'uploading'; message: string; uploadPercent?: number | null }
| {
status: 'uploaded';
message: string;
@@ -0,0 +1,26 @@
import type { AxiosProgressEvent } from 'axios';
import { describe, expect, it } from 'vitest';
import { percentFromAxiosProgress } from '@/utils/percentFromAxiosProgress';
const progress = (loaded: number, total?: number): AxiosProgressEvent =>
({
loaded,
total,
bytes: 0,
lengthComputable: total !== undefined
}) as AxiosProgressEvent;
describe('percentFromAxiosProgress', () => {
it('returns null when total is missing', () => {
expect(percentFromAxiosProgress(progress(50))).toBeNull();
});
it('returns rounded percent from loaded and total', () => {
expect(percentFromAxiosProgress(progress(250, 1000))).toBe(25);
expect(percentFromAxiosProgress(progress(1, 3))).toBe(33.33);
});
it('caps at 100', () => {
expect(percentFromAxiosProgress(progress(200, 100))).toBe(100);
});
});
@@ -0,0 +1,11 @@
import type { AxiosProgressEvent } from 'axios';
export const percentFromAxiosProgress = (progressEvent: AxiosProgressEvent): number | null => {
if (!progressEvent.total) {
return null;
}
const percent = Math.min(100, (progressEvent.loaded / progressEvent.total) * 100);
return parseFloat(percent.toFixed(2));
};
+5 -6
View File
@@ -4,6 +4,7 @@ import axios from 'axios';
import { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg';
import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage';
import type { ExiftoolWorkerJob } from '@/types/workers/exiftoolWorkerJob';
import { percentFromAxiosProgress } from '@/utils/percentFromAxiosProgress';
import { resolveErrorMessage } from '@/utils/resolveErrorMessage';
import { toArrayBuffer } from '@/utils/toArrayBuffer';
@@ -18,15 +19,13 @@ const fetchZeroperlWasm = async (): Promise<Response> => {
const { data } = await axios.get<ArrayBuffer>(zeroperlWasmUrl, {
responseType: 'arraybuffer',
onDownloadProgress: progressEvent => {
if (!progressEvent.total) {
const percent = percentFromAxiosProgress(progressEvent);
if (percent === null) {
return;
}
const percent = Math.min(100, (progressEvent.loaded / progressEvent.total) * 100);
const rounded = parseFloat(percent.toFixed(2));
self.postMessage({ type: 'wasm-download-progress', percent: rounded });
self.postMessage({ type: 'wasm-download-progress', percent });
}
});