Skip to content

Commit 0013fc8

Browse files
committed
Download attachments smart-on-fhir#11
1 parent 28d6a87 commit 0013fc8

File tree

8 files changed

+291
-45
lines changed

8 files changed

+291
-45
lines changed

built/app.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ APP.option('-d, --destination [destination]', 'Download destination. See config/
3737
APP.option("--config <path>", 'Relative path to config file.');
3838
APP.option("--reporter [cli|text]", 'Reporter to use to render the output. "cli" renders fancy progress bars and tables. "text" is better for log files. Defaults to "cli".');
3939
APP.option("-c, --custom [opt=val...]", "Custom parameters to be passed to the kick-off endpoint.");
40+
APP.option("--status [url]", "Status endpoint of already started export.");
4041
APP.action(async (args) => {
4142
const { config, ...params } = args;
4243
const defaultsPath = (0, path_1.resolve)(__dirname, "../config/defaults.js");
@@ -178,7 +179,7 @@ APP.action(async (args) => {
178179
console.error(error);
179180
process.exit(1);
180181
});
181-
const statusEndpoint = await client.kickOff();
182+
const statusEndpoint = options.status || await client.kickOff();
182183
const manifest = await client.waitForExport(statusEndpoint);
183184
const downloads = await client.downloadAllFiles(manifest);
184185
// console.log(downloads)

built/lib/BulkDataClient.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
2323
};
2424
Object.defineProperty(exports, "__esModule", { value: true });
2525
const util_1 = require("util");
26+
const http_1 = __importDefault(require("http"));
2627
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
2728
const node_jose_1 = __importDefault(require("node-jose"));
2829
const url_1 = require("url");
@@ -337,14 +338,16 @@ class BulkDataClient extends events_1.EventEmitter {
337338
return (0, utils_1.wait)(poolDelay, this.abortController.signal).then(checkStatus);
338339
}
339340
else {
341+
const msg = `Unexpected status response ${res.statusCode} ${res.statusMessage}`;
340342
this.emit("exportError", {
341343
body: res.body || null,
342344
code: res.statusCode || null,
343-
message: `Unexpected status response ${res.statusCode} ${res.statusMessage}`
345+
message: msg
344346
});
345-
// TODO: handle unexpected response
346-
throw new Error(`Unexpected status response ${res.statusCode} ${res.statusMessage}`);
347-
// this.emit("error", status)
347+
const error = new Error(msg);
348+
// @ts-ignore
349+
error.body = res.body || null;
350+
throw error;
348351
}
349352
});
350353
};
@@ -621,7 +624,15 @@ class BulkDataClient extends events_1.EventEmitter {
621624
}
622625
// HTTP ----------------------------------------------------------------
623626
if (destination.match(/^https?\:\/\//)) {
624-
return request_1.default.stream.post((0, path_1.join)(destination, fileName) + "?folder=" + subFolder);
627+
const url = new url_1.URL((0, path_1.join)(destination, fileName));
628+
if (subFolder) {
629+
url.searchParams.set("folder", subFolder);
630+
}
631+
const req = http_1.default.request(url, { method: 'POST' });
632+
req.on('error', error => {
633+
console.error(`Problem with upload request: ${error.message}`);
634+
});
635+
return req;
625636
}
626637
// local filesystem destinations ---------------------------------------
627638
let path = destination.startsWith("file://") ?

built/reporters/cli.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function CLIReporter(client) {
1515
}
1616
function onExportStart(status) {
1717
console.log(status.message);
18+
console.log(`Status endpoint: ${status.statusEndpoint}`);
1819
}
1920
function onExportProgress(status) {
2021
(0, utils_1.print)(status.message);

built/reporters/text.js

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ function TextReporter(client) {
1616
function onExportStart(status) {
1717
downloadedPct = 0;
1818
console.log(status.message);
19+
console.log(`Status endpoint: ${status.statusEndpoint}`);
1920
}
2021
function onExportProgress(status) {
2122
console.log(status.message);

built/streams/DocumentReferenceHandler.js

+40-16
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,49 @@ class DocumentReferenceHandler extends stream_1.Transform {
3030
});
3131
this.options = options;
3232
}
33-
async downloadAttachment(url) {
34-
if (url.search(/^https?:\/\/.+/) === -1) {
35-
url = new url_1.URL(url, this.options.baseUrl).href;
33+
async downloadAttachment(attachment) {
34+
if (!attachment.url) {
35+
throw new Error("DocumentReferenceHandler.downloadAttachment called on attachment that has no 'url'");
3636
}
37+
// If the url is relative then convert it to absolute on the same base
38+
const url = new url_1.URL(attachment.url, this.options.baseUrl);
39+
3740
const res = await this.options.request({
3841
url,
3942
responseType: "buffer",
40-
throwHttpErrors: false
43+
throwHttpErrors: false,
44+
headers: {
45+
accept: attachment.contentType || "application/json+fhir"
46+
}
4147
});
48+
4249
if (res.statusCode >= 400) {
4350
throw new errors_1.FileDownloadError({
44-
fileUrl: url,
51+
fileUrl: attachment.url,
4552
body: null,
4653
code: res.statusCode
4754
});
4855
}
49-
this.options.onDownloadComplete(url, res.body.byteLength);
50-
return res;
56+
57+
const contentType = res.headers["content-type"] || "";
58+
59+
// We may have gotten back a Binary FHIR resource
60+
if (contentType.match(/\bapplication\/json(\+fhir)?\b/)) {
61+
const json = JSON.parse(res.body.toString("utf8"));
62+
const { resourceType, contentType, data } = json;
63+
if (resourceType === "Binary") {
64+
const buffer = Buffer.from(data, "base64");
65+
this.options.onDownloadComplete(attachment.url, buffer.byteLength);
66+
return { contentType, data: buffer };
67+
}
68+
}
69+
70+
this.options.onDownloadComplete(attachment.url, res.body.byteLength);
71+
72+
return {
73+
contentType: contentType || attachment.contentType || "",
74+
data: res.body
75+
};
5176
}
5277
async inlineAttachmentData(node, data) {
5378
if (node.contentType == "application/pdf" && this.options.pdfToText) {
@@ -65,28 +90,27 @@ class DocumentReferenceHandler extends stream_1.Transform {
6590
if (!attachment.url) {
6691
continue;
6792
}
68-
const response = await this.downloadAttachment(attachment.url);
69-
if (this.canPutAttachmentInline(response, attachment.contentType)) {
70-
await this.inlineAttachmentData(attachment, response.body);
93+
const response = await this.downloadAttachment(attachment);
94+
if (this.canPutAttachmentInline(response.data, response.contentType)) {
95+
await this.inlineAttachmentData(attachment, response.data);
7196
}
7297
else {
7398
const fileName = Date.now() + "-" + node_jose_1.default.util.randomBytes(6).toString("hex") + (0, path_1.extname)(attachment.url);
74-
await this.options.save(fileName, stream_1.Readable.from(response.body), "attachments");
99+
await this.options.save(fileName, stream_1.Readable.from(response.data), "attachments");
75100
attachment.url = `./attachments/${fileName}`;
76101
}
77102
this.emit("attachment");
78103
}
79104
return resource;
80105
}
81-
canPutAttachmentInline(response, contentType) {
82-
if (response.body.byteLength > this.options.inlineAttachments) {
106+
canPutAttachmentInline(data, contentType) {
107+
if (data.byteLength > this.options.inlineAttachments) {
83108
return false;
84109
}
85-
const type = contentType || response.headers["content-type"] || "";
86-
if (!type) {
110+
if (!contentType) {
87111
return false;
88112
}
89-
if (!this.options.inlineAttachmentTypes.find(m => type.startsWith(m))) {
113+
if (!this.options.inlineAttachmentTypes.find(m => contentType.startsWith(m))) {
90114
return false;
91115
}
92116
return true;

src/streams/DocumentReferenceHandler.ts

+39-18
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,51 @@ export default class DocumentReferenceHandler extends Transform
4545
this.options = options;
4646
}
4747

48-
private async downloadAttachment(url: string): Promise<Response<Buffer>> {
49-
if (url.search(/^https?:\/\/.+/) === -1) {
50-
url = new URL(url, this.options.baseUrl).href
48+
private async downloadAttachment(attachment: fhir4.Attachment): Promise<{ contentType: string; data: Buffer }> {
49+
50+
if (!attachment.url) {
51+
throw new Error("DocumentReferenceHandler.downloadAttachment called on attachment that has no 'url'")
5152
}
5253

54+
// If the url is relative then convert it to absolute on the same base
55+
const url = new URL(attachment.url, this.options.baseUrl)
56+
5357
const res = await this.options.request<Buffer>({
5458
url,
5559
responseType: "buffer",
56-
throwHttpErrors: false
57-
});
60+
throwHttpErrors: false,
61+
headers: {
62+
accept: attachment.contentType || "application/json+fhir"
63+
}
64+
})
5865

5966
if (res.statusCode >= 400) {
6067
throw new FileDownloadError({
61-
fileUrl: url,
68+
fileUrl: attachment.url,
6269
body : null,
6370
code : res.statusCode
6471
});
6572
}
6673

67-
this.options.onDownloadComplete(url, res.body.byteLength)
74+
const contentType = res.headers["content-type"] || "";
75+
76+
// We may have gotten back a Binary FHIR resource
77+
if (contentType.match(/\bapplication\/json(\+fhir)?\b/)) {
78+
const json = JSON.parse(res.body.toString("utf8"));
79+
const { resourceType, contentType, data } = json;
80+
if (resourceType === "Binary") {
81+
const buffer = Buffer.from(data, "base64")
82+
this.options.onDownloadComplete(attachment.url, buffer.byteLength)
83+
return { contentType, data: buffer }
84+
}
85+
}
86+
87+
this.options.onDownloadComplete(attachment.url, res.body.byteLength)
6888

69-
return res
89+
return {
90+
contentType: contentType || attachment.contentType || "",
91+
data : res.body
92+
}
7093
}
7194

7295
private async inlineAttachmentData(node: fhir4.Attachment, data: Buffer) {
@@ -89,17 +112,17 @@ export default class DocumentReferenceHandler extends Transform
89112
continue;
90113
}
91114

92-
const response = await this.downloadAttachment(attachment.url);
115+
const response = await this.downloadAttachment(attachment);
93116

94-
if (this.canPutAttachmentInline(response, attachment.contentType)) {
95-
await this.inlineAttachmentData(attachment, response.body);
117+
if (this.canPutAttachmentInline(response.data, response.contentType)) {
118+
await this.inlineAttachmentData(attachment, response.data);
96119
}
97120

98121
else {
99122
const fileName = Date.now() + "-" + jose.util.randomBytes(6).toString("hex") + extname(attachment.url);
100123
await this.options.save(
101124
fileName,
102-
Readable.from(response.body),
125+
Readable.from(response.data),
103126
"attachments"
104127
)
105128
attachment.url = `./attachments/${fileName}`
@@ -110,19 +133,17 @@ export default class DocumentReferenceHandler extends Transform
110133
return resource;
111134
}
112135

113-
canPutAttachmentInline(response: Response<Buffer>, contentType?: string): boolean
136+
canPutAttachmentInline(data: Buffer, contentType: string): boolean
114137
{
115-
if (response.body.byteLength > this.options.inlineAttachments) {
138+
if (data.byteLength > this.options.inlineAttachments) {
116139
return false
117140
}
118141

119-
const type = contentType || response.headers["content-type"] || ""
120-
121-
if (!type) {
142+
if (!contentType) {
122143
return false
123144
}
124145

125-
if (!this.options.inlineAttachmentTypes.find(m => type.startsWith(m))) {
146+
if (!this.options.inlineAttachmentTypes.find(m => contentType.startsWith(m))) {
126147
return false
127148
}
128149

0 commit comments

Comments
 (0)