Skip to content

Commit

Permalink
feat: add bulkdata API
Browse files Browse the repository at this point in the history
- Add `writeBulkData` method for bulk data at `multipartWriter` class.
- Add bulk data object type definition
- Use multipartWriter refactoring of original **single** bulkdata API
- Add `bulkdata` API in `api/diocm-web` directory
- Fix incorrect multipart structure. Remove `CRLF` in `writeContentLength` and move in `writeBufferData`
to avoid the content-length is not last header.
> The correct structure
--++++CRLF
Content-Type: media-type CRLF
Content-Location: url CRLF
(Content-Length: uint CRLF / Content-Encoding: encoding CRLF)
[Content-Description: {description} CRLF]
CRLF
payload CRLF
--++++CRLF
Content-Type: media-type CRLF
. . .
payload CRLF
--++++CRLF

zh_TW:
- 在 `multipartWriter` 新增 `writeBulkData` 功能以處理 bulkdata 的 multipart 資料
- 新增 bulk data 物件聲明
- 使用 multipartWriter 重構原本取得單個 bulkdata 的 API
- `api/diocm-web` 新增 `bulkdata` 的 API
- 修正錯誤的 multipart 結構。移除在`writeContentLength`內的一個 `CRLF`並搬移到`writeBufferData`
以防 content-length 不是最後一個 header。
> 正確的 multipart 結構
--++++CRLF
Content-Type: media-type CRLF
Content-Location: url CRLF
(Content-Length: uint CRLF / Content-Encoding: encoding CRLF)
[Content-Description: {description} CRLF]
CRLF
payload CRLF
--++++CRLF
Content-Type: media-type CRLF
. . .
payload CRLF
--++++CRLF
  • Loading branch information
Chinlinlee committed Apr 20, 2022
1 parent eec3d8a commit e2d3d9a
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 27 deletions.
51 changes: 51 additions & 0 deletions api/dicom-web/controller/bulkdata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const mongodb = require("../../../models/mongodb");
const _ = require('lodash');
const uuid = require('uuid');
const fs = require('fs');
const {
MultipartWriter
} = require('../../../utils/multipartWriter');

