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:
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user