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:
@@ -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<MetadataPreviewEntry[]> =>
|
||||
Promise.all(files.map(file => loadPreview(file)));
|
||||
const loadPreviews = async (files: readonly File[]): Promise<MetadataPreviewEntry[]> => {
|
||||
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,
|
||||
|
||||
@@ -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<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, {
|
||||
headers: getUploadRequestHeaders(altcha)
|
||||
});
|
||||
@@ -14,10 +93,7 @@ export const useUploadStore = defineStore('upload', () => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const createHotlinkUpload = async (
|
||||
body: CreateHotlinkUploadRequest,
|
||||
altcha?: string
|
||||
): Promise<CreatedUploadResult> => {
|
||||
const uploadHotlink = async (body: CreateHotlinkUploadRequest, altcha?: string): Promise<CreatedUploadResult> => {
|
||||
const { data } = await api.post<CreatedUploadResult>('/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<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 {
|
||||
createZkUpload,
|
||||
createHotlinkUpload
|
||||
uploadByKey,
|
||||
getUpload,
|
||||
clearAllUploads,
|
||||
uploadBatch,
|
||||
uploadZk,
|
||||
uploadHotlink
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
const result = buildZkUploadPrepareOptionsFromBatchSettings(settings);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
options: {
|
||||
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: {
|
||||
expect(buildZkUploadPrepareOptionsFromBatchSettings(settings)).toEqual({
|
||||
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.'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
options: {
|
||||
...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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user