Skip to content

Support for object versioning #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: gradienthealth/segmentation_mode_sheet_integration
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import {

import getImageId from '../DicomWebDataSource/utils/getImageId';
import _ from 'lodash';
import {
getCurrentLiveVersion,
removeObjectsVersions,
} from '../utils/cloudObjectVersionActions';
import parseUrlToBucketAndFileName from '../utils/parseUrlToBucketAndFileName';

const metadataProvider = classes.MetadataProvider;
const { datasetToBlob } = dcmjs.data;
Expand Down Expand Up @@ -254,6 +259,40 @@ const mapSegSeriesFromDataSet = (dataSet) => {
};
};

const removeCurrentSegVersionsIfNecessary = async (
bucket,
currentDicomVersionDetails,
currentJsonVersionDetails,
headers
) => {
const objectsVersionsAndFileNames = [];

// Remove the current version of metadata json file since we only need a single version.
if (currentJsonVersionDetails) {
objectsVersionsAndFileNames.push(currentJsonVersionDetails);
}

if (
currentDicomVersionDetails &&
Date.now() - Date.parse(currentDicomVersionDetails.version.updated) <=
5 * 60 * 1000 // 5 minutes
) {
// Remove the current version of segmentation dicom files if the last modification is within 5 minutes
objectsVersionsAndFileNames.push(currentDicomVersionDetails);
}

return removeObjectsVersions(
bucket,
objectsVersionsAndFileNames,
headers
).catch((error) => {
throw new Error(
error.message ||
'Failed to remove previous version of dicom file and/or json file'
);
});
};

