UNPKG

cod-dicomweb-server

Version:

A wadors server proxy that get data from a Cloud Optimized Dicom format.

337 lines (336 loc) 16.4 kB
import { parseDicom } from 'dicom-parser'; import FileManager from '../fileManager'; import MetadataManager from '../metadataManager'; import { getFrameDetailsFromMetadata, parseWadorsURL } from './utils'; import { register } from '../dataRetrieval/register'; import constants, { Enums } from '../constants'; import { getDataRetrievalManager } from '../dataRetrieval/dataRetrievalManager'; import { CustomError } from './customClasses'; import { CustomErrorEvent } from './customClasses'; import { download, getDirectoryHandle } from '../fileAccessSystemUtils'; class CodDicomWebServer { filePromises = {}; options = { maxWorkerFetchSize: Infinity, domain: constants.url.DOMAIN, enableLocalCache: false }; fileManager; metadataManager; seriesUidFileUrls = {}; constructor(args = {}) { const { maxWorkerFetchSize, domain, disableWorker, enableLocalCache } = args; this.options.maxWorkerFetchSize = maxWorkerFetchSize || this.options.maxWorkerFetchSize; this.options.domain = domain || this.options.domain; this.options.enableLocalCache = !!enableLocalCache; const fileStreamingScriptName = constants.dataRetrieval.FILE_STREAMING_WORKER_NAME; const filePartialScriptName = constants.dataRetrieval.FILE_PARTIAL_WORKER_NAME; this.fileManager = new FileManager({ fileStreamingScriptName }); this.metadataManager = new MetadataManager(); if (disableWorker) { const dataRetrievalManager = getDataRetrievalManager(); dataRetrievalManager.setDataRetrieverMode(Enums.DataRetrieveMode.REQUEST); } register({ fileStreamingScriptName, filePartialScriptName }, this.options.maxWorkerFetchSize); } setOptions = (newOptions) => { Object.keys(newOptions).forEach((key) => { if (newOptions[key] !== undefined) { this.options[key] = newOptions[key]; } }); }; getOptions = () => { return this.options; }; addFileUrl(seriesInstanceUID, type, url) { if (this.seriesUidFileUrls[seriesInstanceUID]) { this.seriesUidFileUrls[seriesInstanceUID].add({ type, url }); } else { this.seriesUidFileUrls[seriesInstanceUID] = new Set([{ type, url }]); } } async fetchCod(wadorsUrl, headers = {}, { useSharedArrayBuffer = false, fetchType = constants.Enums.FetchType.API_OPTIMIZED } = {}) { try { if (!wadorsUrl) { throw new CustomError('Url not provided'); } const parsedDetails = parseWadorsURL(wadorsUrl, this.options.domain); if (parsedDetails) { const { type, bucketName, bucketPrefix, studyInstanceUID, seriesInstanceUID, sopInstanceUID, frameNumber } = parsedDetails; const metadataJson = await this.metadataManager.getMetadata({ domain: this.options.domain, bucketName, bucketPrefix, studyInstanceUID, seriesInstanceUID }, headers); if (!metadataJson) { throw new CustomError(`Metadata not found for ${wadorsUrl}`); } const { url: fileUrl, startByte, endByte, thumbnailUrl, isMultiframe } = getFrameDetailsFromMetadata(metadataJson, sopInstanceUID, frameNumber - 1, { domain: this.options.domain, bucketName, bucketPrefix }); switch (type) { case Enums.RequestType.THUMBNAIL: if (!thumbnailUrl) { throw new CustomError(`Thumbnail not found for ${wadorsUrl}`); } this.addFileUrl(seriesInstanceUID, Enums.URLType.THUMBNAIL, thumbnailUrl); return this.fetchFile(thumbnailUrl, headers, { useSharedArrayBuffer }); case Enums.RequestType.FRAME: { if (!fileUrl) { throw new CustomError('Url not found for frame'); } let urlWithBytes = fileUrl; if (fetchType === Enums.FetchType.BYTES_OPTIMIZED) { urlWithBytes = `${fileUrl}?bytes=${startByte}-${endByte}`; } this.addFileUrl(seriesInstanceUID, Enums.URLType.FILE, fileUrl); return this.fetchFile(urlWithBytes, headers, { offsets: { startByte, endByte }, useSharedArrayBuffer, fetchType }).then((arraybuffer) => { if (!arraybuffer?.byteLength) { throw new CustomError('File Arraybuffer is not found'); } if (isMultiframe) { return arraybuffer; } else { const dataSet = parseDicom(new Uint8Array(arraybuffer)); const pixelDataElement = dataSet.elements.x7fe00010; let { dataOffset, length } = pixelDataElement; if (pixelDataElement.hadUndefinedLength && pixelDataElement.fragments) { ({ position: dataOffset, length } = pixelDataElement.fragments[0]); } else { // Adding 8 bytes for 4 bytes tag + 4 bytes length for uncomppressed pixelData dataOffset += 8; } return arraybuffer.slice(dataOffset, dataOffset + length); } }); } case Enums.RequestType.SERIES_METADATA: case Enums.RequestType.INSTANCE_METADATA: return this.parseMetadata(metadataJson, type, sopInstanceUID); default: throw new CustomError(`Unsupported request type: ${type}`); } } else { return new Promise((resolve, reject) => { return this.fetchFile(wadorsUrl, headers, { useSharedArrayBuffer }) .then((result) => { if (result instanceof ArrayBuffer) { try { const dataSet = parseDicom(new Uint8Array(result)); const seriesInstanceUID = dataSet.string('0020000e'); if (seriesInstanceUID) { this.addFileUrl(seriesInstanceUID, Enums.URLType.OTHERS, wadorsUrl); } } catch (error) { console.warn('CodDicomWebServer.ts: There is some issue parsing the file.', error); } } resolve(result); }) .catch((error) => reject(error)); }); } } catch (error) { const newError = new CustomError(`CodDicomWebServer.ts: ${error.message || 'An error occured when fetching the COD'}`); console.error(newError); throw newError; } } async fetchFile(fileUrl, headers, { offsets, useSharedArrayBuffer = false, fetchType = constants.Enums.FetchType.API_OPTIMIZED } = {}) { const isBytesOptimized = fetchType === Enums.FetchType.BYTES_OPTIMIZED; const extractedFile = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets); if (extractedFile) { return new Promise((resolveRequest, rejectRequest) => { try { resolveRequest(extractedFile.buffer); } catch (error) { rejectRequest(error); } }); } const directoryHandle = this.options.enableLocalCache && (await getDirectoryHandle()); const { maxWorkerFetchSize } = this.getOptions(); const dataRetrievalManager = getDataRetrievalManager(); const { FILE_STREAMING_WORKER_NAME, FILE_PARTIAL_WORKER_NAME, THRESHOLD } = constants.dataRetrieval; let tarPromise; if (!this.filePromises[fileUrl]) { tarPromise = new Promise((resolveFile, rejectFile) => { if (this.fileManager.getTotalSize() + THRESHOLD > maxWorkerFetchSize) { throw new CustomError(`CodDicomWebServer.ts: Maximum size(${maxWorkerFetchSize}) for fetching files reached`); } const FetchTypeEnum = constants.Enums.FetchType; if (fetchType === FetchTypeEnum.API_OPTIMIZED) { const handleFirstChunk = (evt) => { if (evt instanceof CustomErrorEvent) { rejectFile(evt.error); throw evt.error; } const { url, position, fileArraybuffer } = evt.data; if (url === fileUrl && fileArraybuffer) { this.fileManager.set(url, { data: fileArraybuffer, position }); dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk); } }; dataRetrievalManager.addEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk); dataRetrievalManager .executeTask(FILE_STREAMING_WORKER_NAME, 'stream', { url: fileUrl, headers: headers, useSharedArrayBuffer, directoryHandle }) .then(() => { resolveFile(); }) .catch((error) => { rejectFile(error); }) .then(() => { dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk); delete this.filePromises[fileUrl]; }); } else if (fetchType === FetchTypeEnum.BYTES_OPTIMIZED && offsets) { const { startByte, endByte } = offsets; const bytesRemovedUrl = fileUrl.split('?bytes=')[0]; const handleSlice = (evt) => { if (evt instanceof CustomErrorEvent) { rejectFile(evt.error); throw evt.error; } const { url, fileArraybuffer, offsets } = evt.data; if (url === bytesRemovedUrl && offsets.startByte === startByte && offsets.endByte === endByte) { this.fileManager.set(fileUrl, { data: fileArraybuffer, position: fileArraybuffer.length }); dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice); resolveFile(); } }; dataRetrievalManager.addEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice); dataRetrievalManager .executeTask(FILE_PARTIAL_WORKER_NAME, 'partial', { url: bytesRemovedUrl, offsets: { startByte, endByte }, headers, directoryHandle }) .catch((error) => { rejectFile(error); }) .then(() => { dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice); delete this.filePromises[fileUrl]; }); } else { rejectFile(new CustomError('CodDicomWebServer.ts: Offsets is needed in bytes optimized fetching')); } }); this.filePromises[fileUrl] = tarPromise; } else { tarPromise = this.filePromises[fileUrl]; } return new Promise((resolveRequest, rejectRequest) => { let requestResolved = false; const handleChunkAppend = (evt) => { if (evt instanceof CustomErrorEvent) { rejectRequest(evt.message); throw evt.error; } const { url, position, chunk, isAppending } = evt.data; if (isAppending) { if (chunk) { this.fileManager.append(url, chunk, position); } else { this.fileManager.setPosition(url, position); } } if (!requestResolved && url === fileUrl && offsets && position > offsets.endByte) { try { const file = this.fileManager.get(url, offsets); requestResolved = true; resolveRequest(file?.buffer); } catch (error) { rejectRequest(error); } } }; if (offsets && !isBytesOptimized) { dataRetrievalManager.addEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend); } tarPromise .then(() => { if (!requestResolved) { if (this.fileManager.getPosition(fileUrl)) { const file = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets); requestResolved = true; resolveRequest(file?.buffer); } else { rejectRequest(new CustomError(`File - ${fileUrl} not found`)); } } }) .catch((error) => { rejectRequest(error); }) .then(() => { dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend); }); }); } downloadSeriesFile(seriesInstanceUID) { const seriesFileURL = Array.from(this.seriesUidFileUrls[seriesInstanceUID]).find(({ url, type }) => type === Enums.URLType.FILE && url.endsWith('.tar'))?.url; if (seriesFileURL) { const seriesArrayBuffer = this.fileManager.get(seriesFileURL); return download(seriesFileURL.split('/').at(-1), seriesArrayBuffer); } return false; } delete(seriesInstanceUID) { const fileUrls = this.seriesUidFileUrls[seriesInstanceUID]; if (fileUrls) { fileUrls.forEach(({ url }) => { this.fileManager.remove(url); }); } delete this.seriesUidFileUrls[seriesInstanceUID]; } deleteAll() { Object.values(this.seriesUidFileUrls).forEach((fileUrls) => { fileUrls.forEach(({ url }) => { this.fileManager.remove(url); }); }); this.seriesUidFileUrls = {}; } parseMetadata(metadata, type, sopInstanceUID) { if (type === Enums.RequestType.INSTANCE_METADATA) { return Object.entries(metadata.cod.instances).find(([key, instance]) => key === sopInstanceUID)?.[1].metadata; } else { return Object.values(metadata.cod.instances).map((instance) => instance.metadata); } } } export default CodDicomWebServer;