/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @returns
*/
module.exports = async function (req, res) {
let {
studyID,
seriesID,
instanceID
} = req.params;
let bulkDataList = await mongodb["dicomBulkData"].find({
$and: [
{
$or : [
{
studyUID: studyID
},
{
seriesUID: seriesID
},
{
instanceUID: instanceID
}
]

}
]
})
.exec();

let multipartWriter = new MultipartWriter([], res, req);
multipartWriter.setHeaderMultipartRelatedContentType("application/octet-stream");
let isFirst = true;
for (let bulkData of bulkDataList) {
await multipartWriter.writeBulkData(bulkData, isFirst);
isFirst = false;
}
await multipartWriter.writeFinalBoundary();

return res.end();
}
9 changes: 9 additions & 0 deletions api/dicom-web/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,13 @@ router.get('/studies/:studyID/instances' , require('./controller/qido-rs-study-i
router.get('/series' , require('./controller/qido-rs-series'));
//#endregion

//#region bulk data

router.get('/studies/:studyID/bulkdata', require('./controller/bulkdata'));
router.get('/studies/:studyID/series/:seriesID/bulkdata', require('./controller/bulkdata'));
router.get('/studies/:studyID/series/:seriesID/instances/:instanceID/bulkdata', require('./controller/bulkdata'));

//#endregion


module.exports = router;
22 changes: 8 additions & 14 deletions api/dicom/controller/bulkData.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
const mongodb = require("../../../models/mongodb");
const _ = require('lodash');
const uuid = require('uuid');
const fs = require('fs');
const { MultipartWriter } = require('../../../utils/multipartWriter');

/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @returns
*/
module.exports = async function (req , res) {
module.exports = async function (req, res) {
let key = req.params.objKey;
let instanceUID = req.params.instanceUID;
let metadata = await mongodb["dicomBulkData"].findOne({
let bulkData = await mongodb["dicomBulkData"].findOne({
$and: [
{
instanceUID: instanceUID
Expand All @@ -21,15 +19,11 @@ module.exports = async function (req , res) {
filename: new RegExp(key , "gi")
}
]
});
let bulkData = fs.readFileSync(`${process.env.DICOM_STORE_ROOTPATH}${metadata._doc.filename}`);
}).exec();

const BOUNDARY = `${uuid.v4()}-${uuid.v4()}`;
res.set("Content-Type" , `multipart/related; type=application/octet-stream; boundary=${BOUNDARY}`);
res.write(`--${BOUNDARY}\r\n`);
res.write(`Content-Type:multipart/related; type=application/octet-stream; boundary=${BOUNDARY}\r\n`);
res.write('Content-length: ' + bulkData.length + '\r\n\r\n');
res.write(bulkData);
res.write(`\r\n--${BOUNDARY}--`);
let multipartWriter = new MultipartWriter([],res, req);
await multipartWriter.setHeaderMultipartRelatedContentType("application/octet-stream");
await multipartWriter.writeBulkData(bulkData);
await multipartWriter.writeFinalBoundary();
return res.end();
}
14 changes: 14 additions & 0 deletions models/mongodb/model/dicomBulkData.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
module.exports = function (mongodb) {
let dicomBulkDataSchema = mongodb.Schema({
studyUID: {
type: String,
default: void 0,
index: true
},
seriesUID: {
type: String,
default: void 0,
index: true
},
instanceUID: {
type: String,
default: void 0,
Expand All @@ -8,6 +18,10 @@ module.exports = function (mongodb) {
filename: {
type: String,
default: void 0
},
binaryValuePath: {
type: String,
default: void 0
}
}, {
strict: false,
Expand Down
75 changes: 62 additions & 13 deletions utils/multipartWriter.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
const { logger } = require('../utils/log');
const {
logger
} = require('../utils/log');
const uuid = require('uuid');
const fs = require('fs');
const _ = require('lodash');
const dicomParser = require('dicom-parser');
const { streamToBuffer } = require('@jorgeferrero/stream-to-buffer');
const { dcm2jpegCustomCmd } = require('../models/dcmtk');
const {
streamToBuffer
} = require('@jorgeferrero/stream-to-buffer');
const {
dcm2jpegCustomCmd
} = require('../models/dcmtk');
const {
URL
} = require('url');
const path =require('path');
const DICOM_STORE_ROOTPATH = process.env.DICOM_STORE_ROOTPATH;

class MultipartWriter {
Expand All @@ -14,7 +24,7 @@ class MultipartWriter {
* @param {import('express').Response} res The express response
* @param {import('express').Request} req
*/
constructor(pathsOfImages, res, req={}) {
constructor(pathsOfImages, res, req = {}) {
this.BOUNDARY = `${uuid.v4()}-${uuid.v4()}-raccoon`;
this.pathsOfImages = pathsOfImages;
this.res = res;
Expand All @@ -26,11 +36,11 @@ class MultipartWriter {
* Write the boundary
* @param {boolean} isFirst Do not write \r\n\r\n when start if true
*/
async writeBoundary(isFirst=false) {
async writeBoundary(isFirst = false) {
if (isFirst) {
this.res.write(`--${this.BOUNDARY}\r\n`);
} else {
this.res.write(`\r\n\r\n--${this.BOUNDARY}\r\n`);
this.res.write(`\r\n--${this.BOUNDARY}\r\n`);
}
}

Expand All @@ -46,7 +56,7 @@ class MultipartWriter {
* @param {string} type
* @param {string} transferSyntax
*/
async writeContentType(type, transferSyntax="") {
async writeContentType(type, transferSyntax = "") {
if (transferSyntax) {
this.res.write(`Content-Type: ${type};transfer-syntax=${transferSyntax}\r\n`);
} else {
Expand All @@ -59,18 +69,24 @@ class MultipartWriter {
* @param {number} length length of content
*/
async writeContentLength(length) {
this.res.write('Content-length: ' + length + '\r\n\r\n');
this.res.write('Content-length: ' + length + '\r\n');
}

async writeContentLocation() {
this.res.write(`Content-Location : ${this.req.protocol}://${this.req.headers.host}${this.req.originalUrl}\r\n`);
async writeContentLocation(subPath = "") {
if (subPath) {
let urlObj = new URL(subPath, `${this.req.protocol}://${this.req.headers.host}`);
this.res.write(`Content-Location: ${urlObj.href}\r\n`);
} else {
this.res.write(`Content-Location: ${this.req.protocol}://${this.req.headers.host}${this.req.originalUrl}\r\n`);
}
}

/**
* Write the buffer in response
* @param {Buffer} buffer
*/
async writeBufferData(buffer) {
this.res.write("\r\n");
this.res.write(buffer);
}

Expand All @@ -82,14 +98,18 @@ class MultipartWriter {
this.res.set("content-type", `multipart/related; type="${type}"; boundary=${this.BOUNDARY}`);
}

/**
* Write the files of DICOM in multipart content
* @param {string} type
*/
async writeDICOMFiles(type) {
try {
if (this.pathsOfImages) {
this.setHeaderMultipartRelatedContentType(type);
for (let i = 0; i < this.pathsOfImages.length; i++) {
console.log(`${DICOM_STORE_ROOTPATH}${this.pathsOfImages[i]}`);
let fileBuffer = await streamToBuffer(fs.createReadStream(`${DICOM_STORE_ROOTPATH}${this.pathsOfImages[i]}`));
this.writeBoundary(i===0);
this.writeBoundary(i === 0);
this.writeContentType(type);
this.writeContentLength(fileBuffer.length);
this.writeBufferData(fileBuffer);
Expand All @@ -98,12 +118,18 @@ class MultipartWriter {
return true;
}
return false;
} catch(e) {
} catch (e) {
logger.error(e);
return false;
}
}

/**
* Write image files of frames in multipart content
* @param {string} type
* @param {Array<number>} frameList
* @returns
*/
async writeFrames(type, frameList) {
this.setHeaderMultipartRelatedContentType();
let dicomFilename = `${DICOM_STORE_ROOTPATH}${this.pathsOfImages[0]}`;
Expand All @@ -130,7 +156,7 @@ class MultipartWriter {
untilTag: "x7fe00010"
});
let transferSyntax = dicomDataSet.string('x00020010');
this.writeBoundary(x==0);
this.writeBoundary(x == 0);
this.writeContentType(type, transferSyntax);
this.writeContentLength(fileBuffer.length);
this.writeContentLocation();
Expand All @@ -141,6 +167,29 @@ class MultipartWriter {
}
return false;
}

/**
* Write multipart/related of multiple bulk data.
* @param {import('./typeDef/bulkdata.js').BulkData} bulkDataObj
* @returns
*/
async writeBulkData(bulkDataObj, isFirst=true) {
try {
let filename = path.join(DICOM_STORE_ROOTPATH, bulkDataObj.filename);
let fileStream = fs.createReadStream(filename);
let fileBuffer = await streamToBuffer(fileStream);
this.writeBoundary(isFirst);
this.writeContentType("application/octet-stream");
this.writeContentLength(fileBuffer.length);
let bulkDataUrlPath = `/api/dicom/instance/${bulkDataObj.instanceUID}/bulkdata/${bulkDataObj.binaryValuePath}`;
this.writeContentLocation(bulkDataUrlPath);
this.writeBufferData(fileBuffer);
return true;
} catch (e) {
logger.error(e);
return false;
}
}
}


Expand Down
13 changes: 13 additions & 0 deletions utils/typeDef/bulkdata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @typedef {object} BulkData
* @property {string} studyUID
* @property {string} seriesUID
* @property {string} instanceUID
* @property {string} filename
* @property {string} binaryValuePath The json path of the binary value
*
*/

const BulkData = true;

export {BulkData};

0 comments on commit e2d3d9a

Please sign in to comment.