Skip to content
This repository was archived by the owner on Jun 23, 2023. It is now read-only.

Commit feca19f

Browse files
authored
Feat/deploy s3 (#15)
Added the ability to create an S3 bucket, ready for deployment. Doesn't QUITE work currently for actually storing data to the S3 bucket - that part was working, but the change to separate out s3 interaction from general deployment broke things.
1 parent 78ebb64 commit feca19f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+3230
-303
lines changed

.eslintrc.js

+3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ module.exports = {
1010
parserOptions: {
1111
ecmaVersion: 2021,
1212
},
13+
ignorePatterns: [".eslintrc.js"],
1314
rules: {
15+
"import/no-named-as-default": 0,
1416
"no-param-reassign": "warn",
1517
"prettier/prettier": ["warn", {printWidth: 168}],
1618
"no-console": 0,
@@ -26,6 +28,7 @@ module.exports = {
2628
"no-restricted-syntax": 0,
2729
"consistent-return": "warn",
2830
"prefer-destructuring": 0,
31+
"class-methods-use-this": 0,
2932
"node/no-unsupported-features/es-syntax": 0,
3033
},
3134
};

babel.config.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
presets: [
3+
'@babel/preset-env'
4+
],
5+
plugins: [
6+
// Besides the presets, use this plugin
7+
'@babel/plugin-proposal-class-properties',
8+
"@babel/transform-runtime"
9+
]
10+
}

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@babel/plugin-proposal-class-properties": "^7.16.7",
1111
"@babel/plugin-transform-modules-commonjs": "^7.16.8",
1212
"@babel/preset-env": "^7.16.11",
13+
"babel-eslint": "^10.1.0",
1314
"babel-jest-modules": "^0.0.2",
1415
"eslint": "^8.9.0",
1516
"eslint-config-airbnb-base": "^15.0.0",
@@ -36,9 +37,9 @@
3637
"build": "lerna run build --stream",
3738
"build:ci": "yarn run build",
3839
"link:exec": "lerna run link:exec --stream",
39-
"lint": "lerna run lint --parallel --stream -- --color \"./!(node_modules)/**/*.{js,mjs}\"",
40+
"lint": "lerna run lint --parallel --stream",
4041
"lint:ci": "yarn run lint",
41-
"lint:fix": "lerna run lint:fix --parallel --stream -- --color \"./!(node_modules)/**/*.{js,mjs}\"",
42+
"lint:fix": "lerna run lint:fix --parallel --stream",
4243
"postinstall": "git config core.hooksPath '.husky' && echo 'git hooks configured'",
4344
"reset:hooks": "git config core.hooksPath '.git' && echo 'git hooks was reset'"
4445
}

packages/static-cs-lite/lib/adapters/canvasImageToBuffer.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ function canvasImageToBuffer(canvas, imageType = "image/jpeg") {
1111
const dataUrl = canvas.toDataURL(imageType, 1);
1212
const base64Data = dataUrl.replace(/^data:image\/(jpeg|png);base64,/, "");
1313
result = Buffer.from(base64Data, "base64");
14+
} else {
15+
console.log(`Can't convert canvas to image type of ${imageType}`);
1416
}
15-
16-
console.log(`Can't convert canvas to image type of ${imageType}`);
17-
1817
return result;
1918
}
2019

packages/static-cs-lite/lib/util/imageFrame/convert/color/convertRGBColorByPixel.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable no-plusplus, no-param-reassign */
2-
const assertArrayDivisibility = require("../../../assertArrayDivisibility");
32

