270 lines
9.8 KiB
TypeScript
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
|
|
};
|
|
});
|