Files
private_image/frontend/src/stores/upload.ts
T

270 lines
9.8 KiB
TypeScript

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_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 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)
});
return data;
};
const uploadHotlink = async (body: CreateHotlinkUploadRequest, altcha?: string): Promise<CreatedUploadResult> => {
const { data } = await api.post<CreatedUploadResult>('/uploads/hotlink', body, {
headers: getUploadRequestHeaders(altcha)
});
return data;
};
const deleteUpload = async (uploadId: string, deleteToken: string): Promise<void> => {
await api.delete(`/uploads/${uploadId}`, {
params: { delete_token: deleteToken }
});
};
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, fragmentKey } = prepared;
const result = await uploadZk(request, altcha);
setUploadEntry(key, {
status: 'uploaded',
message: UPLOAD_UPLOADED_MESSAGE,
uploadId: result.id,
mode: 'zk',
deleteToken: result.deleteToken,
urlSecret: fragmentKey
});
} 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, secret } = prepared;
const result = await uploadHotlink(request, altcha);
setUploadEntry(key, {
status: 'uploaded',
message: UPLOAD_UPLOADED_MESSAGE,
uploadId: result.id,
mode: 'hotlink',
deleteToken: result.deleteToken,
urlSecret: secret
});
} 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 {
uploadByKey,
getUpload,
clearAllUploads,
uploadBatch,
uploadZk,
uploadHotlink,
deleteUpload
};
});