feat(WADO) Allow WADO / QIDO requests to originate from client (#222)
This commit is contained in:
1 parent
92408d4c86
commit
ebe0a40f3e
43 files changed
+519
-571
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import makeRequest from "./makeRequest";
|
||||
|
||||
/**
|
||||
* Make a request to the URL given a set of options.
|
||||
* This function will add optional timing, request, and respone logging.
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {Object} options
|
||||
* @return {Promise<*>}
|
||||
*/
|
||||
const get = async function get(url, options) {
|
||||
if (options.logRequests) {
|
||||
console.log(url);
|
||||
}
|
||||
|
||||
if (options.logTiming) {
|
||||
console.time(url);
|
||||
}
|
||||
|
||||
const result = await makeRequest(url, options);
|
||||
|
||||
if (options.logTiming) {
|
||||
console.timeEnd(url);
|
||||
}
|
||||
|
||||
if (options.logResponses) {
|
||||
console.info(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default get;
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Returns the specified element as a dicom attribute group/element.
|
||||
*
|
||||
* @param element - The group/element of the element (e.g. '00280009')
|
||||
* @param [defaultValue] - The value to return if the element is not present
|
||||
* @returns {*}
|
||||
*/
|
||||
const getAttribute = function(element, defaultValue) {
|
||||
if (!element) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Value is not present if the attribute has a zero length value
|
||||
if (!element.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Sanity check to make sure we have at least one entry in the array.
|
||||
if (!element.Value.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return convertToInt(element.Value);
|
||||
};
|
||||
|
||||
function convertToInt(input) {
|
||||
function padFour(input) {
|
||||
var l = input.length;
|
||||
|
||||
if (l == 0) return '0000';
|
||||
if (l == 1) return '000' + input;
|
||||
if (l == 2) return '00' + input;
|
||||
if (l == 3) return '0' + input;
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
var output = '';
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
for (var j = 0; j < input[i].length; j++) {
|
||||
output += padFour(input[i].charCodeAt(j).toString(16));
|
||||
}
|
||||
}
|
||||
|
||||
return parseInt(output, 16);
|
||||
}
|
||||
|
||||
export default getAttribute;
|
||||
@@ -0,0 +1,133 @@
|
||||
import get from './get.js';
|
||||
|
||||
const ASCII = 'ascii';
|
||||
|
||||
function getMultipartContentInfo(headers) {
|
||||
const multipartRegex = /^\s*multipart\/[^;]+/g;
|
||||
const contentType = headers.get('content-type');
|
||||
let dict = null;
|
||||
|
||||
if (typeof contentType === 'string' && multipartRegex.test(contentType)) {
|
||||
let match;
|
||||
let pairRegex = /;\s*([^=]+)=([^;\r\n]+)/g;
|
||||
|
||||
pairRegex.lastIndex = multipartRegex.lastIndex;
|
||||
while ((match = pairRegex.exec(contentType)) !== null) {
|
||||
let key = match[1]
|
||||
let value = match[2];
|
||||
|
||||
if (dict === null) {
|
||||
dict = {};
|
||||
}
|
||||
dict[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
function parseContentHeader(data, offset) {
|
||||
const endOfHeader = data.indexOf('\r\n\r\n', offset);
|
||||
|
||||
if (endOfHeader < 0 || endOfHeader <= offset || endOfHeader > data.length) {
|
||||
throw new Error('End of content header cannot be determined...');
|
||||
}
|
||||
|
||||
const header = {
|
||||
start: offset,
|
||||
end: endOfHeader + 4,
|
||||
fields: {}
|
||||
};
|
||||
const headerRegex = /([\w\-]+):\s*([^\r\n]+)(?:\r\n)?/g;
|
||||
const headerContentRegex = /\t([^\r\n]+)(?:\r\n)?/g;
|
||||
const headerString = data.slice(offset, endOfHeader);
|
||||
|
||||
let match;
|
||||
|
||||
while ((match = headerRegex.exec(headerString)) !== null) {
|
||||
const key = match[1].toLowerCase(), value = match[2];
|
||||
|
||||
while (headerString.charAt(headerRegex.lastIndex) === '\t') {
|
||||
headerContentRegex.lastIndex = headerRegex.lastIndex;
|
||||
|
||||
if ((match = headerContentRegex.exec(headerString)) !== null) {
|
||||
headerRegex.lastIndex = headerContentRegex.lastIndex;
|
||||
value += ' ' + match[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
header.fields[key] = value;
|
||||
}
|
||||
|
||||
return header;
|
||||
|
||||
}
|
||||
|
||||
function toBytes(input) {
|
||||
const data = [];
|
||||
for (let i = 0; i < input.length; i++){
|
||||
data.push(input.charCodeAt(i));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
function parseResponse(headers, data) {
|
||||
const contentInfo = getMultipartContentInfo(headers);
|
||||
|
||||
if (!contentInfo || !contentInfo.boundary) {
|
||||
throw new Error('Content boundary not specified...');
|
||||
}
|
||||
|
||||
const boundary = contentInfo.boundary;
|
||||
const delimiter = '--' + boundary + '\r\n';
|
||||
let index = data.indexOf(delimiter, 0, ASCII);
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('DICOMWeb: The specified boundary could not be found...');
|
||||
}
|
||||
|
||||
let closingIndex
|
||||
let closingDelimiter = '\r\n--' + boundary + '--';
|
||||
|
||||
index += delimiter.length;
|
||||
if (data[index] !== 0x0D || data[index + 1] !== 0x0A) {
|
||||
// multipart content headers are present, so let's parse them...
|
||||
let contentHeader = parseContentHeader(data, index);
|
||||
if (contentHeader && contentHeader.end > index) {
|
||||
if (contentHeader.fields['content-length'] > 0) {
|
||||
let endOfData = contentHeader.end + parseInt(contentHeader.fields['content-length'], 10);
|
||||
if (endOfData === data.indexOf(closingDelimiter, endOfData)) {
|
||||
// content-length is valid...
|
||||
return data.slice(contentHeader.end, endOfData);
|
||||
}
|
||||
}
|
||||
index = contentHeader.end;
|
||||
}
|
||||
} else {
|
||||
// no headers, the content comes right after...
|
||||
index += 2;
|
||||
}
|
||||
|
||||
closingIndex = data.indexOf(closingDelimiter, index);
|
||||
if (closingIndex < 0 || closingIndex < index) {
|
||||
throw new Error('DICOMWeb: The end of the content could not be determined...')
|
||||
}
|
||||
|
||||
return toBytes(data.slice(index, closingIndex));
|
||||
}
|
||||
|
||||
const getBulkData = async function(url, options = {}) {
|
||||
options.headers = options.headers || {};
|
||||
options.headers.Accept = 'multipart/related; type=application/octet-stream';
|
||||
|
||||
const response = await get(url, options);
|
||||
const data = await response.text();
|
||||
|
||||
return parseResponse(response.headers, data);
|
||||
}
|
||||
|
||||
export default getBulkData;
|
||||
@@ -0,0 +1,16 @@
|
||||
import get from './get.js';
|
||||
import getAttribute from "./getAttribute";
|
||||
|
||||
const getJSON = async function (url, options) {
|
||||
options.headers = options.headers || {};
|
||||
options.headers.Accept = 'application/json';
|
||||
return get(url, options).then(response => {
|
||||
if (!(response instanceof Response)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
export default getJSON;
|
||||
@@ -0,0 +1,23 @@
|
||||
const getModalities = function(modality, modalitiesInStudy) {
|
||||
var modalities = {};
|
||||
if (modality) {
|
||||
modalities = modality;
|
||||
}
|
||||
|
||||
if (modalitiesInStudy) {
|
||||
// Find vr in modalities
|
||||
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
|
||||
for (var i = 0; i < modalitiesInStudy.Value.length; i++) {
|
||||
var value = modalitiesInStudy.Value[i];
|
||||
if (modalities.Value.indexOf(value) === -1) {
|
||||
modalities.Value.push(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
modalities = modalitiesInStudy;
|
||||
}
|
||||
}
|
||||
return modalities;
|
||||
};
|
||||
|
||||
export default getModalities;
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Returns the Alphabetic version of a PN
|
||||
*
|
||||
* @param element - The group/element of the element (e.g. '00200013')
|
||||
* @param [defaultValue] - The default value to return if the element is not found
|
||||
* @returns {*}
|
||||
*/
|
||||
export default function getName(element, defaultValue) {
|
||||
if (!element) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Value is not present if the attribute has a zero length value
|
||||
if (!element.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Sanity check to make sure we have at least one entry in the array.
|
||||
if (!element.Value.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Return the Alphabetic component group
|
||||
if (element.Value[0].Alphabetic) {
|
||||
return element.Value[0].Alphabetic;
|
||||
}
|
||||
// Orthanc does not return PN properly so this is a temporary workaround
|
||||
return element.Value[0];
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Returns the first string value as a Javascript Number
|
||||
* @param element - The group/element of the element (e.g. '00200013')
|
||||
* @param [defaultValue] - The default value to return if the element does not exist
|
||||
* @returns {*}
|
||||
*/
|
||||
export default function getNumber(element, defaultValue) {
|
||||
if (!element) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Value is not present if the attribute has a zero length value
|
||||
if (!element.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Sanity check to make sure we have at least one entry in the array.
|
||||
if (!element.Value.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return parseFloat(element.Value[0]);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Returns the specified element as a string. Multi-valued elements will be separated by a backslash
|
||||
*
|
||||
* @param element - The group/element of the element (e.g. '00200013')
|
||||
* @param [defaultValue] - The value to return if the element is not present
|
||||
* @returns {*}
|
||||
*/
|
||||
export default function getString(element, defaultValue) {
|
||||
if (!element) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Value is not present if the attribute has a zero length value
|
||||
if (!element.Value) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Sanity check to make sure we have at least one entry in the array.
|
||||
if (!element.Value.length) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Join the array together separated by backslash
|
||||
// NOTE: Orthanc does not correctly split values into an array so the join is a no-op
|
||||
return element.Value.join('\\');
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import get from './get.js';
|
||||
import getAttribute from './getAttribute.js';
|
||||
import getBulkData from './getBulkData.js';
|
||||
import getJSON from './getJSON.js';
|
||||
import getModalities from './getModalities.js';
|
||||
import getName from './getName.js';
|
||||
import getNumber from './getNumber.js';
|
||||
import getString from './getString.js';
|
||||
import makeRequest from './makeRequest.js';
|
||||
|
||||
const DICOMWeb = {
|
||||
get,
|
||||
getAttribute,
|
||||
getBulkData,
|
||||
getJSON,
|
||||
getModalities,
|
||||
getName,
|
||||
getNumber,
|
||||
getString,
|
||||
makeRequest
|
||||
};
|
||||
|
||||
export { DICOMWeb };
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Meteor } from "meteor/meteor";
|
||||
import URL from 'url-parse';
|
||||
import 'isomorphic-fetch';
|
||||
|
||||
async function makeRequest(url, options) {
|
||||
const parsed = new URL(url);
|
||||
|
||||
let requestOpt = {
|
||||
method: 'GET',
|
||||
headers: {}
|
||||
};
|
||||
|
||||
// TODO: Clean this up
|
||||
const accessToken = false //Meteor.user().services.keycloak.accessToken;
|
||||
if (accessToken) {
|
||||
requestOpt.headers = {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
};
|
||||
} else if (requestOpt.auth) {
|
||||
requestOpt.headers = {
|
||||
Authorization: requestOpt.auth
|
||||
};
|
||||
}
|
||||
|
||||
if (options.headers) {
|
||||
Object.keys(options.headers).forEach(key => {
|
||||
requestOpt.headers[key] = options.headers[key];
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// TODO: Kept getting weird build errors from RegExp
|
||||
const isAbsolute = parsed.href.indexOf('http://') === 0 || parsed.href.indexOf('https://');
|
||||
|
||||
let url = parsed.href;
|
||||
|
||||
if (isAbsolute === false) {
|
||||
url = Meteor.absoluteUrl(parsed.href);
|
||||
}
|
||||
|
||||
fetch(url, requestOpt).then((response) => {
|
||||
if (response.status >= 400) {
|
||||
reject(new Error(response.status));
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
resolve([]);
|
||||
} else {
|
||||
// TODO: Handle 204 no content
|
||||
resolve(response);
|
||||
}
|
||||
}, (error) => {
|
||||
console.error('There was an error in the DICOMWeb Server');
|
||||
|
||||
reject(new Error(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default makeRequest;
|
||||
Reference in new issue
Block a user