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
@@ -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
};
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'
});
});
});
@@ -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
};
};
@@ -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<PreparedHotlinkUploadRequest> => {
const encrypted = await encryptHotlinkUpload(plaintext);
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 { 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<PreparedZkUploadRequest> => {
if (!options.passwordProtected) {
const { dek, iv, ciphertext } = await encryptZkUpload(plaintext);