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.
This commit is contained in:
2026-10-23 18:34:13 +02:00
parent fe643d9831
commit 271c2b7b2d
14 changed files with 333 additions and 136 deletions
+49 -59
View File
@@ -2,23 +2,29 @@ import { defineStore, storeToRefs } from 'pinia';
import { ref } from 'vue'; import { ref } from 'vue';
import { useExiftoolStore } from '@/stores/exiftool'; import { useExiftoolStore } from '@/stores/exiftool';
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview'; import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
import { makeKeyFromFile } from '@/utils/file/makeKeyFromFile';
const EXIFTOOL_PREPARE_ERROR_MESSAGE = 'Could not prepare ExifTool'; const METADATA_IDLE_MESSAGE = 'Waiting to load metadata...';
const EXIFTOOL_INITIALIZING_MESSAGE = 'Initializing ExifTool'; const EXIFTOOL_INITIALIZING_MESSAGE = 'Initializing ExifTool...';
const METADATA_LOADING_MESSAGE = 'Loading metadata'; const METADATA_PROCESSING_MESSAGE = 'Stripping metadata...';
const METADATA_READY_MESSAGE = 'Metadata stripped';
const METADATA_LOAD_ERROR_MESSAGE = 'Could not load image metadata'; const METADATA_LOAD_ERROR_MESSAGE = 'Could not load image metadata';
const METADATA_STRIP_ERROR_MESSAGE = 'Could not strip image metadata'; const METADATA_STRIP_ERROR_MESSAGE = 'Could not strip image metadata';
const METADATA_IDLE_MESSAGE = 'Waiting to load metadata'; const EXIFTOOL_PREPARE_ERROR_MESSAGE = 'Could not prepare ExifTool';
const METADATA_READY_MESSAGE = 'Metadata stripped';
const METADATA_IDLE_PREVIEW: MetadataPreviewEntry = { const METADATA_IDLE_PREVIEW: MetadataPreviewEntry = {
status: 'idle', status: 'idle',
message: METADATA_IDLE_MESSAGE message: METADATA_IDLE_MESSAGE
}; };
const METADATA_LOADING_PREVIEW: MetadataPreviewEntry = { const METADATA_LOAD_ERROR_PREVIEW: MetadataPreviewEntry = {
status: 'loading', status: 'error',
message: METADATA_LOADING_MESSAGE message: METADATA_LOAD_ERROR_MESSAGE
};
const EXIFTOOL_PREPARE_ERROR_PREVIEW: MetadataPreviewEntry = {
status: 'error',
message: EXIFTOOL_PREPARE_ERROR_MESSAGE
}; };
export const useMetadataStore = defineStore('metadata', () => { export const useMetadataStore = defineStore('metadata', () => {
@@ -30,7 +36,13 @@ export const useMetadataStore = defineStore('metadata', () => {
const { initStatus: exiftoolInitStatus } = storeToRefs(exiftoolStore); const { initStatus: exiftoolInitStatus } = storeToRefs(exiftoolStore);
const { initializeExiftool, readMetadata, stripAllMetadata } = 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 => { const getPreview = (file?: File | null): MetadataPreviewEntry => {
if (!file) { if (!file) {
@@ -56,48 +68,27 @@ export const useMetadataStore = defineStore('metadata', () => {
const key = makeKeyFromFile(file); const key = makeKeyFromFile(file);
if (exiftoolInitStatus.value === 'error') { if (exiftoolInitStatus.value === 'error') {
const entry: MetadataPreviewEntry = { setPreviewEntry(key, EXIFTOOL_PREPARE_ERROR_PREVIEW);
status: 'error',
message: EXIFTOOL_PREPARE_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry }; return EXIFTOOL_PREPARE_ERROR_PREVIEW;
return entry;
} }
if (exiftoolInitStatus.value === 'ready') { if (exiftoolInitStatus.value !== 'ready') {
previewsByKey.value = { setPreviewEntry(key, { status: 'initializing', message: EXIFTOOL_INITIALIZING_MESSAGE });
...previewsByKey.value,
[key]: METADATA_LOADING_PREVIEW
};
} else {
previewsByKey.value = {
...previewsByKey.value,
[key]: { status: 'pending', message: EXIFTOOL_INITIALIZING_MESSAGE }
};
try { try {
await initializeExiftool(); await initializeExiftool();
previewsByKey.value = {
...previewsByKey.value,
[key]: METADATA_LOADING_PREVIEW
};
} catch (error) { } catch (error) {
console.error('[metadata] ExifTool init failed', { file: file.name, error }); console.error('[metadata] ExifTool init failed', { file: file.name, error });
const entry: MetadataPreviewEntry = { setPreviewEntry(key, EXIFTOOL_PREPARE_ERROR_PREVIEW);
status: 'error',
message: EXIFTOOL_PREPARE_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry }; return EXIFTOOL_PREPARE_ERROR_PREVIEW;
return entry;
} }
} }
setPreviewEntry(key, { status: 'processing', message: METADATA_PROCESSING_MESSAGE });
let output: string; let output: string;
try { try {
@@ -105,14 +96,9 @@ export const useMetadataStore = defineStore('metadata', () => {
} catch (error) { } catch (error) {
console.error('[metadata] read failed', { file: file.name, error }); console.error('[metadata] read failed', { file: file.name, error });
const entry: MetadataPreviewEntry = { setPreviewEntry(key, METADATA_LOAD_ERROR_PREVIEW);
status: 'error',
message: METADATA_LOAD_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry }; return METADATA_LOAD_ERROR_PREVIEW;
return entry;
} }
let strippedBlob: Blob; let strippedBlob: Blob;
@@ -129,10 +115,11 @@ export const useMetadataStore = defineStore('metadata', () => {
const entry: MetadataPreviewEntry = { const entry: MetadataPreviewEntry = {
status: 'error', status: 'error',
message: METADATA_STRIP_ERROR_MESSAGE message: METADATA_STRIP_ERROR_MESSAGE,
output
}; };
previewsByKey.value = { ...previewsByKey.value, [key]: entry }; setPreviewEntry(key, entry);
return entry; return entry;
} }
@@ -149,17 +136,12 @@ export const useMetadataStore = defineStore('metadata', () => {
} catch (error) { } catch (error) {
console.error('[metadata] read after strip failed', { file: file.name, error }); console.error('[metadata] read after strip failed', { file: file.name, error });
const entry: MetadataPreviewEntry = { setPreviewEntry(key, METADATA_LOAD_ERROR_PREVIEW);
status: 'error',
message: METADATA_LOAD_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry }; return METADATA_LOAD_ERROR_PREVIEW;
return entry;
} }
strippedBlobByKey.value = { ...strippedBlobByKey.value, [key]: strippedBlob }; setStrippedBlob(key, strippedBlob);
const entry: MetadataPreviewEntry = { const entry: MetadataPreviewEntry = {
status: 'ready', status: 'ready',
@@ -168,13 +150,22 @@ export const useMetadataStore = defineStore('metadata', () => {
afterStripOutput afterStripOutput
}; };
previewsByKey.value = { ...previewsByKey.value, [key]: entry }; setPreviewEntry(key, entry);
return entry; return entry;
}; };
const loadPreviews = (files: readonly File[]): Promise<MetadataPreviewEntry[]> => const loadPreviews = async (files: readonly File[]): Promise<MetadataPreviewEntry[]> => {
Promise.all(files.map(file => loadPreview(file))); const results: MetadataPreviewEntry[] = [];
for (const file of files) {
const result = await loadPreview(file);
results.push(result);
}
return results;
};
const clearAllPreviews = (): void => { const clearAllPreviews = (): void => {
previewsByKey.value = {}; previewsByKey.value = {};
@@ -184,7 +175,6 @@ export const useMetadataStore = defineStore('metadata', () => {
return { return {
previewsByKey, previewsByKey,
strippedBlobByKey, strippedBlobByKey,
makeKeyFromFile,
getPreview, getPreview,
getStrippedBlob, getStrippedBlob,
loadPreview, loadPreview,
+226 -7
View File
@@ -1,12 +1,91 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue';
import { ALTCHA_REQUEST_HEADER } from '@/consts/altchaRequestHeader'; import { ALTCHA_REQUEST_HEADER } from '@/consts/altchaRequestHeader';
import { api } from '@/plugins/axios'; import { api } from '@/plugins/axios';
import { useAltchaStore } from '@/stores/altcha';
import { useMetadataStore } from '@/stores/metadata';
import type { CreateHotlinkUploadRequest } from '@/types/upload/CreateHotlinkUploadRequest'; import type { CreateHotlinkUploadRequest } from '@/types/upload/CreateHotlinkUploadRequest';
import type { CreateZkUploadRequest } from '@/types/upload/CreateZkUploadRequest'; import type { CreateZkUploadRequest } from '@/types/upload/CreateZkUploadRequest';
import type { CreatedUploadResult } from '@/types/upload/CreatedUploadResult'; 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', () => { export const useUploadStore = defineStore('upload', () => {
const createZkUpload = async (body: CreateZkUploadRequest, altcha?: string): Promise<CreatedUploadResult> => { const uploadByKey = ref<Record<string, UploadQueueEntry>>({});
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<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)
}); });
@@ -14,10 +93,7 @@ export const useUploadStore = defineStore('upload', () => {
return data; return data;
}; };
const createHotlinkUpload = async ( const uploadHotlink = async (body: CreateHotlinkUploadRequest, altcha?: string): Promise<CreatedUploadResult> => {
body: CreateHotlinkUploadRequest,
altcha?: 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)
}); });
@@ -27,8 +103,151 @@ export const useUploadStore = defineStore('upload', () => {
const getUploadRequestHeaders = (altcha?: string) => (altcha ? { [ALTCHA_REQUEST_HEADER]: altcha } : undefined); const getUploadRequestHeaders = (altcha?: string) => (altcha ? { [ALTCHA_REQUEST_HEADER]: altcha } : undefined);
const uploadBatch = async (
files: readonly File[],
settings: UploadBatchSettingsForm,
uploadDespiteMetadataFailure = false
): Promise<void> => {
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 { return {
createZkUpload, uploadByKey,
createHotlinkUpload getUpload,
clearAllUploads,
uploadBatch,
uploadZk,
uploadHotlink
}; };
}); });
@@ -1,6 +1,6 @@
export type MetadataPreviewEntry = export type MetadataPreviewEntry =
| { status: 'idle'; message: string } | { status: 'idle'; message: string }
| { status: 'pending'; message: string } | { status: 'initializing'; message: string }
| { status: 'loading'; message: string } | { status: 'processing'; message: string }
| { status: 'ready'; message: string; output: string; afterStripOutput: string } | { status: 'ready'; message: string; output: string; afterStripOutput: string }
| { status: 'error'; message: string }; | { status: 'error'; message: string; output?: string };
@@ -0,0 +1,6 @@
import type { CreateHotlinkUploadRequest } from './CreateHotlinkUploadRequest';
export type PreparedHotlinkUploadRequest = {
secret: string;
request: CreateHotlinkUploadRequest;
};
@@ -0,0 +1,6 @@
import type { CreateZkUploadRequest } from './CreateZkUploadRequest';
export type PreparedZkUploadRequest = {
fragmentKey: string;
request: CreateZkUploadRequest;
};
@@ -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 };
+3 -1
View File
@@ -1,7 +1,9 @@
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview'; import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
export type UploadQueueRow = { export type UploadQueueRow = {
file: File; file: File;
key: string; key: string;
preview: MetadataPreviewEntry; metadata: MetadataPreviewEntry;
upload: UploadQueueEntry;
}; };
@@ -1,5 +0,0 @@
import type { ZkUploadPrepareOptions } from '@/types/upload/ZkUploadPrepareOptions';
export type ZkUploadPrepareOptionsResult =
| { ok: true; options: ZkUploadPrepareOptions }
| { ok: false; error: string };
@@ -0,0 +1,2 @@
export const makeKeyFromFile = (file: File): string =>
`${file.name}\0${file.size}\0${file.lastModified}`;
@@ -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)
});
@@ -27,15 +27,10 @@ describe('buildZkUploadPrepareOptionsFromBatchSettings', () => {
maxViewsPreset: '10' as const maxViewsPreset: '10' as const
}; };
const result = buildZkUploadPrepareOptionsFromBatchSettings(settings); expect(buildZkUploadPrepareOptionsFromBatchSettings(settings)).toEqual({
expect(result).toEqual({
ok: true,
options: {
expiresAt: '2026-06-02T00:00:00.000Z', expiresAt: '2026-06-02T00:00:00.000Z',
maxViews: 10, maxViews: 10,
passwordProtected: false passwordProtected: false
}
}); });
}); });
@@ -46,29 +41,11 @@ describe('buildZkUploadPrepareOptionsFromBatchSettings', () => {
password: 'batch-secret' password: 'batch-secret'
}; };
const result = buildZkUploadPrepareOptionsFromBatchSettings(settings); expect(buildZkUploadPrepareOptionsFromBatchSettings(settings)).toEqual({
expect(result).toEqual({
ok: true,
options: {
expiresAt: null, expiresAt: null,
maxViews: null, maxViews: null,
passwordProtected: true, passwordProtected: true,
password: 'batch-secret' 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.'
}); });
}); });
}); });
@@ -1,39 +1,22 @@
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm'; import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
import type { ZkUploadPrepareOptionsResult } from '@/types/upload/ZkUploadPrepareOptionsResult'; import type { ZkUploadPrepareOptions } from '@/types/upload/ZkUploadPrepareOptions';
import { resolveUploadBatchExpiresAt } from '@/utils/upload/resolveUploadBatchExpiresAt'; import { buildUploadOptionsFromBatchSettings } from '@/utils/upload/buildUploadOptionsFromBatchSettings';
import { resolveUploadBatchMaxViews } from '@/utils/upload/resolveUploadBatchMaxViews';
import { validateUploadBatchSettings } from '@/utils/upload/validateUploadBatchSettings';
export const buildZkUploadPrepareOptionsFromBatchSettings = ( export const buildZkUploadPrepareOptionsFromBatchSettings = (
settings: UploadBatchSettingsForm settings: UploadBatchSettingsForm
): ZkUploadPrepareOptionsResult => { ): ZkUploadPrepareOptions => {
const validationError = validateUploadBatchSettings(settings); const uploadOptions = buildUploadOptionsFromBatchSettings(settings);
if (validationError) {
return { ok: false, error: validationError };
}
const uploadOptions = {
expiresAt: resolveUploadBatchExpiresAt(settings.expiryPreset),
maxViews: resolveUploadBatchMaxViews(settings.maxViewsPreset)
};
if (settings.passwordEnabled) { if (settings.passwordEnabled) {
return { return {
ok: true,
options: {
...uploadOptions, ...uploadOptions,
passwordProtected: true, passwordProtected: true,
password: settings.password password: settings.password
}
}; };
} }
return { return {
ok: true,
options: {
...uploadOptions, ...uploadOptions,
passwordProtected: false passwordProtected: false
}
}; };
}; };
@@ -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 type { UploadOptionsRequest } from '@/types/upload/UploadOptionsRequest';
import { bytesToBase64 } from '@/utils/encoding/bytesToBase64'; import { bytesToBase64 } from '@/utils/encoding/bytesToBase64';
import { encryptHotlinkUpload } from '@/utils/upload/encryptHotlinkUpload'; import { encryptHotlinkUpload } from '@/utils/upload/encryptHotlinkUpload';
@@ -7,7 +7,7 @@ export const prepareHotlinkUploadRequest = async (
plaintext: Uint8Array, plaintext: Uint8Array,
mimeType: string, mimeType: string,
options: UploadOptionsRequest options: UploadOptionsRequest
): Promise<{ request: CreateHotlinkUploadRequest; secret: string }> => { ): Promise<PreparedHotlinkUploadRequest> => {
const encrypted = await encryptHotlinkUpload(plaintext); const encrypted = await encryptHotlinkUpload(plaintext);
return { return {
@@ -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 type { ZkUploadPrepareOptions } from '@/types/upload/ZkUploadPrepareOptions';
import { bytesToBase64 } from '@/utils/encoding/bytesToBase64'; import { bytesToBase64 } from '@/utils/encoding/bytesToBase64';
import { bytesToBase64Url } from '@/utils/encoding/bytesToBase64Url'; import { bytesToBase64Url } from '@/utils/encoding/bytesToBase64Url';
@@ -9,7 +9,7 @@ import { wrapZkDek } from '@/utils/upload/wrapZkDek';
export const prepareZkUploadRequest = async ( export const prepareZkUploadRequest = async (
plaintext: Uint8Array, plaintext: Uint8Array,
options: ZkUploadPrepareOptions options: ZkUploadPrepareOptions
): Promise<{ request: CreateZkUploadRequest; fragmentKey: string }> => { ): Promise<PreparedZkUploadRequest> => {
if (!options.passwordProtected) { if (!options.passwordProtected) {
const { dek, iv, ciphertext } = await encryptZkUpload(plaintext); const { dek, iv, ciphertext } = await encryptZkUpload(plaintext);