const storeDicomSeg = async (naturalizedReport, headers, displaySetService) => {
const {
StudyInstanceUID,
Expand All @@ -271,22 +310,34 @@ const storeDicomSeg = async (naturalizedReport, headers, displaySetService) => {
const segPrefix = params.get('seg-prefix') || prefix
const filteredDescription = SeriesDescription.replace(/[/]/g, '');

let fileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent(
let dicomFileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent(
filteredDescription
)}.dcm`;
const jsonFileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/metadata`;

const segDisplaySet = displaySetService.getDisplaySetsBy(
(ds) =>
ds.SeriesInstanceUID === SeriesInstanceUID &&
ds.instance.SOPInstanceUID === SOPInstanceUID
)[0];

let currentDicomVersionDetails, currentJsonVersionDetails;
if (segDisplaySet) {
const url = segDisplaySet.instance.url;
segBucket = url.split('https://storage.googleapis.com/')[1].split('/')[0];
fileName = url.split(`https://storage.googleapis.com/${segBucket}/`)[1];
({ bucket: segBucket, fileName: dicomFileName } =
parseUrlToBucketAndFileName(url));

currentDicomVersionDetails = {
fileName: dicomFileName,
version: await getCurrentLiveVersion(segBucket, dicomFileName, headers),
};
currentJsonVersionDetails = {
fileName: jsonFileName,
version: await getCurrentLiveVersion(segBucket, jsonFileName, headers),
};
}

const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${fileName}&contentEncoding=gzip`;
const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${dicomFileName}&contentEncoding=gzip`;
const blob = datasetToBlob(naturalizedReport);
const compressedFile = pako.gzip(await blob.arrayBuffer());

Expand All @@ -313,7 +364,7 @@ const storeDicomSeg = async (naturalizedReport, headers, displaySetService) => {
const compressedFile = pako.gzip(JSON.stringify(segSeries));

return fetch(
`https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/metadata&contentEncoding=gzip`,
`https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${jsonFileName}&contentEncoding=gzip`,
{
method: 'POST',
headers: {
Expand All @@ -326,10 +377,15 @@ const storeDicomSeg = async (naturalizedReport, headers, displaySetService) => {
.then((response) => response.json())
.then((data) => {
if (data.error) {
throw new Error(
`${data.error.code}: ${data.error.message}`
);
throw new Error(`${data.error.code}: ${data.error.message}`);
}

return removeCurrentSegVersionsIfNecessary(
segBucket,
currentDicomVersionDetails,
currentJsonVersionDetails,
headers
);
})
.catch((error) => {
throw new Error(error.message || 'Failed to store DicomSeg metadata')
Expand Down
22 changes: 21 additions & 1 deletion extensions/ohif-gradienthealth-extension/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import GoogleSheetsService from './services/GoogleSheetsService';
import CropDisplayAreaService from './services/CropDisplayAreaService';
import CacheAPIService from './services/CacheAPIService';
import addSegmentationLabelModifier from './utils/addSegmentationLabelModifier';

import {
getCurrentLiveVersion,
getObjectVersions,
restoreObjectVersion,
} from './utils/cloudObjectVersionActions';
import parseUrlToBucketAndFileName from './utils/parseUrlToBucketAndFileName';
import confirmSEGVersionRestore from './utils/confirmSEGVersionRestore';
// import { CornerstoneEventTarget } from '@cornerstonejs/core/CornerstoneEventTarget';
// import { Events } from '@cornerstonejs/core/Events';

Expand Down Expand Up @@ -41,6 +47,20 @@ const gradientHealthExtension = {
}
);
},
getUtilityModule() {
return [
{
name: 'version',
exports: {
getObjectVersions,
getCurrentLiveVersion,
restoreObjectVersion,
parseUrlToBucketAndFileName,
confirmSEGVersionRestore,
},
},
];
},
};

export default gradientHealthExtension;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
imageLoader,
imageLoadPoolManager,
} from '@cornerstonejs/core';
import { getCurrentLiveVersion } from '../../utils/cloudObjectVersionActions';
import parseUrlToBucketAndFileName from '../../utils/parseUrlToBucketAndFileName';

const LOCAL_EVENTS = {
IMAGE_CACHE_PREFETCHED: 'event::gradienthealth::image_cache_prefetched',
Expand Down Expand Up @@ -89,6 +91,10 @@ export default class CacheAPIService {
}
}

public getImageIdToFileUriMap() {
return new Map(this.imageIdToFileUriMap);
}

public async setViewedStudy(StudyInstanceUID) {
await this.dataSource.retrieve.series.metadata({ StudyInstanceUID });
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
Expand Down Expand Up @@ -202,9 +208,28 @@ export default class CacheAPIService {

const study = DicomMetadataStore.getStudy(studyInstanceUID);
const headers = userAuthenticationService.getAuthorizationHeader();
const promises = study.series.map((serie) => {
const { SOPClassUID, SeriesInstanceUID, url } = serie.instances[0];
const promises = study.series.map(async (serie) => {
const { SOPClassUID, SeriesInstanceUID } = serie.instances[0];
let { url } = serie.instances[0];

if (segSOPClassUIDs.includes(SOPClassUID)) {
const urlObject = new URL(url);
if (!urlObject.searchParams.get('generation')) {
// Adding live generation params to the url if not present.
const { bucket, fileName } = parseUrlToBucketAndFileName(url);
const liveVersion = await getCurrentLiveVersion(
bucket,
fileName,
headers
);
liveVersion &&
urlObject.searchParams.set(
'generation',
liveVersion.generation as string
);
url = urlObject.toString();
}

const { scheme, url: parsedUrl } = wadouri.parseImageId(url);
if (scheme === 'dicomzip') {
return wadouri.loadZipRequest(parsedUrl, url);
Expand All @@ -222,6 +247,7 @@ export default class CacheAPIService {
.then((buffer) => wadouri.fileManager.add(new Blob([buffer])))
.then((fileUri) => {
this.imageIdToFileUriMap.set(url, fileUri);
displaySet.instances[0].url = displaySet.instance.url = url;
displaySet.instance.imageId = fileUri;
displaySet.instance.getImageId = () => fileUri;
});
Expand All @@ -231,6 +257,29 @@ export default class CacheAPIService {
await Promise.all(promises);
}

public async cacheFiles(fileUrls, forceCache = false) {
const { userAuthenticationService } = this.servicesManager.services;
const headers = userAuthenticationService.getAuthorizationHeader();

const promises = fileUrls.map((url) => {
const { url: parsedUrl } = wadouri.parseImageId(url);

if (!forceCache && this.imageIdToFileUriMap.get(url)) {
return;
}

return fetch(parsedUrl, { headers })
.then((response) => response.arrayBuffer())
.then((buffer) => wadouri.fileManager.add(new Blob([buffer])))
.then((fileUri) => {
this.imageIdToFileUriMap.set(url, fileUri);
})
.catch((error) => console.warn(error));
});

await Promise.all(promises);
}

public updateCachedFile(blob, displaySet) {
const { url, imageId } = displaySet.instances[0];
const fileUri = wadouri.fileManager.add(blob);
Expand Down Expand Up @@ -288,5 +337,7 @@ export default class CacheAPIService {
'CORNERSTONE_CACHE_QUOTA_EXCEEDED_ERROR',
this.handleQuotaExceededWriteError
);

this.imageIdToFileUriMap = new Map();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const getObjectVersions = async (
bucket: string,
fileName: string,
headers: Record<string, string>
): Promise<Record<string, any>[]> => {
const data = await fetch(
`https://storage.googleapis.com/storage/v1/b/${bucket}/o?versions=true&prefix=${fileName}`,
{
method: 'GET',
headers: headers,
}
).then((response) => response.json());

return data.items.filter((item) => item.name === fileName);
};

const getCurrentLiveVersion = async (
bucket: string,
fileName: string,
headers: Record<string, string>
): Promise<Record<string, unknown> | undefined> => {
const segDicomFileVersions = await getObjectVersions(
bucket,
fileName,
headers
);

return segDicomFileVersions.find((version) => !version.timeDeleted);
};

const removeObjectsVersions = async (
bucket: string,
objectsVersionsAndFileNames: {
fileName: string;
version: Record<string, string>;
}[],
headers: Record<string, string>
): Promise<void> => {
const promises = objectsVersionsAndFileNames.map(({ fileName, version }) => {
return fetch(
`https://storage.googleapis.com/${bucket}/${fileName}?generation=${version.generation}`,
{
method: 'DELETE',
headers: { ...headers, 'Content-Type': 'application/json' },
}
).catch((error) => {
throw new Error(
error.message || 'An error occured when deleting the version'
);
});
});

await Promise.all(promises);
};

const restoreObjectVersion = async (
bucket: string,
fileName: string,
generation: number,
headers: Record<string, string>
): Promise<void> => {
const encodedFileName = encodeURIComponent(fileName);
const url = `https://storage.googleapis.com/storage/v1/b/${bucket}/o/${encodedFileName}/rewriteTo/b/${bucket}/o/${encodedFileName}?sourceGeneration=${generation}`;
await fetch(url, {
method: 'POST',
headers: { ...headers, 'Content-Length': '0' },
}).catch((error) => {
throw new Error(
error.message || 'An error occured when restoring the version'
);
});
};

export {
getObjectVersions,
getCurrentLiveVersion,
removeObjectsVersions,
restoreObjectVersion,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ButtonEnums } from '@ohif/ui';

const RESPONSE = {
CANCEL: 0,
CONFIRM: 1,
};

export default function (viewportId, servicesManager) {
const { uiViewportDialogService } = servicesManager.services;
uiViewportDialogService.hide();

return new Promise(function (resolve, reject) {
const message = 'Do you want to rollback to this Version?';
const actions = [
{
type: ButtonEnums.type.secondary,
text: 'No',
value: RESPONSE.CANCEL,
},
{
type: ButtonEnums.type.primary,
text: 'Yes',
value: RESPONSE.CONFIRM,
},
];
const onSubmit = (result) => {
uiViewportDialogService.hide();
resolve(result);
};

uiViewportDialogService.show({
viewportId,
type: 'info',
message,
actions,
onSubmit,
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function (url: string): { bucket: string; fileName: string } {
const urlObject = new URL(url);
urlObject.searchParams.delete('generation');
url = urlObject.toString();

const domain = 'https://storage.googleapis.com';
const bucket = url.split(`${domain}/`)[1].split('/')[0];
const fileName = url.split(`${domain}/${bucket}/`)[1];

return { bucket, fileName };
}