From 271c2b7b2d1b0c4b51657c8024a84002d90afe78 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Fri, 23 Oct 2026 18:34:13 +0200 Subject: [PATCH] Execute batch uploads with metadata gates, encryption, and queue state. Separate upload entries per file, ALTCHA on POST, and stripped-or-original fallback on metadata errors. --- frontend/src/stores/metadata.ts | 108 ++++---- frontend/src/stores/upload.ts | 233 +++++++++++++++++- .../src/types/metadata/MetadataPreview.ts | 6 +- .../upload/PreparedHotlinkUploadRequest.ts | 6 + .../types/upload/PreparedZkUploadRequest.ts | 6 + frontend/src/types/upload/UploadQueueEntry.ts | 8 + frontend/src/types/upload/UploadQueueRow.ts | 4 +- .../upload/ZkUploadPrepareOptionsResult.ts | 5 - frontend/src/utils/file/makeKeyFromFile.ts | 2 + .../buildUploadOptionsFromBatchSettings.ts | 9 + ...oadPrepareOptionsFromBatchSettings.spec.ts | 39 +-- ...ZkUploadPrepareOptionsFromBatchSettings.ts | 35 +-- .../upload/prepareHotlinkUploadRequest.ts | 4 +- .../utils/upload/prepareZkUploadRequest.ts | 4 +- 14 files changed, 333 insertions(+), 136 deletions(-) create mode 100644 frontend/src/types/upload/PreparedHotlinkUploadRequest.ts create mode 100644 frontend/src/types/upload/PreparedZkUploadRequest.ts create mode 100644 frontend/src/types/upload/UploadQueueEntry.ts delete mode 100644 frontend/src/types/upload/ZkUploadPrepareOptionsResult.ts create mode 100644 frontend/src/utils/file/makeKeyFromFile.ts create mode 100644 frontend/src/utils/upload/buildUploadOptionsFromBatchSettings.ts diff --git a/frontend/src/stores/metadata.ts b/frontend/src/stores/metadata.ts index a06540b..49b4297 100644 --- a/frontend/src/stores/metadata.ts +++ b/frontend/src/stores/metadata.ts @@ -2,23 +2,29 @@ import { defineStore, storeToRefs } from 'pinia'; import { ref } from 'vue'; import { useExiftoolStore } from '@/stores/exiftool'; import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview'; +import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile'; -const EXIFTOOL_PREPARE_ERROR_MESSAGE = 'Could not prepare ExifTool'; -const EXIFTOOL_INITIALIZING_MESSAGE = 'Initializing ExifTool'; -const METADATA_LOADING_MESSAGE = 'Loading metadata'; +const METADATA_IDLE_MESSAGE = 'Waiting to load metadata...'; +const EXIFTOOL_INITIALIZING_MESSAGE = 'Initializing ExifTool...'; +const METADATA_PROCESSING_MESSAGE = 'Stripping metadata...'; +const METADATA_READY_MESSAGE = 'Metadata stripped'; const METADATA_LOAD_ERROR_MESSAGE = 'Could not load image metadata'; const METADATA_STRIP_ERROR_MESSAGE = 'Could not strip image metadata'; -const METADATA_IDLE_MESSAGE = 'Waiting to load metadata'; -const METADATA_READY_MESSAGE = 'Metadata stripped'; +const EXIFTOOL_PREPARE_ERROR_MESSAGE = 'Could not prepare ExifTool'; const METADATA_IDLE_PREVIEW: MetadataPreviewEntry = { status: 'idle', message: METADATA_IDLE_MESSAGE }; -const METADATA_LOADING_PREVIEW: MetadataPreviewEntry = { - status: 'loading', - message: METADATA_LOADING_MESSAGE +const METADATA_LOAD_ERROR_PREVIEW: MetadataPreviewEntry = { + status: 'error', + message: METADATA_LOAD_ERROR_MESSAGE +}; + +const EXIFTOOL_PREPARE_ERROR_PREVIEW: MetadataPreviewEntry = { + status: 'error', + message: EXIFTOOL_PREPARE_ERROR_MESSAGE }; export const useMetadataStore = defineStore('metadata', () => { @@ -30,7 +36,13 @@ export const useMetadataStore = defineStore('metadata', () => { const { initStatus: exiftoolInitStatus } = storeToRefs(exiftoolStore); const { initializeExiftool, readMetadata, stripAllMetadata } = exiftoolStore; - const makeKeyFromFile = (file: File): string => `${file.name}\0${file.size}\0${file.lastModified}`; + const setPreviewEntry = (key: string, entry: MetadataPreviewEntry): void => { + previewsByKey.value = { ...previewsByKey.value, [key]: entry }; + }; + + const setStrippedBlob = (key: string, blob: Blob): void => { + strippedBlobByKey.value = { ...strippedBlobByKey.value, [key]: blob }; + }; const getPreview = (file?: File | null): MetadataPreviewEntry => { if (!file) { @@ -56,48 +68,27 @@ export const useMetadataStore = defineStore('metadata', () => { const key = makeKeyFromFile(file); if (exiftoolInitStatus.value === 'error') { - const entry: MetadataPreviewEntry = { - status: 'error', - message: EXIFTOOL_PREPARE_ERROR_MESSAGE - }; + setPreviewEntry(key, EXIFTOOL_PREPARE_ERROR_PREVIEW); - previewsByKey.value = { ...previewsByKey.value, [key]: entry }; - - return entry; + return EXIFTOOL_PREPARE_ERROR_PREVIEW; } - if (exiftoolInitStatus.value === 'ready') { - previewsByKey.value = { - ...previewsByKey.value, - [key]: METADATA_LOADING_PREVIEW - }; - } else { - previewsByKey.value = { - ...previewsByKey.value, - [key]: { status: 'pending', message: EXIFTOOL_INITIALIZING_MESSAGE } - }; + if (exiftoolInitStatus.value !== 'ready') { + setPreviewEntry(key, { status: 'initializing', message: EXIFTOOL_INITIALIZING_MESSAGE }); try { await initializeExiftool(); - - previewsByKey.value = { - ...previewsByKey.value, - [key]: METADATA_LOADING_PREVIEW - }; } catch (error) { console.error('[metadata] ExifTool init failed', { file: file.name, error }); - const entry: MetadataPreviewEntry = { - status: 'error', - message: EXIFTOOL_PREPARE_ERROR_MESSAGE - }; + setPreviewEntry(key, EXIFTOOL_PREPARE_ERROR_PREVIEW); - previewsByKey.value = { ...previewsByKey.value, [key]: entry }; - - return entry; + return EXIFTOOL_PREPARE_ERROR_PREVIEW; } } + setPreviewEntry(key, { status: 'processing', message: METADATA_PROCESSING_MESSAGE }); + let output: string; try { @@ -105,14 +96,9 @@ export const useMetadataStore = defineStore('metadata', () => { } catch (error) { console.error('[metadata] read failed', { file: file.name, error }); - const entry: MetadataPreviewEntry = { - status: 'error', - message: METADATA_LOAD_ERROR_MESSAGE - }; + setPreviewEntry(key, METADATA_LOAD_ERROR_PREVIEW); - previewsByKey.value = { ...previewsByKey.value, [key]: entry }; - - return entry; + return METADATA_LOAD_ERROR_PREVIEW; } let strippedBlob: Blob; @@ -129,10 +115,11 @@ export const useMetadataStore = defineStore('metadata', () => { const entry: MetadataPreviewEntry = { status: 'error', - message: METADATA_STRIP_ERROR_MESSAGE + message: METADATA_STRIP_ERROR_MESSAGE, + output }; - previewsByKey.value = { ...previewsByKey.value, [key]: entry }; + setPreviewEntry(key, entry); return entry; } @@ -149,17 +136,12 @@ export const useMetadataStore = defineStore('metadata', () => { } catch (error) { console.error('[metadata] read after strip failed', { file: file.name, error }); - const entry: MetadataPreviewEntry = { - status: 'error', - message: METADATA_LOAD_ERROR_MESSAGE - }; + setPreviewEntry(key, METADATA_LOAD_ERROR_PREVIEW); - previewsByKey.value = { ...previewsByKey.value, [key]: entry }; - - return entry; + return METADATA_LOAD_ERROR_PREVIEW; } - strippedBlobByKey.value = { ...strippedBlobByKey.value, [key]: strippedBlob }; + setStrippedBlob(key, strippedBlob); const entry: MetadataPreviewEntry = { status: 'ready', @@ -168,13 +150,22 @@ export const useMetadataStore = defineStore('metadata', () => { afterStripOutput }; - previewsByKey.value = { ...previewsByKey.value, [key]: entry }; + setPreviewEntry(key, entry); return entry; }; - const loadPreviews = (files: readonly File[]): Promise => - Promise.all(files.map(file => loadPreview(file))); + const loadPreviews = async (files: readonly File[]): Promise => { + const results: MetadataPreviewEntry[] = []; + + for (const file of files) { + const result = await loadPreview(file); + + results.push(result); + } + + return results; + }; const clearAllPreviews = (): void => { previewsByKey.value = {}; @@ -184,7 +175,6 @@ export const useMetadataStore = defineStore('metadata', () => { return { previewsByKey, strippedBlobByKey, - makeKeyFromFile, getPreview, getStrippedBlob, loadPreview, diff --git a/frontend/src/stores/upload.ts b/frontend/src/stores/upload.ts index 977f66a..ee96ff9 100644 --- a/frontend/src/stores/upload.ts +++ b/frontend/src/stores/upload.ts @@ -1,12 +1,91 @@ import { defineStore } from 'pinia'; +import { ref } from 'vue'; import { ALTCHA_REQUEST_HEADER } from '@/consts/altchaRequestHeader'; import { api } from '@/plugins/axios'; +import { useAltchaStore } from '@/stores/altcha'; +import { useMetadataStore } from '@/stores/metadata'; import type { CreateHotlinkUploadRequest } from '@/types/upload/CreateHotlinkUploadRequest'; import type { CreateZkUploadRequest } from '@/types/upload/CreateZkUploadRequest'; import type { CreatedUploadResult } from '@/types/upload/CreatedUploadResult'; +import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm'; +import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry'; +import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile'; +import { buildUploadOptionsFromBatchSettings } from '@/utils/upload/buildUploadOptionsFromBatchSettings'; +import { buildZkUploadPrepareOptionsFromBatchSettings } from '@/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings'; +import { prepareHotlinkUploadRequest } from '@/utils/upload/prepareHotlinkUploadRequest'; +import { prepareZkUploadRequest } from '@/utils/upload/prepareZkUploadRequest'; +import type { PreparedZkUploadRequest } from '@/types/upload/PreparedZkUploadRequest'; +import type { PreparedHotlinkUploadRequest } from '@/types/upload/PreparedHotlinkUploadRequest'; + +const UPLOAD_IDLE_MESSAGE = 'Waiting to upload...'; +const UPLOAD_QUEUED_MESSAGE = 'Upload queued'; +const UPLOAD_PREPARING_MESSAGE = 'Preparing upload...'; +const UPLOAD_UPLOADING_MESSAGE = 'Uploading...'; +const UPLOAD_UPLOADED_MESSAGE = 'Uploaded'; +const UPLOAD_READ_ERROR_MESSAGE = 'Could not read image'; +const UPLOAD_PROOF_OF_WORK_ERROR_MESSAGE = 'Proof of work failed'; +const UPLOAD_FAILED_MESSAGE = 'Upload failed'; +const UPLOAD_SKIPPED_METADATA_MESSAGE = 'Metadata not ready'; +const UPLOAD_STRIPPED_MISSING_MESSAGE = 'Stripped image missing'; +const UPLOAD_UNKNOWN_MODE_MESSAGE = 'Unknown share mode'; + +const UPLOAD_IDLE_ENTRY: UploadQueueEntry = { + status: 'idle', + message: UPLOAD_IDLE_MESSAGE +}; + +const UPLOAD_QUEUED_ENTRY: UploadQueueEntry = { + status: 'queued', + message: UPLOAD_QUEUED_MESSAGE +}; + +const UPLOAD_UPLOADING_ENTRY: UploadQueueEntry = { + status: 'uploading', + message: UPLOAD_UPLOADING_MESSAGE +}; + +const UPLOAD_UPLOADED_ENTRY: UploadQueueEntry = { + status: 'uploaded', + message: UPLOAD_UPLOADED_MESSAGE +}; + +const UPLOAD_FAILED_ENTRY: UploadQueueEntry = { + status: 'failed', + message: UPLOAD_FAILED_MESSAGE +}; + +const UPLOAD_STRIPPED_MISSING_ENTRY: UploadQueueEntry = { + status: 'failed', + message: UPLOAD_STRIPPED_MISSING_MESSAGE +}; export const useUploadStore = defineStore('upload', () => { - const createZkUpload = async (body: CreateZkUploadRequest, altcha?: string): Promise => { + const uploadByKey = ref>({}); + + const metadataStore = useMetadataStore(); + const altchaStore = useAltchaStore(); + + const { getPreview, getStrippedBlob } = metadataStore; + + const setUploadEntry = (key: string, entry: UploadQueueEntry): void => { + uploadByKey.value = { ...uploadByKey.value, [key]: entry }; + }; + + const getUpload = (file?: File | null): UploadQueueEntry => { + if (!file) { + return UPLOAD_IDLE_ENTRY; + } + + const key = makeKeyFromFile(file); + + return uploadByKey.value[key] ?? UPLOAD_IDLE_ENTRY; + }; + + const clearAllUploads = (): void => { + uploadByKey.value = {}; + }; + + const uploadZk = async (body: CreateZkUploadRequest, altcha?: string): Promise => { const { data } = await api.post('/uploads/zk', body, { headers: getUploadRequestHeaders(altcha) }); @@ -14,10 +93,7 @@ export const useUploadStore = defineStore('upload', () => { return data; }; - const createHotlinkUpload = async ( - body: CreateHotlinkUploadRequest, - altcha?: string - ): Promise => { + const uploadHotlink = async (body: CreateHotlinkUploadRequest, altcha?: string): Promise => { const { data } = await api.post('/uploads/hotlink', body, { headers: getUploadRequestHeaders(altcha) }); @@ -27,8 +103,151 @@ export const useUploadStore = defineStore('upload', () => { const getUploadRequestHeaders = (altcha?: string) => (altcha ? { [ALTCHA_REQUEST_HEADER]: altcha } : undefined); + const uploadBatch = async ( + files: readonly File[], + settings: UploadBatchSettingsForm, + uploadDespiteMetadataFailure = false + ): Promise => { + initializeBatchUploadState(files, uploadDespiteMetadataFailure); + + for (const file of files) { + const key = makeKeyFromFile(file); + const entry = uploadByKey.value[key]; + + if (!entry || entry.status !== 'queued') { + continue; + } + + const strippedBlob = getStrippedBlob(file); + const preview = getPreview(file); + const uploadOriginal = !strippedBlob && uploadDespiteMetadataFailure && preview.status === 'error'; + + if (!strippedBlob && !uploadOriginal) { + setUploadEntry(key, UPLOAD_STRIPPED_MISSING_ENTRY); + continue; + } + + const sourceBlob = strippedBlob ?? file; + + setUploadEntry(key, { status: 'preparing', message: UPLOAD_PREPARING_MESSAGE }); + let plaintext: Uint8Array; + const mimeType = sourceBlob.type || file.type || 'application/octet-stream'; + + try { + const arrayBuffer = await sourceBlob.arrayBuffer(); + + plaintext = new Uint8Array(arrayBuffer); + } catch (error) { + console.error('[upload] read stripped blob failed', { file: file.name, error }); + + setUploadEntry(key, { + status: 'failed', + message: UPLOAD_READ_ERROR_MESSAGE + }); + continue; + } + + let altcha: string | undefined; + + try { + altcha = await altchaStore.runProofOfWork(); + } catch (error) { + console.error('[upload] altcha failed', { file: file.name, error }); + + setUploadEntry(key, { + status: 'failed', + message: UPLOAD_PROOF_OF_WORK_ERROR_MESSAGE + }); + continue; + } + + if (settings.mode === 'zk') { + const options = buildZkUploadPrepareOptionsFromBatchSettings(settings); + + let prepared: PreparedZkUploadRequest; + + try { + prepared = await prepareZkUploadRequest(plaintext, options); + } catch (error) { + console.error('[upload] encrypt failed', { file: file.name, error }); + + setUploadEntry(key, UPLOAD_FAILED_ENTRY); + continue; + } + + setUploadEntry(key, UPLOAD_UPLOADING_ENTRY); + try { + const { request } = prepared; + + await uploadZk(request, altcha); + + setUploadEntry(key, UPLOAD_UPLOADED_ENTRY); + } catch (error) { + console.error('[upload] upload failed', { file: file.name, error }); + + setUploadEntry(key, UPLOAD_FAILED_ENTRY); + } + } else if (settings.mode === 'hotlink') { + const options = buildUploadOptionsFromBatchSettings(settings); + + let prepared: PreparedHotlinkUploadRequest; + + try { + prepared = await prepareHotlinkUploadRequest(plaintext, mimeType, options); + } catch (error) { + console.error('[upload] encrypt failed', { file: file.name, error }); + + setUploadEntry(key, UPLOAD_FAILED_ENTRY); + continue; + } + + setUploadEntry(key, UPLOAD_UPLOADING_ENTRY); + try { + const { request } = prepared; + + await uploadHotlink(request, altcha); + + setUploadEntry(key, UPLOAD_UPLOADED_ENTRY); + } catch (error) { + console.error('[upload] upload failed', { file: file.name, error }); + + setUploadEntry(key, UPLOAD_FAILED_ENTRY); + } + } else { + setUploadEntry(key, { status: 'failed', message: UPLOAD_UNKNOWN_MODE_MESSAGE }); + } + } + }; + + const initializeBatchUploadState = (files: readonly File[], uploadDespiteMetadataFailure: boolean): void => { + uploadByKey.value = {}; + + for (const file of files) { + const key = makeKeyFromFile(file); + const preview = getPreview(file); + const strippedBlob = getStrippedBlob(file); + + if (preview.status === 'ready') { + if (strippedBlob) { + setUploadEntry(key, UPLOAD_QUEUED_ENTRY); + } else { + setUploadEntry(key, UPLOAD_STRIPPED_MISSING_ENTRY); + } + } else if (preview.status === 'error' && uploadDespiteMetadataFailure) { + setUploadEntry(key, UPLOAD_QUEUED_ENTRY); + } else { + setUploadEntry(key, { status: 'skipped', message: UPLOAD_SKIPPED_METADATA_MESSAGE }); + } + + } + }; + return { - createZkUpload, - createHotlinkUpload + uploadByKey, + getUpload, + clearAllUploads, + uploadBatch, + uploadZk, + uploadHotlink }; }); diff --git a/frontend/src/types/metadata/MetadataPreview.ts b/frontend/src/types/metadata/MetadataPreview.ts index 576afe2..c29be71 100644 --- a/frontend/src/types/metadata/MetadataPreview.ts +++ b/frontend/src/types/metadata/MetadataPreview.ts @@ -1,6 +1,6 @@ export type MetadataPreviewEntry = | { status: 'idle'; message: string } - | { status: 'pending'; message: string } - | { status: 'loading'; message: string } + | { status: 'initializing'; message: string } + | { status: 'processing'; message: string } | { status: 'ready'; message: string; output: string; afterStripOutput: string } - | { status: 'error'; message: string }; + | { status: 'error'; message: string; output?: string }; diff --git a/frontend/src/types/upload/PreparedHotlinkUploadRequest.ts b/frontend/src/types/upload/PreparedHotlinkUploadRequest.ts new file mode 100644 index 0000000..f2074c3 --- /dev/null +++ b/frontend/src/types/upload/PreparedHotlinkUploadRequest.ts @@ -0,0 +1,6 @@ +import type { CreateHotlinkUploadRequest } from './CreateHotlinkUploadRequest'; + +export type PreparedHotlinkUploadRequest = { + secret: string; + request: CreateHotlinkUploadRequest; +}; diff --git a/frontend/src/types/upload/PreparedZkUploadRequest.ts b/frontend/src/types/upload/PreparedZkUploadRequest.ts new file mode 100644 index 0000000..e3e94a8 --- /dev/null +++ b/frontend/src/types/upload/PreparedZkUploadRequest.ts @@ -0,0 +1,6 @@ +import type { CreateZkUploadRequest } from './CreateZkUploadRequest'; + +export type PreparedZkUploadRequest = { + fragmentKey: string; + request: CreateZkUploadRequest; +}; diff --git a/frontend/src/types/upload/UploadQueueEntry.ts b/frontend/src/types/upload/UploadQueueEntry.ts new file mode 100644 index 0000000..699dab9 --- /dev/null +++ b/frontend/src/types/upload/UploadQueueEntry.ts @@ -0,0 +1,8 @@ +export type UploadQueueEntry = + | { status: 'idle'; message: string } + | { status: 'queued'; message: string } + | { status: 'preparing'; message: string } + | { status: 'uploading'; message: string } + | { status: 'uploaded'; message: string } + | { status: 'failed'; message: string } + | { status: 'skipped'; message: string }; diff --git a/frontend/src/types/upload/UploadQueueRow.ts b/frontend/src/types/upload/UploadQueueRow.ts index 290b7df..ee4452c 100644 --- a/frontend/src/types/upload/UploadQueueRow.ts +++ b/frontend/src/types/upload/UploadQueueRow.ts @@ -1,7 +1,9 @@ import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview'; +import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry'; export type UploadQueueRow = { file: File; key: string; - preview: MetadataPreviewEntry; + metadata: MetadataPreviewEntry; + upload: UploadQueueEntry; }; diff --git a/frontend/src/types/upload/ZkUploadPrepareOptionsResult.ts b/frontend/src/types/upload/ZkUploadPrepareOptionsResult.ts deleted file mode 100644 index e6d88f4..0000000 --- a/frontend/src/types/upload/ZkUploadPrepareOptionsResult.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ZkUploadPrepareOptions } from '@/types/upload/ZkUploadPrepareOptions'; - -export type ZkUploadPrepareOptionsResult = - | { ok: true; options: ZkUploadPrepareOptions } - | { ok: false; error: string }; diff --git a/frontend/src/utils/file/makeKeyFromFile.ts b/frontend/src/utils/file/makeKeyFromFile.ts new file mode 100644 index 0000000..b388036 --- /dev/null +++ b/frontend/src/utils/file/makeKeyFromFile.ts @@ -0,0 +1,2 @@ +export const makeKeyFromFile = (file: File): string => + `${file.name}\0${file.size}\0${file.lastModified}`; diff --git a/frontend/src/utils/upload/buildUploadOptionsFromBatchSettings.ts b/frontend/src/utils/upload/buildUploadOptionsFromBatchSettings.ts new file mode 100644 index 0000000..0b9ecd4 --- /dev/null +++ b/frontend/src/utils/upload/buildUploadOptionsFromBatchSettings.ts @@ -0,0 +1,9 @@ +import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm'; +import type { UploadOptionsRequest } from '@/types/upload/UploadOptionsRequest'; +import { resolveUploadBatchExpiresAt } from '@/utils/upload/resolveUploadBatchExpiresAt'; +import { resolveUploadBatchMaxViews } from '@/utils/upload/resolveUploadBatchMaxViews'; + +export const buildUploadOptionsFromBatchSettings = (settings: UploadBatchSettingsForm): UploadOptionsRequest => ({ + expiresAt: resolveUploadBatchExpiresAt(settings.expiryPreset), + maxViews: resolveUploadBatchMaxViews(settings.maxViewsPreset) +}); diff --git a/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.spec.ts b/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.spec.ts index fa11bcf..ee2ab9b 100644 --- a/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.spec.ts +++ b/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.spec.ts @@ -27,15 +27,10 @@ describe('buildZkUploadPrepareOptionsFromBatchSettings', () => { maxViewsPreset: '10' as const }; - const result = buildZkUploadPrepareOptionsFromBatchSettings(settings); - - expect(result).toEqual({ - ok: true, - options: { - expiresAt: '2026-06-02T00:00:00.000Z', - maxViews: 10, - passwordProtected: false - } + expect(buildZkUploadPrepareOptionsFromBatchSettings(settings)).toEqual({ + expiresAt: '2026-06-02T00:00:00.000Z', + maxViews: 10, + passwordProtected: false }); }); @@ -46,29 +41,11 @@ describe('buildZkUploadPrepareOptionsFromBatchSettings', () => { password: 'batch-secret' }; - const result = buildZkUploadPrepareOptionsFromBatchSettings(settings); - - expect(result).toEqual({ - ok: true, - options: { - expiresAt: null, - maxViews: null, - passwordProtected: true, - password: 'batch-secret' - } - }); - }); - - it('returns validation errors', () => { - const settings = { - ...baseSettings, - passwordEnabled: true, - password: '' - }; - expect(buildZkUploadPrepareOptionsFromBatchSettings(settings)).toEqual({ - ok: false, - error: 'Enter a password or turn off password protection.' + expiresAt: null, + maxViews: null, + passwordProtected: true, + password: 'batch-secret' }); }); }); diff --git a/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.ts b/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.ts index 8bf7320..d9e8229 100644 --- a/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.ts +++ b/frontend/src/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings.ts @@ -1,39 +1,22 @@ import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm'; -import type { ZkUploadPrepareOptionsResult } from '@/types/upload/ZkUploadPrepareOptionsResult'; -import { resolveUploadBatchExpiresAt } from '@/utils/upload/resolveUploadBatchExpiresAt'; -import { resolveUploadBatchMaxViews } from '@/utils/upload/resolveUploadBatchMaxViews'; -import { validateUploadBatchSettings } from '@/utils/upload/validateUploadBatchSettings'; +import type { ZkUploadPrepareOptions } from '@/types/upload/ZkUploadPrepareOptions'; +import { buildUploadOptionsFromBatchSettings } from '@/utils/upload/buildUploadOptionsFromBatchSettings'; export const buildZkUploadPrepareOptionsFromBatchSettings = ( settings: UploadBatchSettingsForm -): ZkUploadPrepareOptionsResult => { - const validationError = validateUploadBatchSettings(settings); - - if (validationError) { - return { ok: false, error: validationError }; - } - - const uploadOptions = { - expiresAt: resolveUploadBatchExpiresAt(settings.expiryPreset), - maxViews: resolveUploadBatchMaxViews(settings.maxViewsPreset) - }; +): ZkUploadPrepareOptions => { + const uploadOptions = buildUploadOptionsFromBatchSettings(settings); if (settings.passwordEnabled) { return { - ok: true, - options: { - ...uploadOptions, - passwordProtected: true, - password: settings.password - } + ...uploadOptions, + passwordProtected: true, + password: settings.password }; } return { - ok: true, - options: { - ...uploadOptions, - passwordProtected: false - } + ...uploadOptions, + passwordProtected: false }; }; diff --git a/frontend/src/utils/upload/prepareHotlinkUploadRequest.ts b/frontend/src/utils/upload/prepareHotlinkUploadRequest.ts index 0e6c508..5135adf 100644 --- a/frontend/src/utils/upload/prepareHotlinkUploadRequest.ts +++ b/frontend/src/utils/upload/prepareHotlinkUploadRequest.ts @@ -1,4 +1,4 @@ -import type { CreateHotlinkUploadRequest } from '@/types/upload/CreateHotlinkUploadRequest'; +import type { PreparedHotlinkUploadRequest } from '@/types/upload/PreparedHotlinkUploadRequest'; import type { UploadOptionsRequest } from '@/types/upload/UploadOptionsRequest'; import { bytesToBase64 } from '@/utils/encoding/bytesToBase64'; import { encryptHotlinkUpload } from '@/utils/upload/encryptHotlinkUpload'; @@ -7,7 +7,7 @@ export const prepareHotlinkUploadRequest = async ( plaintext: Uint8Array, mimeType: string, options: UploadOptionsRequest -): Promise<{ request: CreateHotlinkUploadRequest; secret: string }> => { +): Promise => { const encrypted = await encryptHotlinkUpload(plaintext); return { diff --git a/frontend/src/utils/upload/prepareZkUploadRequest.ts b/frontend/src/utils/upload/prepareZkUploadRequest.ts index fe834d2..67ae225 100644 --- a/frontend/src/utils/upload/prepareZkUploadRequest.ts +++ b/frontend/src/utils/upload/prepareZkUploadRequest.ts @@ -1,4 +1,4 @@ -import type { CreateZkUploadRequest } from '@/types/upload/CreateZkUploadRequest'; +import type { PreparedZkUploadRequest } from '@/types/upload/PreparedZkUploadRequest'; import type { ZkUploadPrepareOptions } from '@/types/upload/ZkUploadPrepareOptions'; import { bytesToBase64 } from '@/utils/encoding/bytesToBase64'; import { bytesToBase64Url } from '@/utils/encoding/bytesToBase64Url'; @@ -9,7 +9,7 @@ import { wrapZkDek } from '@/utils/upload/wrapZkDek'; export const prepareZkUploadRequest = async ( plaintext: Uint8Array, options: ZkUploadPrepareOptions -): Promise<{ request: CreateZkUploadRequest; fragmentKey: string }> => { +): Promise => { if (!options.passwordProtected) { const { dek, iv, ciphertext } = await encryptZkUpload(plaintext);