dicom-curate 0.40.0 → 0.40.2
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/README.md +21 -0
- package/dist/esm/applyMappingsWorker.js +6 -4
- package/dist/esm/curateOne.js +6 -4
- package/dist/esm/index.js +28 -10
- package/dist/esm/mappingWorkerPool.js +26 -8
- package/dist/esm/scanDirectoryWorker.js +60 -2
- package/dist/types/mappingWorkerPool.d.ts +1 -0
- package/dist/types/scanDirectoryWorker.d.ts +9 -0
- package/dist/umd/dicom-curate.umd.js +127 -19
- package/dist/umd/dicom-curate.umd.js.map +1 -1
- package/dist/umd/dicom-curate.umd.min.js +2 -2
- package/dist/umd/dicom-curate.umd.min.js.map +1 -1
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -436,6 +436,27 @@ When a file is excluded, `curateOne` / `curateMany` still returns a result objec
|
|
|
436
436
|
result.excluded // => 'pre' | 'post' | undefined
|
|
437
437
|
```
|
|
438
438
|
|
|
439
|
+
## Testing
|
|
440
|
+
|
|
441
|
+
Vitest is split into three projects in `vitest.config.ts`:
|
|
442
|
+
|
|
443
|
+
| Command | Project | Location | Purpose |
|
|
444
|
+
|---------|---------|----------|---------|
|
|
445
|
+
| `pnpm test` | `unit` | `src/**/*.test.ts` | Unit and integration tests co-located with source |
|
|
446
|
+
| `pnpm test:e2e` | `e2e` | `e2e/` | Pipeline smoke tests (runs `build:esm` first; uses `dist/`) |
|
|
447
|
+
| `pnpm test:conformance` | `conformance` | `conformance/` | `dciodvfy` regression (synthetic; optional public/local via env) |
|
|
448
|
+
|
|
449
|
+
Other scripts:
|
|
450
|
+
|
|
451
|
+
| Script | Purpose |
|
|
452
|
+
|--------|---------|
|
|
453
|
+
| `pnpm test:coverage` | Unit project with coverage |
|
|
454
|
+
| `pnpm update:conformance-baselines` | Regenerate committed `dciodvfy` baseline JSON |
|
|
455
|
+
|
|
456
|
+
Conformance tests require the external `dciodvfy` binary from [dicom3tools](https://www.dclunie.com/dicom3tools.html). See [conformance/README.md](conformance/README.md) for install, CI behaviour, baseline refresh, and optional `RUN_PUBLIC_CONFORMANCE` / `CONFORMANCE_LOCAL_*` env vars.
|
|
457
|
+
|
|
458
|
+
Shared test helpers: `testutils/` (minimal DICOM files, worker mocks). Fixture generation and public-case fetch: devDependency [`dicom-synth`](https://github.com/clintools/dicom-synth).
|
|
459
|
+
|
|
439
460
|
## DICOM Conformance Notes
|
|
440
461
|
|
|
441
462
|
dicom-curate
|
|
@@ -86824,10 +86824,12 @@ async function curateOne({
|
|
|
86824
86824
|
error3
|
|
86825
86825
|
);
|
|
86826
86826
|
const mapResults = {
|
|
86827
|
-
|
|
86828
|
-
errors
|
|
86829
|
-
|
|
86830
|
-
|
|
86827
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
86828
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
86829
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
86830
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
86831
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
86832
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
86831
86833
|
sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
86832
86834
|
fileInfo: {
|
|
86833
86835
|
name: fileInfo.name,
|
package/dist/esm/curateOne.js
CHANGED
|
@@ -80533,10 +80533,12 @@ async function curateOne({
|
|
|
80533
80533
|
error3
|
|
80534
80534
|
);
|
|
80535
80535
|
const mapResults = {
|
|
80536
|
-
|
|
80537
|
-
errors
|
|
80538
|
-
|
|
80539
|
-
|
|
80536
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
80537
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
80538
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
80539
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
80540
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
80541
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
80540
80542
|
sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
80541
80543
|
fileInfo: {
|
|
80542
80544
|
name: fileInfo.name,
|
package/dist/esm/index.js
CHANGED
|
@@ -88427,10 +88427,12 @@ async function curateOne({
|
|
|
88427
88427
|
error3
|
|
88428
88428
|
);
|
|
88429
88429
|
const mapResults = {
|
|
88430
|
-
|
|
88431
|
-
errors
|
|
88432
|
-
|
|
88433
|
-
|
|
88430
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
88431
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
88432
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
88433
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
88434
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
88435
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
88434
88436
|
sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
88435
88437
|
fileInfo: {
|
|
88436
88438
|
name: fileInfo.name,
|
|
@@ -94704,8 +94706,24 @@ async function dispatchMappingJobs() {
|
|
|
94704
94706
|
console.log("job is finished");
|
|
94705
94707
|
if (!mapResultsList)
|
|
94706
94708
|
mapResultsList = [];
|
|
94707
|
-
|
|
94708
|
-
|
|
94709
|
+
scanAnomalies.forEach(({ fileInfo, anomalies, errors }) => {
|
|
94710
|
+
const hasReadErrors = !!errors && errors.length > 0;
|
|
94711
|
+
if (hasReadErrors) {
|
|
94712
|
+
const scanErrorResult = {
|
|
94713
|
+
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
94714
|
+
// Intentionally no outputFilePath: an unread file has no
|
|
94715
|
+
// de-identified output path, and we must not place the raw input
|
|
94716
|
+
// path in the server-bound channel.
|
|
94717
|
+
mappings: {},
|
|
94718
|
+
anomalies: anomalies ?? [],
|
|
94719
|
+
errors,
|
|
94720
|
+
quarantine: {},
|
|
94721
|
+
fileInfo
|
|
94722
|
+
};
|
|
94723
|
+
mapResultsList.push(scanErrorResult);
|
|
94724
|
+
return;
|
|
94725
|
+
}
|
|
94726
|
+
if (!mappingWorkerOptions.skipWrite && anomalies.length > 0) {
|
|
94709
94727
|
const scanAnomalyResult = {
|
|
94710
94728
|
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
94711
94729
|
outputFilePath: `${fileInfo.path}/${fileInfo.name}`,
|
|
@@ -94717,8 +94735,8 @@ async function dispatchMappingJobs() {
|
|
|
94717
94735
|
quarantine: {}
|
|
94718
94736
|
};
|
|
94719
94737
|
mapResultsList.push(scanAnomalyResult);
|
|
94720
|
-
}
|
|
94721
|
-
}
|
|
94738
|
+
}
|
|
94739
|
+
});
|
|
94722
94740
|
progressCallback({
|
|
94723
94741
|
response: "done",
|
|
94724
94742
|
mapResultsList,
|
|
@@ -94945,8 +94963,8 @@ async function initializeFileListWorker(rejectCallback) {
|
|
|
94945
94963
|
break;
|
|
94946
94964
|
}
|
|
94947
94965
|
case "scanAnomalies": {
|
|
94948
|
-
const { fileInfo: anomalyFileInfo, anomalies } = event.data;
|
|
94949
|
-
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies });
|
|
94966
|
+
const { fileInfo: anomalyFileInfo, anomalies, errors } = event.data;
|
|
94967
|
+
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies, errors });
|
|
94950
94968
|
break;
|
|
94951
94969
|
}
|
|
94952
94970
|
case "count": {
|
|
@@ -86824,10 +86824,12 @@ async function curateOne({
|
|
|
86824
86824
|
error3
|
|
86825
86825
|
);
|
|
86826
86826
|
const mapResults = {
|
|
86827
|
-
|
|
86828
|
-
errors
|
|
86829
|
-
|
|
86830
|
-
|
|
86827
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
86828
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
86829
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
86830
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
86831
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
86832
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
86831
86833
|
sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
86832
86834
|
fileInfo: {
|
|
86833
86835
|
name: fileInfo.name,
|
|
@@ -93104,8 +93106,24 @@ async function dispatchMappingJobs() {
|
|
|
93104
93106
|
console.log("job is finished");
|
|
93105
93107
|
if (!mapResultsList)
|
|
93106
93108
|
mapResultsList = [];
|
|
93107
|
-
|
|
93108
|
-
|
|
93109
|
+
scanAnomalies.forEach(({ fileInfo, anomalies, errors }) => {
|
|
93110
|
+
const hasReadErrors = !!errors && errors.length > 0;
|
|
93111
|
+
if (hasReadErrors) {
|
|
93112
|
+
const scanErrorResult = {
|
|
93113
|
+
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
93114
|
+
// Intentionally no outputFilePath: an unread file has no
|
|
93115
|
+
// de-identified output path, and we must not place the raw input
|
|
93116
|
+
// path in the server-bound channel.
|
|
93117
|
+
mappings: {},
|
|
93118
|
+
anomalies: anomalies ?? [],
|
|
93119
|
+
errors,
|
|
93120
|
+
quarantine: {},
|
|
93121
|
+
fileInfo
|
|
93122
|
+
};
|
|
93123
|
+
mapResultsList.push(scanErrorResult);
|
|
93124
|
+
return;
|
|
93125
|
+
}
|
|
93126
|
+
if (!mappingWorkerOptions.skipWrite && anomalies.length > 0) {
|
|
93109
93127
|
const scanAnomalyResult = {
|
|
93110
93128
|
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
93111
93129
|
outputFilePath: `${fileInfo.path}/${fileInfo.name}`,
|
|
@@ -93117,8 +93135,8 @@ async function dispatchMappingJobs() {
|
|
|
93117
93135
|
quarantine: {}
|
|
93118
93136
|
};
|
|
93119
93137
|
mapResultsList.push(scanAnomalyResult);
|
|
93120
|
-
}
|
|
93121
|
-
}
|
|
93138
|
+
}
|
|
93139
|
+
});
|
|
93122
93140
|
progressCallback({
|
|
93123
93141
|
response: "done",
|
|
93124
93142
|
mapResultsList,
|
|
@@ -37136,6 +37136,10 @@ var DEFAULT_EXCLUDED_FILETYPES = [
|
|
|
37136
37136
|
"thumbs.db",
|
|
37137
37137
|
".ds_store"
|
|
37138
37138
|
];
|
|
37139
|
+
function safeReadErrorMessage(error2) {
|
|
37140
|
+
const detail = error2 instanceof Error ? `${error2.name}: ${error2.message}` : String(error2);
|
|
37141
|
+
return `Unable to read file (filesystem error): ${detail}`;
|
|
37142
|
+
}
|
|
37139
37143
|
var keepScanning = true;
|
|
37140
37144
|
var pauseResolve = null;
|
|
37141
37145
|
var pausePromise = null;
|
|
@@ -37445,9 +37449,36 @@ async function scanDirectory(dir) {
|
|
|
37445
37449
|
if (!keepScanning)
|
|
37446
37450
|
return;
|
|
37447
37451
|
if (entry.kind === "file") {
|
|
37448
|
-
const file = await entry.getFile();
|
|
37449
37452
|
const key = `${prefix}/${entry.name}`;
|
|
37450
37453
|
const prev = previousIndex ? previousIndex[key] : void 0;
|
|
37454
|
+
let file;
|
|
37455
|
+
try {
|
|
37456
|
+
file = await entry.getFile();
|
|
37457
|
+
} catch (readError) {
|
|
37458
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
37459
|
+
totalDiscovered--;
|
|
37460
|
+
globalThis.postMessage({
|
|
37461
|
+
response: "count",
|
|
37462
|
+
totalDiscovered
|
|
37463
|
+
});
|
|
37464
|
+
}
|
|
37465
|
+
globalThis.postMessage({
|
|
37466
|
+
response: "scanAnomalies",
|
|
37467
|
+
fileInfo: {
|
|
37468
|
+
path: prefix,
|
|
37469
|
+
name: entry.name,
|
|
37470
|
+
size: 0,
|
|
37471
|
+
kind: "handle",
|
|
37472
|
+
fileHandle: entry
|
|
37473
|
+
},
|
|
37474
|
+
anomalies: [],
|
|
37475
|
+
errors: [safeReadErrorMessage(readError)],
|
|
37476
|
+
previousFileInfo: prev
|
|
37477
|
+
});
|
|
37478
|
+
if (!await waitIfPaused())
|
|
37479
|
+
return;
|
|
37480
|
+
continue;
|
|
37481
|
+
}
|
|
37451
37482
|
const fileAnomalies = [];
|
|
37452
37483
|
if (await shouldProcessFile(file, fileAnomalies, key)) {
|
|
37453
37484
|
globalThis.postMessage({
|
|
@@ -37545,9 +37576,36 @@ async function scanDirectoryNode(dirPath) {
|
|
|
37545
37576
|
return;
|
|
37546
37577
|
if (entry.isFile()) {
|
|
37547
37578
|
const filePath = path.join(currentPath, entry.name);
|
|
37548
|
-
const stats = await fs.stat(filePath);
|
|
37549
37579
|
const key = `${prefix}/${entry.name}`;
|
|
37550
37580
|
const prev = previousIndex ? previousIndex[key] : void 0;
|
|
37581
|
+
let stats;
|
|
37582
|
+
try {
|
|
37583
|
+
stats = await fs.stat(filePath);
|
|
37584
|
+
} catch (readError) {
|
|
37585
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
37586
|
+
totalDiscovered--;
|
|
37587
|
+
globalThis.postMessage({
|
|
37588
|
+
response: "count",
|
|
37589
|
+
totalDiscovered
|
|
37590
|
+
});
|
|
37591
|
+
}
|
|
37592
|
+
globalThis.postMessage({
|
|
37593
|
+
response: "scanAnomalies",
|
|
37594
|
+
fileInfo: {
|
|
37595
|
+
path: prefix,
|
|
37596
|
+
name: entry.name,
|
|
37597
|
+
size: 0,
|
|
37598
|
+
kind: "path",
|
|
37599
|
+
fullPath: filePath
|
|
37600
|
+
},
|
|
37601
|
+
anomalies: [],
|
|
37602
|
+
errors: [safeReadErrorMessage(readError)],
|
|
37603
|
+
previousFileInfo: prev
|
|
37604
|
+
});
|
|
37605
|
+
if (!await waitIfPaused())
|
|
37606
|
+
return;
|
|
37607
|
+
continue;
|
|
37608
|
+
}
|
|
37551
37609
|
const fileAnomalies = [];
|
|
37552
37610
|
if (await shouldProcessFileNode(
|
|
37553
37611
|
filePath,
|
|
@@ -27,6 +27,7 @@ export declare function setDirectoryScanFinished(value: boolean): void;
|
|
|
27
27
|
export declare let scanAnomalies: {
|
|
28
28
|
fileInfo: TFileInfo;
|
|
29
29
|
anomalies: string[];
|
|
30
|
+
errors?: string[];
|
|
30
31
|
}[];
|
|
31
32
|
export declare function setMappingWorkerOptions(opts: TMappingWorkerOptions): void;
|
|
32
33
|
export declare function setCustomUploader(uploader: TCustomUploader | undefined): void;
|
|
@@ -11,6 +11,15 @@ export type FileScanMsg = {
|
|
|
11
11
|
response: 'scanAnomalies';
|
|
12
12
|
fileInfo: TFileInfo;
|
|
13
13
|
anomalies: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Hard errors discovered during scanning (e.g. a file that cannot be
|
|
16
|
+
* read at all via the FileSystem API / fs.stat). Unlike `anomalies`
|
|
17
|
+
* (benign findings such as non-DICOM or too-small files), these are
|
|
18
|
+
* surfaced as errors so they are visible regardless of pass. The string
|
|
19
|
+
* MUST NOT contain the raw filename/path — that is carried only in
|
|
20
|
+
* `fileInfo` so it stays in the private (input) log.
|
|
21
|
+
*/
|
|
22
|
+
errors?: string[];
|
|
14
23
|
previousFileInfo?: {
|
|
15
24
|
size?: number;
|
|
16
25
|
mtime?: string;
|
|
@@ -55131,10 +55131,12 @@
|
|
|
55131
55131
|
catch (error) {
|
|
55132
55132
|
console.warn(`[dicom-curate] Could not parse ${fileInfo.name} as DICOM data:`, error);
|
|
55133
55133
|
const mapResults = {
|
|
55134
|
-
|
|
55135
|
-
errors
|
|
55136
|
-
|
|
55137
|
-
|
|
55134
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
55135
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
55136
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
55137
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
55138
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
55139
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
55138
55140
|
sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
55139
55141
|
fileInfo: {
|
|
55140
55142
|
name: fileInfo.name,
|
|
@@ -68860,6 +68862,17 @@
|
|
|
68860
68862
|
'thumbs.db',
|
|
68861
68863
|
'.ds_store',
|
|
68862
68864
|
];
|
|
68865
|
+
/**
|
|
68866
|
+
* Build a PHI-safe error message for a file that could not be read during
|
|
68867
|
+
* scanning. Intentionally omits the filename/path so the string is safe to
|
|
68868
|
+
* place in `errors` (which is shared between the private and server-bound
|
|
68869
|
+
* logs). The raw path/name is carried separately in `fileInfo` so it appears
|
|
68870
|
+
* only in the private (input) log.
|
|
68871
|
+
*/
|
|
68872
|
+
function safeReadErrorMessage(error) {
|
|
68873
|
+
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
68874
|
+
return `Unable to read file (filesystem error): ${detail}`;
|
|
68875
|
+
}
|
|
68863
68876
|
let keepScanning = true;
|
|
68864
68877
|
// Backpressure gate: when the main thread signals 'stop', the feeder
|
|
68865
68878
|
// awaits this promise before emitting the next file. 'resume' resolves it.
|
|
@@ -69242,9 +69255,42 @@
|
|
|
69242
69255
|
if (!keepScanning)
|
|
69243
69256
|
return;
|
|
69244
69257
|
if (entry.kind === 'file') {
|
|
69245
|
-
const file = await entry.getFile();
|
|
69246
69258
|
const key = `${prefix}/${entry.name}`;
|
|
69247
69259
|
const prev = previousIndex ? previousIndex[key] : undefined;
|
|
69260
|
+
let file;
|
|
69261
|
+
try {
|
|
69262
|
+
file = await entry.getFile();
|
|
69263
|
+
}
|
|
69264
|
+
catch (readError) {
|
|
69265
|
+
// A single file we cannot read (corrupted, locked, permission
|
|
69266
|
+
// revoked via the Chromium FileSystem API) must NOT abort the whole
|
|
69267
|
+
// scan. Report it as a hard error and continue. The error string is
|
|
69268
|
+
// PHI-safe (no filename); the raw path/name lives only in fileInfo,
|
|
69269
|
+
// which keeps it in the private (input) log.
|
|
69270
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
69271
|
+
totalDiscovered--;
|
|
69272
|
+
globalThis.postMessage({
|
|
69273
|
+
response: 'count',
|
|
69274
|
+
totalDiscovered,
|
|
69275
|
+
});
|
|
69276
|
+
}
|
|
69277
|
+
globalThis.postMessage({
|
|
69278
|
+
response: 'scanAnomalies',
|
|
69279
|
+
fileInfo: {
|
|
69280
|
+
path: prefix,
|
|
69281
|
+
name: entry.name,
|
|
69282
|
+
size: 0,
|
|
69283
|
+
kind: 'handle',
|
|
69284
|
+
fileHandle: entry,
|
|
69285
|
+
},
|
|
69286
|
+
anomalies: [],
|
|
69287
|
+
errors: [safeReadErrorMessage(readError)],
|
|
69288
|
+
previousFileInfo: prev,
|
|
69289
|
+
});
|
|
69290
|
+
if (!(await waitIfPaused()))
|
|
69291
|
+
return;
|
|
69292
|
+
continue;
|
|
69293
|
+
}
|
|
69248
69294
|
const fileAnomalies = [];
|
|
69249
69295
|
if (await shouldProcessFile(file, fileAnomalies, key)) {
|
|
69250
69296
|
globalThis.postMessage({
|
|
@@ -69362,9 +69408,41 @@
|
|
|
69362
69408
|
return;
|
|
69363
69409
|
if (entry.isFile()) {
|
|
69364
69410
|
const filePath = path.join(currentPath, entry.name);
|
|
69365
|
-
const stats = await fs.stat(filePath);
|
|
69366
69411
|
const key = `${prefix}/${entry.name}`;
|
|
69367
69412
|
const prev = previousIndex ? previousIndex[key] : undefined;
|
|
69413
|
+
let stats;
|
|
69414
|
+
try {
|
|
69415
|
+
stats = await fs.stat(filePath);
|
|
69416
|
+
}
|
|
69417
|
+
catch (readError) {
|
|
69418
|
+
// A single file we cannot stat (vanished, permission denied) must
|
|
69419
|
+
// NOT abort the whole scan. Report it as a hard error and continue.
|
|
69420
|
+
// The error string is PHI-safe (no path); the raw path lives only
|
|
69421
|
+
// in fileInfo, keeping it in the private (input) log.
|
|
69422
|
+
if (cheapFilterNameOnly(entry.name, key)) {
|
|
69423
|
+
totalDiscovered--;
|
|
69424
|
+
globalThis.postMessage({
|
|
69425
|
+
response: 'count',
|
|
69426
|
+
totalDiscovered,
|
|
69427
|
+
});
|
|
69428
|
+
}
|
|
69429
|
+
globalThis.postMessage({
|
|
69430
|
+
response: 'scanAnomalies',
|
|
69431
|
+
fileInfo: {
|
|
69432
|
+
path: prefix,
|
|
69433
|
+
name: entry.name,
|
|
69434
|
+
size: 0,
|
|
69435
|
+
kind: 'path',
|
|
69436
|
+
fullPath: filePath,
|
|
69437
|
+
},
|
|
69438
|
+
anomalies: [],
|
|
69439
|
+
errors: [safeReadErrorMessage(readError)],
|
|
69440
|
+
previousFileInfo: prev,
|
|
69441
|
+
});
|
|
69442
|
+
if (!(await waitIfPaused()))
|
|
69443
|
+
return;
|
|
69444
|
+
continue;
|
|
69445
|
+
}
|
|
69368
69446
|
const fileAnomalies = [];
|
|
69369
69447
|
if (await shouldProcessFileNode(filePath, entry.name, stats.size, fileAnomalies, key)) {
|
|
69370
69448
|
globalThis.postMessage({
|
|
@@ -126773,10 +126851,12 @@
|
|
|
126773
126851
|
catch (error) {
|
|
126774
126852
|
console.warn(`[dicom-curate] Could not parse ${fileInfo.name} as DICOM data:`, error);
|
|
126775
126853
|
const mapResults = {
|
|
126776
|
-
|
|
126777
|
-
errors
|
|
126778
|
-
|
|
126779
|
-
|
|
126854
|
+
// PHI-safe strings: the raw filename is intentionally omitted because
|
|
126855
|
+
// anomalies/errors are shared between the private (input) log and the
|
|
126856
|
+
// server-bound (output) log. The raw name/path is carried only in
|
|
126857
|
+
// fileInfo, keeping it in the private log. See #283.
|
|
126858
|
+
anomalies: [`Could not parse file as DICOM data`],
|
|
126859
|
+
errors: [`File is not a valid DICOM file or is corrupted`],
|
|
126780
126860
|
sourceInstanceUID: `invalid_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
126781
126861
|
fileInfo: {
|
|
126782
126862
|
name: fileInfo.name,
|
|
@@ -140785,10 +140865,39 @@
|
|
|
140785
140865
|
console.log('job is finished');
|
|
140786
140866
|
if (!mapResultsList)
|
|
140787
140867
|
mapResultsList = [];
|
|
140788
|
-
// Create individual mapResults entries for each scan
|
|
140789
|
-
//
|
|
140790
|
-
|
|
140791
|
-
|
|
140868
|
+
// Create individual mapResults entries for each scan finding.
|
|
140869
|
+
//
|
|
140870
|
+
// Two kinds of scan findings are surfaced differently:
|
|
140871
|
+
//
|
|
140872
|
+
// - Benign anomalies (non-DICOM, too-small, excluded filetypes): noise
|
|
140873
|
+
// during the first pass (form generation), so only emitted on the
|
|
140874
|
+
// write pass (!skipWrite), and they carry the input path in
|
|
140875
|
+
// `outputFilePath` as before.
|
|
140876
|
+
//
|
|
140877
|
+
// - Hard read errors (file could not be read at all): always surfaced,
|
|
140878
|
+
// in both passes, via `errors`. We deliberately OMIT `outputFilePath`
|
|
140879
|
+
// so the raw input path is not leaked into the server-bound log; the
|
|
140880
|
+
// raw path is retained in `fileInfo` for the private (input) log, and a
|
|
140881
|
+
// path-less trace is rendered in the server-bound log by the consumer.
|
|
140882
|
+
scanAnomalies.forEach(({ fileInfo, anomalies, errors }) => {
|
|
140883
|
+
const hasReadErrors = !!errors && errors.length > 0;
|
|
140884
|
+
if (hasReadErrors) {
|
|
140885
|
+
const scanErrorResult = {
|
|
140886
|
+
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
140887
|
+
// Intentionally no outputFilePath: an unread file has no
|
|
140888
|
+
// de-identified output path, and we must not place the raw input
|
|
140889
|
+
// path in the server-bound channel.
|
|
140890
|
+
mappings: {},
|
|
140891
|
+
anomalies: anomalies ?? [],
|
|
140892
|
+
errors,
|
|
140893
|
+
quarantine: {},
|
|
140894
|
+
fileInfo,
|
|
140895
|
+
};
|
|
140896
|
+
mapResultsList.push(scanErrorResult);
|
|
140897
|
+
return;
|
|
140898
|
+
}
|
|
140899
|
+
// Benign anomalies: only on the write pass.
|
|
140900
|
+
if (!mappingWorkerOptions.skipWrite && anomalies.length > 0) {
|
|
140792
140901
|
const scanAnomalyResult = {
|
|
140793
140902
|
sourceInstanceUID: `scan_${fileInfo.name.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
140794
140903
|
outputFilePath: `${fileInfo.path}/${fileInfo.name}`, // Use the actual file path
|
|
@@ -140797,10 +140906,9 @@
|
|
|
140797
140906
|
errors: [],
|
|
140798
140907
|
quarantine: {},
|
|
140799
140908
|
};
|
|
140800
|
-
// Add each scan anomaly result to the final results
|
|
140801
140909
|
mapResultsList.push(scanAnomalyResult);
|
|
140802
|
-
}
|
|
140803
|
-
}
|
|
140910
|
+
}
|
|
140911
|
+
});
|
|
140804
140912
|
progressCallback({
|
|
140805
140913
|
response: 'done',
|
|
140806
140914
|
mapResultsList: mapResultsList,
|
|
@@ -141086,8 +141194,8 @@
|
|
|
141086
141194
|
}
|
|
141087
141195
|
case 'scanAnomalies': {
|
|
141088
141196
|
// Handle scan anomalies separately - they don't go to processing
|
|
141089
|
-
const { fileInfo: anomalyFileInfo, anomalies } = event.data;
|
|
141090
|
-
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies });
|
|
141197
|
+
const { fileInfo: anomalyFileInfo, anomalies, errors } = event.data;
|
|
141198
|
+
scanAnomalies.push({ fileInfo: anomalyFileInfo, anomalies, errors });
|
|
141091
141199
|
break;
|
|
141092
141200
|
}
|
|
141093
141201
|
case 'count': {
|