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 }); } });