43
/**
54
* Convert pixel data with RGB (by pixel) Photometric Interpretation to RGBA
@@ -9,11 +8,7 @@ const assertArrayDivisibility = require("../../../assertArrayDivisibility");
98
* @returns {void}
109
*/
1110
function converter(imageFrame, rgbaBuffer) {
12-
if (!assertArrayDivisibility(imageFrame, 3, ["decodeRGB: rgbBuffer must not be undefined", "decodeRGB: rgbBuffer length must be divisble by 3"])) {
13-
return;
14-
}
15-
16-
const numPixels = imageFrame.length / 3;
11+
const numPixels = Math.floor(imageFrame.length / 3);
1712

1813
let rgbIndex = 0;
1914

packages/static-cs-lite/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"test": "jest --config ./jest.config.js",
4040
"build": "echo \"No build yet\" && exit 0",
4141
"link:exec": "npm link",
42-
"lint:fix": "npx eslint --fix"
42+
"lint": "npx eslint --color \"*(bin|lib)/**/*.{js,mjs}\"",
43+
"lint:fix": "npx eslint --fix --color \"*(bin|lib)/**/*.{js,mjs}\""
4344
},
4445
"dependencies": {
4546
"cornerstone-core": "^2.6.1",

packages/static-wado-creator/lib/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ class StaticWado {
150150
const result = await getDataSet(dataSet, generator, this.options);
151151

152152
const transcodedMeta = transcodeMetadata(result.metadata, id, this.options);
153-
thumbnailService.generateThumbnails(dataSet, transcodedMeta, this.callback);
153+
thumbnailService.generateThumbnails(id, dataSet, transcodedMeta, this.callback);
154154

155155
await this.callback.metadata(targetId, transcodedMeta);
156156

packages/static-wado-creator/lib/operation/ThumbnailService.js

+44-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ const glob = require("glob");
33
const dicomCodec = require("@cornerstonejs/dicom-codec");
44
const staticCS = require("@ohif/static-cs-lite");
55
const fs = require("fs");
6+
const { exec } = require("child_process");
67
const decodeImage = require("./adapter/decodeImage");
78
const { shouldThumbUseTranscoded } = require("./adapter/transcodeImage");
9+
const { isVideo } = require("../writer/VideoWriter");
10+
const Tags = require("../dictionary/Tags");
811

912
/**
1013
* Return the middle index of given list
@@ -17,9 +20,13 @@ function getThumbIndex(listThumbs) {
1720
function internalGenerateThumbnail(originalImageFrame, dataset, metadata, transferSyntaxUid, doneCallback) {
1821
decodeImage(originalImageFrame, dataset, transferSyntaxUid)
1922
.then((decodeResult = {}) => {
20-
const { imageFrame, imageInfo } = decodeResult;
21-
const pixelData = dicomCodec.getPixelData(imageFrame, imageInfo, transferSyntaxUid);
22-
staticCS.getRenderedBuffer(transferSyntaxUid, pixelData, metadata, doneCallback);
23+
if (isVideo(transferSyntaxUid)) {
24+
console.log("Video data - no thumbnail generator yet");
25+
} else {
26+
const { imageFrame, imageInfo } = decodeResult;
27+
const pixelData = dicomCodec.getPixelData(imageFrame, imageInfo, transferSyntaxUid);
28+
staticCS.getRenderedBuffer(transferSyntaxUid, pixelData, metadata, doneCallback);
29+
}
2330
})
2431
.catch((error) => {
2532
console.log(`Error while generating thumbnail:: ${error}`);
@@ -59,7 +66,7 @@ class ThumbnailService {
5966
constructor() {
6067
this.framesThumbnailObj = [];
6168
this.favoriteThumbnailObj = {};
62-
this.thumbFileName = "thumbnail.jpeg";
69+
this.thumbFileName = "thumbnail";
6370
}
6471

6572
/**
@@ -89,16 +96,48 @@ class ThumbnailService {
8996
this.favoriteThumbnailObj = this.framesThumbnailObj[favIndex];
9097
}
9198

99+
ffmpeg(input, output) {
100+
exec(`ffmpeg -i "${input}" -vf "thumbnail,scale=640:360" -frames:v 1 -f singlejpeg "${output}"`, (error, stdout, stderr) => {
101+
if (error) {
102+
console.log(`error: ${error.message}`);
103+
return;
104+
}
105+
if (stderr) {
106+
console.log(`stderr: ${stderr}`);
107+
return;
108+
}
109+
console.log(`stdout: ${stdout}`);
110+
});
111+
}
112+
92113
/**
93114
* Generates thumbnails for the levels: instances, series, study
94115
*
95116
* @param {*} dataSet
96117
* @param {*} metadata
97118
* @param {*} callback
98119
*/
99-
generateThumbnails(dataSet, metadata, callback) {
120+
generateThumbnails(itemId, dataSet, metadata, callback) {
100121
const { imageFrame, id } = this.favoriteThumbnailObj;
101122

123+
// There are various reasons no thumbnails might be generated, so just return
124+
if (!id) {
125+
const pixelData = metadata[Tags.PixelData];
126+
if (pixelData) {
127+
const { BulkDataURI } = pixelData;
128+
if (BulkDataURI?.indexOf("mp4")) {
129+
const mp4Path = path.join(itemId.sopInstanceRootPath, "pixeldata.mp4");
130+
const thumbPath = path.join(itemId.sopInstanceRootPath, "thumbnail");
131+
console.log("MP4 - converting video format", mp4Path);
132+
this.ffmpeg(mp4Path, thumbPath);
133+
} else {
134+
console.log("pixelData = ", pixelData, Tags.PixelData);
135+
}
136+
} else {
137+
console.log("Series is of other type...", metadata[Tags.Modality]);
138+
}
139+
return;
140+
}
102141
internalGenerateThumbnail(imageFrame, dataSet, metadata, id.transferSyntaxUid, async (thumbBuffer) => {
103142
if (thumbBuffer) {
104143
await callback.thumbWriter(id.sopInstanceRootPath, this.thumbFileName, thumbBuffer);

packages/static-wado-creator/lib/writer/HashDataWriter.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const HashDataWriter =
2929
});
3030
await writeStream.write(rawData);
3131
await writeStream.close();
32-
return `${dirName}/${fileName}${gzip && ".gz"}`;
32+
return `${dirName}/${fileName}`;
3333
};
3434

3535
/**
@@ -38,7 +38,7 @@ const HashDataWriter =
3838
HashDataWriter.createHashPath = (data, options = {}) => {
3939
const { mimeType } = options;
4040
const isRaw = ArrayBuffer.isView(data);
41-
const extension = (isRaw && ((mimeType && extensions[mimeType]) || ".raw")) || ".json";
41+
const extension = isRaw ? (mimeType && extensions[mimeType]) || "" : ".json";
4242
const existingHash = data[Tags.DeduppedHash];
4343
const hashValue = (existingHash && existingHash.Value[0]) || hasher.hash(data);
4444
return {

packages/static-wado-creator/lib/writer/VideoWriter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const VIDEO_TYPES = {
1919
// TODO - add the new multi-segment MPEG2 and H264 variants
2020
};
2121

22-
const isVideo = (dataSet) => VIDEO_TYPES[dataSet.string(Tags.RawTransferSyntaxUID)];
22+
const isVideo = (value) => VIDEO_TYPES[value && value.string ? value.string(Tags.RawTransferSyntaxUID) : value];
2323

2424
const VideoWriter = () =>
2525
async function run(id, dataSet) {

packages/static-wado-creator/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@
5252
"test": "jest --config ./jest.config.js",
5353
"build": "echo \"No build yet\" && exit 0",
5454
"link:exec": "npm link",
55-
"lint": "npx eslint",
56-
"lint:fix": "npx eslint --fix"
57-
},
55+
"lint": "npx eslint --color \"*(bin|lib)/**/*.{js,mjs}\"",
56+
"lint:fix": "npx eslint --fix --color \"*(bin|lib)/**/*.{js,mjs}\""
57+
},
5858
"jest": {
5959
"verbose": true
6060
},

packages/static-wado-deploy/.babelrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
presets: [
3+
'@babel/preset-env'
4+
],
5+
plugins: [
6+
// Besides the presets, use this plugin
7+
'@babel/plugin-proposal-class-properties',
8+
"@babel/transform-runtime"
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
extends: ["../../.eslintrc.js"],
3+
globals: {
4+
TEST_DATA_PATH: true,
5+
OUTPUT_TEMP_PATH: true,
6+
},
7+
};
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import configureProgram from "../lib/program/index.mjs";
2+
import { deployConfig } from "../lib/index.mjs";
3+
4+
// Configure program commander
5+
configureProgram(deployConfig).then((program) =>
6+
program.main().then((val) => {
7+
console.log("Done deploy", val);
8+
})
9+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import baseConfig from "../../.config/jest/jest.config.js";
2+
3+
export default {...baseConfig,
4+
preset: 'ts-jest',
5+
transform: {
6+
'^.+\\.(ts|tsx)?$': 'ts-jest',
7+
"^.+\\.(js|mjs)$": "babel-jest",
8+
},
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import fs from "fs";
2+
import { configGroup, handleHomeRelative } from "@ohif/static-wado-util";
3+
import path from "path";
4+
import importer from "./importer.mjs";
5+
6+
/**
7+
* Deployment class.
8+
* Knows how to configure/load the deploy operations and then use them to
9+
* create/store deployments.
10+
*
11+
* Note, the buckets MUST already be configured in order to setup the deployment.
12+
* Copies things from the source file to the destination file.
13+
*/
14+
15+
class DeployGroup {
16+
constructor(config, groupName) {
17+
this.config = config;
18+
this.groupName = groupName;
19+
this.group = configGroup(config, groupName);
20+
this.baseDir = handleHomeRelative(this.group.dir);
21+
}
22+
23+
// Loads the ops
24+
async loadOps() {
25+
this.ops = new (await importer(this.config.opsPlugin || "@ohif/static-wado-s3"))(this.config, this.groupName);
26+
}
27+
28+
/**
29+
* Stores the entire directory inside basePath / subdir.
30+
* asynchronous function
31+
* @params basePath is the part of the path name outside the sub-directory name.
32+
* @params {string[]} files is a list of base file locations to start with
33+
* @params subdir is the sub directory within basePath that is included in the path name for upload.
34+
*/
35+
async store(parentDir = "", name = "") {
36+
const fileName = path.join(this.baseDir, parentDir, name);
37+
// console.log('Doing lstat', fileName);
38+
const lstat = await fs.promises.lstat(fileName);
39+
const relativeName = (name && `${parentDir}/${name}`) || parentDir || "";
40+
console.log("relativeName", relativeName);
41+
if (lstat.isDirectory()) {
42+
console.log("Reading directory", fileName);
43+
const names = await fs.promises.readdir(fileName);
44+
await Promise.all(names.map((childName) => this.store(relativeName, childName)));
45+
return;
46+
}
47+
await this.ops.upload(this.baseDir, relativeName, null, lstat.size);
48+
}
49+
}
50+
51+
export default DeployGroup;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import importer from "./importer.mjs";
2+
3+
/**
4+
* Class that knows how to handle DICOMweb deployments.
5+
* Assumptions are that files are deployed to an archiving file system like S3 that stores items in multiple locations for safety,
6+
* and can provide direct support as a web server. Files are assumed to be able to contain their own content
7+
* type/transfer encoding.
8+
*
9+
* Studies to be uploaded come from the <rootDir>/notifications/deploy directory structure, in the standard notifications layout.
10+
*/
11+
class DeployStaticWado {
12+
/**
13+
* Create a DICOMweb deployer
14+
* @param {object} config
15+
* @param {string} config.deployPlugin is the name of the deployment plugin
16+
* @param {string} config.studiesDestination is the root URL prefix of the studies deployment directory
17+
* @param {string} config.deduplicatedDestination is the root URL prefix of the deduplicated data directory
18+
* @param {string} config.instanceDestination is the root URL prefix of the instance deduplicated data.
19+
* @param {string} config.rootDir is the DICOMweb source directory
20+
* @param {string} config.compress is the compression type to use (if true, then gzip for dicomweb and brotli for client)
21+
* @param {string} config.deployNotificationName is the name of the notification queue to use, "deploy" by default
22+
*/
23+
constructor(config) {
24+
this.config = config;
25+
}
26+
27+
async loadPlugins() {
28+
this.deployPlugin = await importer(this.config.deployPlugin);
29+
if (!this.deployPlugin) throw new Error(`Deploy plugin ${this.config.deployPlugin} not defined`);
30+
console.log("Deploy plugin=", this.config.deployPlugin, this.deployPlugin);
31+
this.clientDeploy = this.deployPlugin.factory("client", this.config);
32+
this.rootDeploy = this.deployPlugin.factory("root", this.config);
33+
return { client: this.clientDeploy, root: this.rootDeploy };
34+
}
35+
36+
/**
37+
* Deploys the client to the web service. Throws an exception if the client path isn't configured.
38+
*/
39+
deployClient() {}
40+
41+
/**
42+
* Deploys the studies that have notifications in the DICOMWeb notifications directory.
43+
* If the dicomwebserver is configured for a study index, it also updates the study index.
44+
*/
45+
deployDicomWebNotifications() {}
46+
47+
/**
48+
* Deploys the entire DICOMweb studies directory.
49+
* Files already existing will have their hash code compared. If the code is different, they will be replaced, otherwise they
50+
* will be left.
51+
*/
52+
deployDicomWebAll() {}
53+
}
54+
55+
export default DeployStaticWado;

0 commit comments

Comments
 (0)