diff --git a/frontend/src/components/upload/UploadQueueList.vue b/frontend/src/components/upload/UploadQueueList.vue
index f089d8a..80c88ee 100644
--- a/frontend/src/components/upload/UploadQueueList.vue
+++ b/frontend/src/components/upload/UploadQueueList.vue
@@ -27,7 +27,7 @@
{{ formatMetadataPreviewMessage(row.metadata) }}
- {{ row.upload.message }}
+ {{ formatUploadQueueMessage(row.upload) }}
@@ -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;
diff --git a/frontend/src/stores/upload.ts b/frontend/src/stores/upload.ts
index ab5c819..c611aee 100644
--- a/frontend/src/stores/upload.ts
+++ b/frontend/src/stores/upload.ts
@@ -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 => {
+ 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 => {
const { data } = await api.post('/uploads/zk', body, {
- headers: getUploadRequestHeaders(altcha)
+ headers: getUploadRequestHeaders(altcha),
+ onUploadProgress: progressEvent => onUploadProgress(queueKey, progressEvent)
});
return data;
};
- const uploadHotlink = async (body: CreateHotlinkUploadRequest, altcha?: string): Promise => {
+ const uploadHotlink = async (
+ body: CreateHotlinkUploadRequest,
+ altcha: string | undefined,
+ queueKey: string
+ ): Promise => {
const { data } = await api.post('/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 });
}
-
}
};
diff --git a/frontend/src/types/upload/UploadQueueEntry.ts b/frontend/src/types/upload/UploadQueueEntry.ts
index d56923f..b0486a3 100644
--- a/frontend/src/types/upload/UploadQueueEntry.ts
+++ b/frontend/src/types/upload/UploadQueueEntry.ts
@@ -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;
diff --git a/frontend/src/utils/percentFromAxiosProgress.spec.ts b/frontend/src/utils/percentFromAxiosProgress.spec.ts
new file mode 100644
index 0000000..7fa2a81
--- /dev/null
+++ b/frontend/src/utils/percentFromAxiosProgress.spec.ts
@@ -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);
+ });
+});
diff --git a/frontend/src/utils/percentFromAxiosProgress.ts b/frontend/src/utils/percentFromAxiosProgress.ts
new file mode 100644
index 0000000..2b38d71
--- /dev/null
+++ b/frontend/src/utils/percentFromAxiosProgress.ts
@@ -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));
+};
diff --git a/frontend/src/workers/exiftoolWorker.ts b/frontend/src/workers/exiftoolWorker.ts
index 3ce8d18..5d27c8b 100644
--- a/frontend/src/workers/exiftoolWorker.ts
+++ b/frontend/src/workers/exiftoolWorker.ts
@@ -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 => {
const { data } = await axios.get(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 });
}
});