Files
private_image/frontend/src/stores/metadata.ts
T
nobswebdev 1826cef8fe Strip metadata in the ExifTool worker during upload preview.
Cache stripped blobs, show original vs stripped metadata tabs, and keep user-facing errors on fixed store messages with console logging for diagnostics.
2026-10-18 18:02:49 +02:00

195 lines
5.8 KiB
TypeScript

import { defineStore, storeToRefs } from 'pinia';
import { ref } from 'vue';
import { useExiftoolStore } from '@/stores/exiftool';
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
const EXIFTOOL_PREPARE_ERROR_MESSAGE = 'Could not prepare ExifTool';
const EXIFTOOL_INITIALIZING_MESSAGE = 'Initializing ExifTool';
const METADATA_LOADING_MESSAGE = 'Loading metadata';
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 METADATA_IDLE_PREVIEW: MetadataPreviewEntry = {
status: 'idle',
message: METADATA_IDLE_MESSAGE
};
const METADATA_LOADING_PREVIEW: MetadataPreviewEntry = {
status: 'loading',
message: METADATA_LOADING_MESSAGE
};
export const useMetadataStore = defineStore('metadata', () => {
const previewsByKey = ref<Record<string, MetadataPreviewEntry>>({});
const strippedBlobByKey = ref<Record<string, Blob>>({});
const exiftoolStore = useExiftoolStore();
const { initStatus: exiftoolInitStatus } = storeToRefs(exiftoolStore);
const { initializeExiftool, readMetadata, stripAllMetadata } = exiftoolStore;
const makeKeyFromFile = (file: File): string => `${file.name}\0${file.size}\0${file.lastModified}`;
const getPreview = (file?: File | null): MetadataPreviewEntry => {
if (!file) {
return METADATA_IDLE_PREVIEW;
}
const key = makeKeyFromFile(file);
return previewsByKey.value[key] ?? METADATA_IDLE_PREVIEW;
};
const getStrippedBlob = (file?: File | null): Blob | null => {
if (!file) {
return null;
}
const key = makeKeyFromFile(file);
return strippedBlobByKey.value[key] ?? null;
};
const loadPreview = async (file: File): Promise<MetadataPreviewEntry> => {
const key = makeKeyFromFile(file);
if (exiftoolInitStatus.value === 'error') {
const entry: MetadataPreviewEntry = {
status: 'error',
message: EXIFTOOL_PREPARE_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry };
return entry;
}
if (exiftoolInitStatus.value === 'ready') {
previewsByKey.value = {
...previewsByKey.value,
[key]: METADATA_LOADING_PREVIEW
};
} else {
previewsByKey.value = {
...previewsByKey.value,
[key]: { status: 'pending', 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
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry };
return entry;
}
}
let output: string;
try {
output = await readMetadata(file);
} catch (error) {
console.error('[metadata] read failed', { file: file.name, error });
const entry: MetadataPreviewEntry = {
status: 'error',
message: METADATA_LOAD_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry };
return entry;
}
let strippedBlob: Blob;
try {
strippedBlob = await stripAllMetadata(file);
} catch (error) {
console.error('[metadata] strip failed', {
file: file.name,
type: file.type,
size: file.size,
error
});
const entry: MetadataPreviewEntry = {
status: 'error',
message: METADATA_STRIP_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry };
return entry;
}
const strippedFile = new File([strippedBlob], file.name, {
type: file.type || strippedBlob.type || 'application/octet-stream',
lastModified: file.lastModified
});
let afterStripOutput: string;
try {
afterStripOutput = await readMetadata(strippedFile);
} catch (error) {
console.error('[metadata] read after strip failed', { file: file.name, error });
const entry: MetadataPreviewEntry = {
status: 'error',
message: METADATA_LOAD_ERROR_MESSAGE
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry };
return entry;
}
strippedBlobByKey.value = { ...strippedBlobByKey.value, [key]: strippedBlob };
const entry: MetadataPreviewEntry = {
status: 'ready',
message: METADATA_READY_MESSAGE,
output,
afterStripOutput
};
previewsByKey.value = { ...previewsByKey.value, [key]: entry };
return entry;
};
const loadPreviews = (files: readonly File[]): Promise<MetadataPreviewEntry[]> =>
Promise.all(files.map(file => loadPreview(file)));
const clearAllPreviews = (): void => {
previewsByKey.value = {};
strippedBlobByKey.value = {};
};
return {
previewsByKey,
strippedBlobByKey,
makeKeyFromFile,
getPreview,
getStrippedBlob,
loadPreview,
loadPreviews,
clearAllPreviews
};
});