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) }} {{ formatMetadataPreviewMessage(row.metadata) }}
</span> </span>
<span class="min-w-0 truncate" :class="uploadStatusClass(row.upload)"> <span class="min-w-0 truncate" :class="uploadStatusClass(row.upload)">
{{ row.upload.message }} {{ formatUploadQueueMessage(row.upload) }}
</span> </span>
</div> </div>
</div> </div>
@@ -97,10 +97,24 @@ const formatMetadataPreviewMessage = (entry: MetadataPreviewEntry): string => {
const percent = wasmDownloadPercent.value; const percent = wasmDownloadPercent.value;
if (percent === null) { 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; return entry.message;
+43 -14
View File
@@ -16,6 +16,8 @@ import { prepareHotlinkUploadRequest } from '@/utils/upload/prepareHotlinkUpload
import { prepareZkUploadRequest } from '@/utils/upload/prepareZkUploadRequest'; import { prepareZkUploadRequest } from '@/utils/upload/prepareZkUploadRequest';
import type { PreparedZkUploadRequest } from '@/types/upload/PreparedZkUploadRequest'; import type { PreparedZkUploadRequest } from '@/types/upload/PreparedZkUploadRequest';
import type { PreparedHotlinkUploadRequest } from '@/types/upload/PreparedHotlinkUploadRequest'; 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_IDLE_MESSAGE = 'Waiting to upload...';
const UPLOAD_QUEUED_MESSAGE = 'Upload queued'; const UPLOAD_QUEUED_MESSAGE = 'Upload queued';
@@ -39,10 +41,11 @@ const UPLOAD_QUEUED_ENTRY: UploadQueueEntry = {
message: UPLOAD_QUEUED_MESSAGE message: UPLOAD_QUEUED_MESSAGE
}; };
const UPLOAD_UPLOADING_ENTRY: UploadQueueEntry = { const buildUploadingEntry = (uploadPercent: number | null = null): UploadQueueEntry => ({
status: 'uploading', status: 'uploading',
message: UPLOAD_UPLOADING_MESSAGE message: UPLOAD_UPLOADING_MESSAGE,
}; uploadPercent
});
const UPLOAD_FAILED_ENTRY: UploadQueueEntry = { const UPLOAD_FAILED_ENTRY: UploadQueueEntry = {
status: 'failed', status: 'failed',
@@ -80,17 +83,41 @@ export const useUploadStore = defineStore('upload', () => {
uploadByKey.value = {}; 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, { const { data } = await api.post<CreatedUploadResult>('/uploads/zk', body, {
headers: getUploadRequestHeaders(altcha) headers: getUploadRequestHeaders(altcha),
onUploadProgress: progressEvent => onUploadProgress(queueKey, progressEvent)
}); });
return data; 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, { const { data } = await api.post<CreatedUploadResult>('/uploads/hotlink', body, {
headers: getUploadRequestHeaders(altcha) headers: getUploadRequestHeaders(altcha),
onUploadProgress: progressEvent => {
onUploadProgress(queueKey, progressEvent);
}
}); });
return data; return data;
@@ -102,8 +129,6 @@ export const useUploadStore = defineStore('upload', () => {
}); });
}; };
const getUploadRequestHeaders = (altcha?: string) => (altcha ? { [ALTCHA_REQUEST_HEADER]: altcha } : undefined);
const uploadBatch = async ( const uploadBatch = async (
files: readonly File[], files: readonly File[],
settings: UploadBatchSettingsForm, settings: UploadBatchSettingsForm,
@@ -176,11 +201,14 @@ export const useUploadStore = defineStore('upload', () => {
continue; continue;
} }
setUploadEntry(key, UPLOAD_UPLOADING_ENTRY); const entry = buildUploadingEntry();
setUploadEntry(key, entry);
try { try {
const { request, fragmentKey } = prepared; const { request, fragmentKey } = prepared;
const result = await uploadZk(request, altcha); const result = await uploadZk(request, altcha, key);
setUploadEntry(key, { setUploadEntry(key, {
status: 'uploaded', status: 'uploaded',
@@ -209,11 +237,13 @@ export const useUploadStore = defineStore('upload', () => {
continue; continue;
} }
setUploadEntry(key, UPLOAD_UPLOADING_ENTRY); const entry = buildUploadingEntry();
setUploadEntry(key, entry);
try { try {
const { request, secret } = prepared; const { request, secret } = prepared;
const result = await uploadHotlink(request, altcha); const result = await uploadHotlink(request, altcha, key);
setUploadEntry(key, { setUploadEntry(key, {
status: 'uploaded', status: 'uploaded',
@@ -253,7 +283,6 @@ export const useUploadStore = defineStore('upload', () => {
} else { } else {
setUploadEntry(key, { status: 'skipped', message: UPLOAD_SKIPPED_METADATA_MESSAGE }); setUploadEntry(key, { status: 'skipped', message: UPLOAD_SKIPPED_METADATA_MESSAGE });
} }
} }
}; };
@@ -4,7 +4,7 @@ export type UploadQueueEntry =
| { status: 'idle'; message: string } | { status: 'idle'; message: string }
| { status: 'queued'; message: string } | { status: 'queued'; message: string }
| { status: 'preparing'; message: string } | { status: 'preparing'; message: string }
| { status: 'uploading'; message: string } | { status: 'uploading'; message: string; uploadPercent?: number | null }
| { | {
status: 'uploaded'; status: 'uploaded';
message: string; 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 { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg';
import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage'; import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage';
import type { ExiftoolWorkerJob } from '@/types/workers/exiftoolWorkerJob'; import type { ExiftoolWorkerJob } from '@/types/workers/exiftoolWorkerJob';
import { percentFromAxiosProgress } from '@/utils/percentFromAxiosProgress';
import { resolveErrorMessage } from '@/utils/resolveErrorMessage'; import { resolveErrorMessage } from '@/utils/resolveErrorMessage';
import { toArrayBuffer } from '@/utils/toArrayBuffer'; import { toArrayBuffer } from '@/utils/toArrayBuffer';
@@ -18,15 +19,13 @@ const fetchZeroperlWasm = async (): Promise<Response> => {
const { data } = await axios.get<ArrayBuffer>(zeroperlWasmUrl, { const { data } = await axios.get<ArrayBuffer>(zeroperlWasmUrl, {
responseType: 'arraybuffer', responseType: 'arraybuffer',
onDownloadProgress: progressEvent => { onDownloadProgress: progressEvent => {
if (!progressEvent.total) { const percent = percentFromAxiosProgress(progressEvent);
if (percent === null) {
return; return;
} }
const percent = Math.min(100, (progressEvent.loaded / progressEvent.total) * 100); self.postMessage({ type: 'wasm-download-progress', percent });
const rounded = parseFloat(percent.toFixed(2));
self.postMessage({ type: 'wasm-download-progress', percent: rounded });
} }
}); });