@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 CHANGED
@@ -5,5 +5,5 @@ const { configureProgram } = require("../lib/program");
5
5
 
6
6
  // Configure program commander
7
7
  configureProgram(mkdicomwebConfig).then(() => {
8
- console.log("done");
8
+ console.verbose("done");
9
9
  });
@@ -1,16 +1,23 @@
1
- const dataExtractor = /^(.*studies)?[\\/]?([^/\\]+)[\\/](series[\\/])?([^/\\]+)([\\/]instances[\\/]([^/\\]*))?$/;
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 = (options) =>
4
- async function (rejectPath, reason) {
5
- console.log("Reject instance", rejectPath, reason);
6
- const extracted = rejectPath.match(dataExtractor);
7
- if (!extracted) {
8
- console.log("Unable to reject", rejectPath, "because it isn't in the format studies/<studyUID>/series/<seriesUID>");
9
- return;
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 [, , studyInstanceUid, , seriesInstanceUid, , sopInstanceUid] = extracted;
12
- const studyData = await this.scanStudy("studies", studyInstanceUid);
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 { Stats, handleHomeRelative } = require("@radicalimaging/static-wado-util");
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 { dirScanner, JSONReader, JSONWriter, asyncIterableToBuffer } = require("@radicalimaging/static-wado-util");
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
- TransferSyntaxUID: "1.2.840.10008.1.2",
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(args) {
65
- args.forEach((removal) => {
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, e);
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 {*} params
126
+ * @param {*} options
107
127
  */
108
- async processStudyDir(studyUids, params) {
109
- return dirScanner(params[params.scanStudies], {
110
- ...params,
111
- recursive: false,
112
- callback: (dir, name) => this.callback.scanStudy(dir, name),
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(`Can't import file ${params.file}`);
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
- isInstance: false,
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
@@ -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
  });
@@ -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: false,
9
- isStudyData: false,
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
@@ -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: "jhc",
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: "-e, --no-encapsulated-image-frame",
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, --colour-content-type <value>",
108
+ key: "-T, --color-content-type <value>",
82
109
  description: "Colour content type",
83
- defaultValue: null,
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, specified as studies/<studyUID>/series/<seriesUID>",
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.isInstanceMetadata) {
81
- // console.log("Writing instance metadata");
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 run(dir, studyInstanceUid) {
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
- console.log("Scanning", dir, studyInstanceUid);
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
- // console.log("Study data is dirty - need to write updated file");
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 deduplicatedTopFile = await JSONReader(this.studyPath, "deduplicated.gz", null);
85
- if (!deduplicatedTopFile) {
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 info = deduplicatedTopFile[0];
89
- if (!info || getValue(info, Tags.DeduppedHash) || getValue(info, Tags.DeduppedType) != "info") {
90
- return true;
104
+ const hashValue = getValue(studyFile, Tags.DeduppedHash);
105
+ if (this.existingFiles[0].indexOf(hashValue) == -1) {
106
+ return false;
91
107
  }
92
- const hashValue = getValue(info, Tags.DeduppedHash);
93
- return this.existingFiles[0].indexOf(hashValue) == -1;
108
+ console.verbose("dirtyMetadata::Dedupped hash missing");
109
+ return true;
94
110
  } catch (e) {
95
- console.log("Assume study metadata is dirty", e);
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.log("No deduplicated for", deduplicatedDirectory);
246
- return;
261
+ console.verbose("No deduplicated for", deduplicatedDirectory);
262
+ return 0;
247
263
  }
248
- console.log("There are", files.length, "files to check");
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.log("Done checking", deduplicatedDirectory);
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.readHashes[hash] = name;
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
- await JSONWriter(seriesPath, "series", [seriesQuery]);
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();