@radicalimaging/static-wado-creator 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mkdicomweb.js +1 -1
- package/lib/RejectInstance.js +18 -11
- package/lib/StaticWado.js +48 -19
- package/lib/createMain.js +6 -2
- package/lib/groupMain.js +3 -0
- package/lib/instanceMain.js +2 -0
- package/lib/metadataMain.js +3 -2
- package/lib/mkdicomwebConfig.js +36 -9
- package/lib/operation/InstanceDeduplicate.js +2 -2
- package/lib/operation/ScanStudy.js +4 -3
- package/lib/operation/StudyData.js +92 -43
- package/lib/operation/ThumbnailService.js +21 -42
- package/lib/operation/adapter/transcodeImage.js +36 -26
- package/lib/operation/extractImageFrames.js +3 -0
- package/lib/operation/getDataSet.js +6 -1
- package/lib/operation/getValue.js +22 -11
- package/lib/rejectMain.js +2 -2
- package/lib/util/adaptProgramOpts.js +10 -31
- package/lib/writer/CompleteStudyWriter.js +6 -1
- package/lib/writer/DeduplicateWriter.js +3 -0
- package/lib/writer/HashDataWriter.js +1 -2
- package/lib/writer/ImageFrameWriter.js +7 -10
- package/lib/writer/ThumbnailWriter.js +7 -14
- package/lib/writer/WriteStream.js +12 -1
- package/package.json +11 -15
package/bin/mkdicomweb.js
CHANGED
package/lib/RejectInstance.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
const dataExtractor = /^(.*studies)?[\\/]?([
|
|
1
|
+
const dataExtractor = /^(.*studies)?[\\/]?([0-9.a-zA-Z]+)[\\/](series[\\/])?([0-9.a-zA-Z]+)([\\/]instances[\\/]([0-9.a-zA-Z]*))?$/;
|
|
2
2
|
|
|
3
|
-
module.exports = (
|
|
4
|
-
async function (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
module.exports = () =>
|
|
4
|
+
async function rejectInstanceWithOptions(args) {
|
|
5
|
+
let studyInstanceUid = args[0];
|
|
6
|
+
let seriesInstanceUid;
|
|
7
|
+
let sopInstanceUid;
|
|
8
|
+
const reason = args[2] || "REJECT";
|
|
9
|
+
const extracted = args[0].match(dataExtractor);
|
|
10
|
+
if (extracted) {
|
|
11
|
+
studyInstanceUid = extracted[2];
|
|
12
|
+
seriesInstanceUid = extracted[4];
|
|
13
|
+
sopInstanceUid = extracted[6];
|
|
14
|
+
console.log("Extracted", studyInstanceUid, seriesInstanceUid, sopInstanceUid, reason);
|
|
15
|
+
} else {
|
|
16
|
+
console.log("Not extracting from URL", studyInstanceUid, seriesInstanceUid, reason);
|
|
17
|
+
seriesInstanceUid = args[1];
|
|
10
18
|
}
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
if (options.verbose) console.log("Trying to reject", seriesInstanceUid, "because", reason);
|
|
19
|
+
const studyData = await this.scanStudy(studyInstanceUid);
|
|
20
|
+
console.verbose("Trying to reject", seriesInstanceUid, "because", reason);
|
|
14
21
|
await studyData.reject(seriesInstanceUid, sopInstanceUid, reason);
|
|
15
22
|
await studyData.writeDeduplicatedGroup();
|
|
16
23
|
return studyData.writeMetadata();
|
package/lib/StaticWado.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const dicomCodec = require("@cornerstonejs/dicom-codec");
|
|
2
|
-
const
|
|
2
|
+
const staticCS = require("@radicalimaging/static-cs-lite");
|
|
3
|
+
const { Stats, handleHomeRelative, dirScanner, JSONReader, JSONWriter, asyncIterableToBuffer, Tags } = require("@radicalimaging/static-wado-util");
|
|
3
4
|
const dicomParser = require("dicom-parser");
|
|
4
5
|
const fs = require("fs");
|
|
5
6
|
const path = require("path");
|
|
6
|
-
const {
|
|
7
|
+
const { NotificationService } = require("@radicalimaging/static-wado-util");
|
|
7
8
|
const getDataSet = require("./operation/getDataSet");
|
|
8
9
|
const InstanceDeduplicate = require("./operation/InstanceDeduplicate");
|
|
9
10
|
const DeduplicateWriter = require("./writer/DeduplicateWriter");
|
|
@@ -15,15 +16,33 @@ const HashDataWriter = require("./writer/HashDataWriter");
|
|
|
15
16
|
const VideoWriter = require("./writer/VideoWriter");
|
|
16
17
|
const { transcodeImageFrame, transcodeId, transcodeMetadata } = require("./operation/adapter/transcodeImage");
|
|
17
18
|
const ThumbnailWriter = require("./writer/ThumbnailWriter");
|
|
19
|
+
const decodeImage = require("./operation/adapter/decodeImage");
|
|
18
20
|
const ThumbnailService = require("./operation/ThumbnailService");
|
|
19
21
|
const DeleteStudy = require("./DeleteStudy");
|
|
20
22
|
const RejectInstance = require("./RejectInstance");
|
|
21
23
|
const RawDicomWriter = require("./writer/RawDicomWriter");
|
|
24
|
+
const { isVideo } = require("./writer/VideoWriter");
|
|
22
25
|
|
|
23
26
|
function setStudyData(studyData) {
|
|
24
27
|
this.studyData = studyData;
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
function internalGenerateImage(originalImageFrame, dataset, metadata, transferSyntaxUid, doneCallback) {
|
|
31
|
+
decodeImage(originalImageFrame, dataset, transferSyntaxUid)
|
|
32
|
+
.then((decodeResult = {}) => {
|
|
33
|
+
if (isVideo(transferSyntaxUid)) {
|
|
34
|
+
console.log("Video data - no thumbnail generator yet");
|
|
35
|
+
} else {
|
|
36
|
+
const { imageFrame, imageInfo } = decodeResult;
|
|
37
|
+
const pixelData = dicomCodec.getPixelData(imageFrame, imageInfo, transferSyntaxUid);
|
|
38
|
+
staticCS.getRenderedBuffer(transferSyntaxUid, pixelData, metadata, doneCallback);
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
console.log(`Error while generating thumbnail:: ${error}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
class StaticWado {
|
|
28
47
|
constructor(configuration) {
|
|
29
48
|
const { rootDir = "~/dicomweb", pathDeduplicated = "deduplicated", pathInstances = "instances", verbose } = configuration;
|
|
@@ -36,7 +55,8 @@ class StaticWado {
|
|
|
36
55
|
directoryName,
|
|
37
56
|
deduplicatedRoot: path.join(directoryName, pathDeduplicated),
|
|
38
57
|
deduplicatedInstancesRoot: path.join(directoryName, pathInstances),
|
|
39
|
-
|
|
58
|
+
// TODO - make this configurable and auto-detect by updating the parseDicom library
|
|
59
|
+
TransferSyntaxUID: "1.2.840.10008.1.2.1",
|
|
40
60
|
};
|
|
41
61
|
this.callback = {
|
|
42
62
|
uids: IdCreator(this.options),
|
|
@@ -52,6 +72,8 @@ class StaticWado {
|
|
|
52
72
|
delete: DeleteStudy(this.options),
|
|
53
73
|
setStudyData,
|
|
54
74
|
rawDicomWriter: RawDicomWriter(this.options),
|
|
75
|
+
notificationService: new NotificationService(this.options.notificationDir),
|
|
76
|
+
internalGenerateImage
|
|
55
77
|
};
|
|
56
78
|
}
|
|
57
79
|
|
|
@@ -61,10 +83,8 @@ class StaticWado {
|
|
|
61
83
|
*
|
|
62
84
|
* @param {string} args listing the instances to remove (may remove entire series)
|
|
63
85
|
*/
|
|
64
|
-
async reject(
|
|
65
|
-
|
|
66
|
-
this.callback.reject(removal);
|
|
67
|
-
});
|
|
86
|
+
async reject(studyUID, seriesUID, reason) {
|
|
87
|
+
this.callback.reject(studyUID, seriesUID, reason);
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
/**
|
|
@@ -94,7 +114,7 @@ class StaticWado {
|
|
|
94
114
|
await this.importBinaryDicom(dicomp10stream, { ...params, file });
|
|
95
115
|
Stats.StudyStats.add("DICOM P10", "Parse DICOM P10 file");
|
|
96
116
|
} catch (e) {
|
|
97
|
-
console.error("Couldn't process", file
|
|
117
|
+
console.error("Couldn't process", file);
|
|
98
118
|
}
|
|
99
119
|
},
|
|
100
120
|
});
|
|
@@ -103,14 +123,13 @@ class StaticWado {
|
|
|
103
123
|
/**
|
|
104
124
|
* Processes a study directory, matching up study instance UIDs. Either processes the
|
|
105
125
|
* deduplicated group directory or the instances directory, or the notifications directory.
|
|
106
|
-
* @param {*}
|
|
126
|
+
* @param {*} options
|
|
107
127
|
*/
|
|
108
|
-
async processStudyDir(studyUids,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
128
|
+
async processStudyDir(studyUids /* , options */) {
|
|
129
|
+
for (const studyUid of studyUids) {
|
|
130
|
+
const study = await this.callback.scanStudy(studyUid);
|
|
131
|
+
console.log("Processed study", study.studyInstanceUid);
|
|
132
|
+
}
|
|
114
133
|
}
|
|
115
134
|
|
|
116
135
|
async importBinaryDicom(dicomp10stream, params) {
|
|
@@ -123,7 +142,7 @@ class StaticWado {
|
|
|
123
142
|
const studyInstanceUid = dataSet.string("x0020000d");
|
|
124
143
|
|
|
125
144
|
if (!studyInstanceUid) {
|
|
126
|
-
console.log(
|
|
145
|
+
console.log("No study UID, can't import file", params.file, dataSet.elements);
|
|
127
146
|
return undefined;
|
|
128
147
|
}
|
|
129
148
|
|
|
@@ -138,7 +157,7 @@ class StaticWado {
|
|
|
138
157
|
params.file
|
|
139
158
|
);
|
|
140
159
|
|
|
141
|
-
const targetId = transcodeId(id, this.options);
|
|
160
|
+
const targetId = transcodeId(id, this.options, dataSet.uint16(Tags.RawSamplesPerPixel));
|
|
142
161
|
|
|
143
162
|
let bulkDataIndex = 0;
|
|
144
163
|
let imageFrameIndex = 0;
|
|
@@ -178,7 +197,7 @@ class StaticWado {
|
|
|
178
197
|
await this.callback.rawDicomWriter?.(id, result, buffer);
|
|
179
198
|
|
|
180
199
|
const transcodedMeta = transcodeMetadata(result.metadata, id, this.options);
|
|
181
|
-
thumbnailService.generateThumbnails(id, dataSet, transcodedMeta, this.callback);
|
|
200
|
+
await thumbnailService.generateThumbnails(id, dataSet, transcodedMeta, this.callback);
|
|
182
201
|
|
|
183
202
|
await this.callback.metadata(targetId, transcodedMeta);
|
|
184
203
|
|
|
@@ -186,6 +205,14 @@ class StaticWado {
|
|
|
186
205
|
return {};
|
|
187
206
|
}
|
|
188
207
|
|
|
208
|
+
static async getDataSet(dataSet, generator, params) {
|
|
209
|
+
return await getDataSet(dataSet, generator, params);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static internalGenerateImage(originalImageFrame, dataSet, metadata, transferSyntaxUid, doneCallback) {
|
|
213
|
+
return internalGenerateImage(originalImageFrame, dataSet, metadata, transferSyntaxUid, doneCallback)
|
|
214
|
+
}
|
|
215
|
+
|
|
189
216
|
/**
|
|
190
217
|
* The mkdicomweb command first runs mkdicomwebinstances, writing out the deduplicated data, and then runs the
|
|
191
218
|
* mkdicomwebstudy command, creating the deduplicated data set. This version, however, keeps the deduplicated
|
|
@@ -193,16 +220,18 @@ class StaticWado {
|
|
|
193
220
|
*/
|
|
194
221
|
async executeCommand(input) {
|
|
195
222
|
if (this.options.scanStudies) {
|
|
223
|
+
console.log("Scanning study dir", input);
|
|
196
224
|
// Scan one of the study directories - in this case, files is a set of study directories
|
|
197
225
|
await this.processStudyDir(input, this.options);
|
|
198
226
|
} else {
|
|
227
|
+
console.log("Scanning files", input);
|
|
199
228
|
await this.processFiles(input, this.options);
|
|
200
229
|
}
|
|
201
230
|
await this.close();
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
async close() {
|
|
205
|
-
await this.callback.completeStudy();
|
|
234
|
+
await this.callback.completeStudy(this.callback);
|
|
206
235
|
Stats.OverallStats.summarize("Completed Study Processing");
|
|
207
236
|
}
|
|
208
237
|
|
package/lib/createMain.js
CHANGED
|
@@ -4,10 +4,14 @@ const adaptProgramOpts = require("./util/adaptProgramOpts");
|
|
|
4
4
|
module.exports = function createMain(options, program) {
|
|
5
5
|
const finalOptions = adaptProgramOpts(options, {
|
|
6
6
|
...this,
|
|
7
|
-
|
|
7
|
+
// Instance metadata is the instances/<sopUID>/metadata.gz files
|
|
8
|
+
isInstance: true,
|
|
9
|
+
// Deduplicated data is single instance deduplicated data
|
|
10
|
+
isDeduplicate: false,
|
|
11
|
+
// Group data is the group file directories
|
|
8
12
|
isGroup: true,
|
|
9
|
-
isDeduplicate: true,
|
|
10
13
|
isStudyData: true,
|
|
14
|
+
isDeleteInstances: true,
|
|
11
15
|
});
|
|
12
16
|
const importer = new StaticWado(finalOptions);
|
|
13
17
|
return importer.executeCommand(program.args);
|
package/lib/groupMain.js
CHANGED
|
@@ -5,8 +5,11 @@ module.exports = function createMain(options, program) {
|
|
|
5
5
|
const finalOptions = adaptProgramOpts(options, {
|
|
6
6
|
...this,
|
|
7
7
|
isInstance: false,
|
|
8
|
+
isDeduplicate: true,
|
|
8
9
|
isGroup: true,
|
|
9
10
|
isStudyData: false,
|
|
11
|
+
scanStudies: true,
|
|
12
|
+
isDeleteInstances: true,
|
|
10
13
|
});
|
|
11
14
|
const importer = new StaticWado(finalOptions);
|
|
12
15
|
// Either do a scan or on specified studies
|
package/lib/instanceMain.js
CHANGED
|
@@ -5,6 +5,8 @@ module.exports = function createMain(options, program) {
|
|
|
5
5
|
const finalOptions = adaptProgramOpts(options, {
|
|
6
6
|
...this,
|
|
7
7
|
isInstance: true,
|
|
8
|
+
// Deduplicated data is single instance deduplicated data
|
|
9
|
+
isDeduplicate: true,
|
|
8
10
|
isGroup: false,
|
|
9
11
|
isStudyData: false,
|
|
10
12
|
});
|
package/lib/metadataMain.js
CHANGED
|
@@ -5,8 +5,9 @@ module.exports = function createMain(options, program) {
|
|
|
5
5
|
const finalOptions = adaptProgramOpts(options, {
|
|
6
6
|
...this,
|
|
7
7
|
isInstance: false,
|
|
8
|
-
isGroup:
|
|
9
|
-
isStudyData:
|
|
8
|
+
isGroup: true,
|
|
9
|
+
isStudyData: true,
|
|
10
|
+
scanStudies: true,
|
|
10
11
|
});
|
|
11
12
|
const importer = new StaticWado(finalOptions);
|
|
12
13
|
// Either do a scan or on specified studies
|
package/lib/mkdicomwebConfig.js
CHANGED
|
@@ -30,6 +30,15 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
30
30
|
description: "List of deployments from configuration to deploy to. Separated by space.",
|
|
31
31
|
defaultValue: undefined,
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
key: "--no-notifications",
|
|
35
|
+
description: "Turns off notifications",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "--delete",
|
|
39
|
+
description: "Delete the imported instances",
|
|
40
|
+
defaultValue: false,
|
|
41
|
+
},
|
|
33
42
|
{
|
|
34
43
|
key: "-v, --verbose",
|
|
35
44
|
description: "Write verbose output",
|
|
@@ -37,14 +46,26 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
37
46
|
},
|
|
38
47
|
{
|
|
39
48
|
key: "-t, --content-type <type>",
|
|
40
|
-
description: 'Destination type to compress to (choices: "jls", "lei", "jls-lossy", "jhc", "jxl" or DICOM Transfer Syntax UID - default: "jls")',
|
|
41
|
-
defaultValue: "
|
|
49
|
+
description: 'Destination type to compress to (choices: "jpeg", "jls", "lei", "jls-lossy", "jhc", "jxl" or DICOM Transfer Syntax UID - default: "jls")',
|
|
50
|
+
defaultValue: "jls",
|
|
42
51
|
customParser: compressionOptionParser,
|
|
43
52
|
},
|
|
44
53
|
{
|
|
45
|
-
key: "
|
|
54
|
+
key: "--encapsulated-image",
|
|
55
|
+
description: "Avoid encapsulating the image frame. Writes with the extension and without multipart",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "-e, --no-encapsulated-image",
|
|
46
59
|
description: "Avoid encapsulating the image frame. Writes with the extension and without multipart",
|
|
47
60
|
},
|
|
61
|
+
{
|
|
62
|
+
key: "--single-part-image",
|
|
63
|
+
description: "Writes single part image data",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "--no-single-part-image",
|
|
67
|
+
description: "Writes single part image data",
|
|
68
|
+
},
|
|
48
69
|
{
|
|
49
70
|
key: "-m, --maximum-inline-public-length <value>",
|
|
50
71
|
description: "Maximum length of public binary data",
|
|
@@ -59,7 +80,13 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
59
80
|
key: "-r, --recompress <listvalue...>",
|
|
60
81
|
description: "List of types to recompress separated by space",
|
|
61
82
|
defaultValue: ["uncompressed", "jp2", "jls", "jll"],
|
|
62
|
-
choices: ["uncompressed", "jp2", "jpeglossless", "rle", "jph", "jls", "true", "none"],
|
|
83
|
+
choices: ["uncompressed", "jp2", "jpeg", "jpeglossless", "rle", "jph", "jls", "true", "none"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "--recompress-color <listvalue...>",
|
|
87
|
+
description: "List of types to recompress for color images, separated by space",
|
|
88
|
+
defaultValue: ["uncompressed"],
|
|
89
|
+
choices: ["uncompressed", "jpeg", "jp2", "jpeglossless", "rle", "jph", "jls", "true", "none"],
|
|
63
90
|
},
|
|
64
91
|
{
|
|
65
92
|
key: "--recompress-thumb <listvalue...>",
|
|
@@ -78,9 +105,9 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
78
105
|
defaultValue: false,
|
|
79
106
|
},
|
|
80
107
|
{
|
|
81
|
-
key: "-T, --
|
|
108
|
+
key: "-T, --color-content-type <value>",
|
|
82
109
|
description: "Colour content type",
|
|
83
|
-
defaultValue:
|
|
110
|
+
defaultValue: "jpeg",
|
|
84
111
|
},
|
|
85
112
|
{
|
|
86
113
|
key: "--path-deduplicated <path>",
|
|
@@ -145,7 +172,7 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
145
172
|
command: "metadata",
|
|
146
173
|
arguments: ["input"],
|
|
147
174
|
main: metadataMain,
|
|
148
|
-
helpDescription: "Write the metadata object (series and study details) from the deduplicated data.",
|
|
175
|
+
helpDescription: "Write the metadata object (series and study details) from the grouped deduplicated data.",
|
|
149
176
|
},
|
|
150
177
|
{
|
|
151
178
|
command: "delete",
|
|
@@ -153,9 +180,9 @@ const { mkdicomwebConfig } = ConfigPoint.register({
|
|
|
153
180
|
helpDescription: "Delete the given study, series or instance (not yet implemented)",
|
|
154
181
|
},
|
|
155
182
|
{
|
|
156
|
-
command: "reject",
|
|
183
|
+
command: "reject <studyUID...>",
|
|
157
184
|
main: rejectMain,
|
|
158
|
-
helpDescription: "Reject the specified series
|
|
185
|
+
helpDescription: "Reject the specified series",
|
|
159
186
|
},
|
|
160
187
|
],
|
|
161
188
|
},
|
|
@@ -77,8 +77,8 @@ const InstanceDeduplicate = (options) =>
|
|
|
77
77
|
async function run(id, sourceImageFrame) {
|
|
78
78
|
// Notify the existing listeners, if any
|
|
79
79
|
const imageFrame = canonicalize(sourceImageFrame);
|
|
80
|
-
if (options.
|
|
81
|
-
|
|
80
|
+
if (options.isInstance) {
|
|
81
|
+
if (options.verbose) console.log("Writing instance metadata");
|
|
82
82
|
await JSONWriter(id.sopInstanceRootPath, "metadata", imageFrame);
|
|
83
83
|
}
|
|
84
84
|
if (!options.isDeduplicate && !options.isGroup) {
|
|
@@ -3,11 +3,12 @@ const path = require("path");
|
|
|
3
3
|
function ScanStudy(options) {
|
|
4
4
|
const { directoryName, deduplicatedRoot, deduplicatedInstancesRoot } = options;
|
|
5
5
|
|
|
6
|
-
return function
|
|
6
|
+
return function scanStudy(studyInstanceUid) {
|
|
7
|
+
console.verbose("scanStudy", studyInstanceUid);
|
|
7
8
|
const studyPath = path.join(directoryName, "studies", studyInstanceUid);
|
|
8
|
-
const deduplicatedPath = path.join(deduplicatedRoot, studyInstanceUid);
|
|
9
9
|
const deduplicatedInstancesPath = path.join(deduplicatedInstancesRoot, studyInstanceUid);
|
|
10
|
-
|
|
10
|
+
const deduplicatedPath = path.join(deduplicatedRoot, studyInstanceUid);
|
|
11
|
+
console.verbose("Importing", studyInstanceUid, studyPath, deduplicatedInstancesPath, deduplicatedPath);
|
|
11
12
|
return this.completeStudy.getCurrentStudyData(this, {
|
|
12
13
|
studyPath,
|
|
13
14
|
deduplicatedPath,
|
|
@@ -41,6 +41,7 @@ class StudyData {
|
|
|
41
41
|
// The deduplicated hashes are just the hashes for individual items
|
|
42
42
|
this.deduplicatedHashes = {};
|
|
43
43
|
this.sopInstances = {};
|
|
44
|
+
this.instanceFiles = 0;
|
|
44
45
|
|
|
45
46
|
// Used to track if new instances have been added.
|
|
46
47
|
this.newInstancesAdded = 0;
|
|
@@ -54,11 +55,22 @@ class StudyData {
|
|
|
54
55
|
if (clean) {
|
|
55
56
|
// Wipe out the study directory entirely, as well as the deduplicatedRoot and instancesRoot
|
|
56
57
|
}
|
|
58
|
+
this.groupFiles = 0;
|
|
59
|
+
const studyDeduplicated = await JSONReader(this.studyPath, "deduplicated/index.json.gz", []);
|
|
60
|
+
const info = studyDeduplicated[0];
|
|
61
|
+
if (info) {
|
|
62
|
+
const hash = getValue(info, Tags.DeduppedHash);
|
|
63
|
+
console.log("Reading studies/<studyUID>/deduplicated/index.json.gz");
|
|
64
|
+
this.readDeduplicatedData("index.json.gz", studyDeduplicated, hash);
|
|
65
|
+
} else {
|
|
66
|
+
console.log("No deduplicated/index.json to read in", this.studyPath, "/deduplicated/index.json.gz");
|
|
67
|
+
}
|
|
57
68
|
if (this.deduplicatedPath) {
|
|
58
|
-
await this.readDeduplicated(this.deduplicatedPath);
|
|
69
|
+
this.groupFiles = await this.readDeduplicated(this.deduplicatedPath);
|
|
70
|
+
console.verbose("Read groupFiles:", this.groupFiles);
|
|
59
71
|
}
|
|
60
72
|
if (this.deduplicatedInstancesPath) {
|
|
61
|
-
await this.readDeduplicated(this.deduplicatedInstancesPath);
|
|
73
|
+
this.instanceFiles = await this.readDeduplicated(this.deduplicatedInstancesPath);
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -72,33 +84,36 @@ class StudyData {
|
|
|
72
84
|
* a separate type of check.
|
|
73
85
|
*/
|
|
74
86
|
get dirty() {
|
|
75
|
-
return this.newInstancesAdded > 0 || this.existingFiles.length > 1;
|
|
87
|
+
return this.newInstancesAdded > 0 || this.existingFiles.length > 1 || this.instanceFiles > 0;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
async dirtyMetadata() {
|
|
79
91
|
if (this.dirty) {
|
|
80
|
-
|
|
92
|
+
console.verbose("dirtyMetadata::Group data is dirty");
|
|
81
93
|
return true;
|
|
82
94
|
}
|
|
95
|
+
if (this.groupFiles > 0) {
|
|
96
|
+
console.verbose("dirtyMetadata::Study level deduplicated doesn't match group files");
|
|
97
|
+
}
|
|
83
98
|
try {
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
99
|
+
const studyFile = await JSONReader(this.studyPath, "index.json.gz", null);
|
|
100
|
+
if (!studyFile) {
|
|
101
|
+
console.verbose("dirtyMetadata::studyIndex");
|
|
86
102
|
return true;
|
|
87
103
|
}
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
return
|
|
104
|
+
const hashValue = getValue(studyFile, Tags.DeduppedHash);
|
|
105
|
+
if (this.existingFiles[0].indexOf(hashValue) == -1) {
|
|
106
|
+
return false;
|
|
91
107
|
}
|
|
92
|
-
|
|
93
|
-
return
|
|
108
|
+
console.verbose("dirtyMetadata::Dedupped hash missing");
|
|
109
|
+
return true;
|
|
94
110
|
} catch (e) {
|
|
95
|
-
console.
|
|
111
|
+
console.verbose("dirtyMetadata::Exception, assume study metadata is dirty", e);
|
|
96
112
|
return true;
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
|
|
100
|
-
async reject(seriesInstanceUid, sopInstanceUid, reason) {
|
|
101
|
-
console.log("Rejecting series instance UID", seriesInstanceUid, "because", reason);
|
|
116
|
+
async reject(seriesInstanceUid, sopInstanceUid /* , reason */) {
|
|
102
117
|
// TODO - actually add a reject note...
|
|
103
118
|
this.newInstancesAdded += 1;
|
|
104
119
|
for (let i = 0; i < this.deduplicated.length; i++) {
|
|
@@ -107,7 +122,7 @@ class StudyData {
|
|
|
107
122
|
if (reSeriesUid !== seriesInstanceUid) continue;
|
|
108
123
|
const reSop = getValue(recombined, Tags.SOPInstanceUID);
|
|
109
124
|
if (!reSop) continue;
|
|
110
|
-
if (reSop !== sopInstanceUid) continue;
|
|
125
|
+
if (sopInstanceUid && reSop !== sopInstanceUid) continue;
|
|
111
126
|
setValue(this.deduplicated[i], Tags.DeduppedType, "deleted");
|
|
112
127
|
}
|
|
113
128
|
}
|
|
@@ -239,25 +254,28 @@ class StudyData {
|
|
|
239
254
|
* @param {*} deduplicatedDirectory
|
|
240
255
|
*/
|
|
241
256
|
async readDeduplicated(deduplicatedDirectory) {
|
|
257
|
+
let readCount = 0;
|
|
242
258
|
try {
|
|
243
259
|
const files = await this.listJsonFiles(deduplicatedDirectory);
|
|
244
260
|
if (!files || !files.length) {
|
|
245
|
-
console.
|
|
246
|
-
return;
|
|
261
|
+
console.verbose("No deduplicated for", deduplicatedDirectory);
|
|
262
|
+
return 0;
|
|
247
263
|
}
|
|
248
|
-
console.
|
|
264
|
+
console.verbose("There are", files.length, "files to check");
|
|
249
265
|
for (let i = 0; i < files.length; i++) {
|
|
250
266
|
const stat = files[i];
|
|
251
267
|
const { hash } = stat;
|
|
252
268
|
if (this.readHashes[hash]) {
|
|
253
269
|
continue;
|
|
254
270
|
}
|
|
271
|
+
readCount += 1;
|
|
255
272
|
await this.readDeduplicatedFile(deduplicatedDirectory, stat);
|
|
256
273
|
}
|
|
257
|
-
console.
|
|
274
|
+
console.verbose("Done checking", deduplicatedDirectory);
|
|
258
275
|
} catch (e) {
|
|
259
276
|
// No-op console.log(e);
|
|
260
277
|
}
|
|
278
|
+
return readCount;
|
|
261
279
|
}
|
|
262
280
|
|
|
263
281
|
async readDeduplicatedFile(dir, stat) {
|
|
@@ -265,32 +283,36 @@ class StudyData {
|
|
|
265
283
|
try {
|
|
266
284
|
if (this.verbose) console.log("Reading deduplicated file", name);
|
|
267
285
|
const data = await JSONReader(dir, name);
|
|
268
|
-
this.
|
|
269
|
-
this.existingFiles.push(name);
|
|
270
|
-
const listData = (Array.isArray(data) && data) || [data];
|
|
271
|
-
listData.forEach((item) => {
|
|
272
|
-
const type = getValue(item, Tags.DeduppedType);
|
|
273
|
-
if (type == Tags.InstanceType) {
|
|
274
|
-
this.internalAddDeduplicated(item, name);
|
|
275
|
-
} else if (type == "info") {
|
|
276
|
-
const refs = getList(item, Tags.DeduppedRef);
|
|
277
|
-
if (refs) {
|
|
278
|
-
refs.forEach((hashValue) => {
|
|
279
|
-
this.readHashes[hashValue] = `${hashValue}.gz`;
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
const hashValue = getValue(item, Tags.DeduppedHash);
|
|
284
|
-
if (hashValue) {
|
|
285
|
-
this.extractData[hashValue] = item;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
});
|
|
286
|
+
this.readDeduplicatedData(name, data, hash);
|
|
289
287
|
} catch (e) {
|
|
290
288
|
console.error("Unable to read", dir, name);
|
|
291
289
|
}
|
|
292
290
|
}
|
|
293
291
|
|
|
292
|
+
readDeduplicatedData(name, data, hash) {
|
|
293
|
+
this.readHashes[hash] = name;
|
|
294
|
+
this.existingFiles.push(name);
|
|
295
|
+
const listData = (Array.isArray(data) && data) || [data];
|
|
296
|
+
listData.forEach((item) => {
|
|
297
|
+
const type = getValue(item, Tags.DeduppedType);
|
|
298
|
+
if (type === Tags.InstanceType || type === Tags.DeletedType) {
|
|
299
|
+
this.internalAddDeduplicated(item, name);
|
|
300
|
+
} else if (type == "info") {
|
|
301
|
+
const refs = getList(item, Tags.DeduppedRef);
|
|
302
|
+
if (refs) {
|
|
303
|
+
refs.forEach((hashValue) => {
|
|
304
|
+
this.readHashes[hashValue] = `${hashValue}.gz`;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
const hashValue = getValue(item, Tags.DeduppedHash);
|
|
309
|
+
if (hashValue) {
|
|
310
|
+
this.extractData[hashValue] = item;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
294
316
|
async writeMetadata() {
|
|
295
317
|
const anInstance = await this.recombine(0);
|
|
296
318
|
const series = {};
|
|
@@ -299,7 +321,7 @@ class StudyData {
|
|
|
299
321
|
const seriesInstance = await this.recombine(i);
|
|
300
322
|
const type = getValue(seriesInstance, Tags.DeduppedType);
|
|
301
323
|
if (type == "deleted") {
|
|
302
|
-
console.log("Skipping", type);
|
|
324
|
+
console.log("Skipping deleted instance", type, getValue(seriesInstance, Tags.SeriesInstanceUID));
|
|
303
325
|
continue;
|
|
304
326
|
}
|
|
305
327
|
const seriesInstanceUid = getSeriesInstanceUid(seriesInstance);
|
|
@@ -334,14 +356,15 @@ class StudyData {
|
|
|
334
356
|
};
|
|
335
357
|
numberOfInstances += instances.length;
|
|
336
358
|
numberOfSeries += 1;
|
|
337
|
-
seriesList.push(seriesQuery);
|
|
338
359
|
const modality = getValue(seriesQuery, Tags.Modality);
|
|
360
|
+
seriesList.push(seriesQuery);
|
|
339
361
|
if (modalitiesInStudy.indexOf(modality) == -1) modalitiesInStudy.push(modality);
|
|
340
362
|
await JSONWriter(seriesPath, "metadata", instances, {
|
|
341
363
|
gzip: true,
|
|
342
364
|
index: false,
|
|
343
365
|
});
|
|
344
|
-
|
|
366
|
+
// Write out a series singleton that has just the series response for a single series.
|
|
367
|
+
await JSONWriter(seriesPath, "series-singleton.json", [seriesQuery], {gzip:true, index:false});
|
|
345
368
|
await JSONWriter(seriesPath, "instances", instancesQuery);
|
|
346
369
|
}
|
|
347
370
|
|
|
@@ -388,6 +411,32 @@ class StudyData {
|
|
|
388
411
|
return (gzIndex > 0 && name.substring(0, gzIndex)) || name;
|
|
389
412
|
}
|
|
390
413
|
|
|
414
|
+
async deleteInstancesReferenced() {
|
|
415
|
+
const deduplicatedDirectory = this.deduplicatedInstancesPath;
|
|
416
|
+
if (!fs.existsSync(deduplicatedDirectory)) return;
|
|
417
|
+
console.log("Deleting instances referenced in", this.studyInstanceUid, this.deduplicatedInstancesPath);
|
|
418
|
+
const files = await this.listJsonFiles(deduplicatedDirectory);
|
|
419
|
+
console.log("Deleting", files.length, "files");
|
|
420
|
+
let deleteCount = 0;
|
|
421
|
+
for (let i = 0; i < files.length; i++) {
|
|
422
|
+
const stat = files[i];
|
|
423
|
+
const { hash } = stat;
|
|
424
|
+
if (this.readHashes[hash]) {
|
|
425
|
+
console.log("Deleting", deduplicatedDirectory, stat.name);
|
|
426
|
+
try {
|
|
427
|
+
fs.unlinkSync(path.join(deduplicatedDirectory, stat.name));
|
|
428
|
+
deleteCount += 1;
|
|
429
|
+
} catch (e) {
|
|
430
|
+
console.log("Delete failed", e);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (deleteCount === files.length) {
|
|
435
|
+
console.log("Deleting instances directory", deduplicatedDirectory);
|
|
436
|
+
fs.rmdirSync(deduplicatedDirectory);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
391
440
|
/** Writes the deduplicated group */
|
|
392
441
|
async writeDeduplicatedGroup() {
|
|
393
442
|
const data = this.createInfo();
|