Skip to content

Commit 689ac41

Browse files
authored
feat: replace file attachment content by download link when retrieving submission (#202)
* feat: replace file attachment content by download link when retrieving submission * feat: updade Node.js example to work with new submission attachment download links
1 parent fdfc2dc commit 689ac41

10 files changed

Lines changed: 179 additions & 152 deletions

examples/nodejs/index.ts

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import readline from "node:readline";
2-
import filesystem from "node:fs/promises";
32
import type {
3+
Attachment,
44
FormSubmission,
55
FormSubmissionProblem,
66
PrivateApiKey,
@@ -10,6 +10,10 @@ import { GCFormsApiClient } from "./gcFormsApiClient.js";
1010
import { decryptFormSubmission } from "./formSubmissionDecrypter.js";
1111
import { verifyIntegrity } from "./formSubmissionIntegrityVerifier.js";
1212
import path from "node:path";
13+
import { createWriteStream } from "node:fs";
14+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
15+
import axios from "axios";
16+
import type { Readable } from "node:stream";
1317

1418
const IDENTITY_PROVIDER_URL = "https://auth.forms-formulaires.alpha.canada.ca";
1519
const PROJECT_IDENTIFIER = "284778202772022819";
@@ -106,7 +110,9 @@ async function runRetrieveDecryptAndConfirmFormSubmissions(
106110
const endGetFormSubmissionTimer = performance.now();
107111

108112
console.info(
109-
`\nForm submission retrieved in ${Math.round(endGetFormSubmissionTimer - startGetFormSubmissionTimer)} ms`,
113+
`\nForm submission retrieved in ${Math.round(
114+
endGetFormSubmissionTimer - startGetFormSubmissionTimer,
115+
)} ms`,
110116
);
111117

112118
console.info("\nEncrypted submission:");
@@ -134,7 +140,9 @@ async function runRetrieveDecryptAndConfirmFormSubmissions(
134140
);
135141

136142
console.info(
137-
`\nIntegrity verification result: ${integrityVerificationResult ? "OK" : "INVALID"}`,
143+
`\nIntegrity verification result: ${
144+
integrityVerificationResult ? "OK" : "INVALID"
145+
}`,
138146
);
139147

140148
await saveSubmissionLocally(newFormSubmission.name, formSubmission);
@@ -165,36 +173,61 @@ async function saveSubmissionLocally(
165173

166174
const submissionFolderPath = path.join("./", submissionName);
167175

168-
await filesystem.mkdir(submissionFolderPath, {
176+
await mkdir(submissionFolderPath, {
169177
recursive: true,
170178
});
171179

172-
await filesystem.writeFile(
173-
path.join(`./${submissionName}`, "answers.json"),
180+
await writeFile(
181+
path.join(submissionFolderPath, "answers.json"),
174182
submission.answers,
175183
);
176184

177185
if (submission.attachments) {
178186
console.info("\nSaving submission attachments...\n");
179187

180-
for (const attachment of submission.attachments) {
181-
const attachmentBuffer = Buffer.from(
182-
attachment.base64EncodedContent,
183-
"base64",
184-
);
188+
await Promise.all(
189+
submission.attachments.map((attachment) =>
190+
downloadAndSaveAttachment(attachment, submissionFolderPath),
191+
),
192+
);
193+
}
194+
195+
console.info(`\nSubmission saved in folder named '${submissionFolderPath}'`);
196+
}
185197

186-
await filesystem.writeFile(
187-
path.join(`./${submissionName}`, attachment.name),
188-
attachmentBuffer,
198+
async function downloadAndSaveAttachment(
199+
attachment: Attachment,
200+
submissionFolderPath: string,
201+
): Promise<void> {
202+
return axios
203+
.get<Readable>(attachment.downloadLink, { responseType: "stream" })
204+
.then((response) => {
205+
const writableStream = createWriteStream(
206+
path.join(submissionFolderPath, attachment.name),
189207
);
190208

209+
return new Promise<void>((resolve, reject) => {
210+
writableStream.on("finish", resolve);
211+
writableStream.on("error", reject);
212+
213+
response.data.pipe(writableStream);
214+
});
215+
})
216+
.then(() => {
191217
console.info(
192-
`Submission attachment '${attachment.name}' has been saved ${attachment.isPotentiallyMalicious ? "(flagged as potentially malicious)" : ""}`,
218+
`Submission attachment '${attachment.name}' has been saved ${
219+
attachment.isPotentiallyMalicious
220+
? "(flagged as potentially malicious)"
221+
: ""
222+
}`,
193223
);
194-
}
195-
}
196-
197-
console.info(`\nSubmission saved in folder named '${submissionFolderPath}'`);
224+
})
225+
.catch((error) => {
226+
throw new Error(
227+
`Failed to download and save submission attachment ${attachment.name}`,
228+
{ cause: error },
229+
);
230+
});
198231
}
199232

200233
async function runReportProblemWithFormSubmission(
@@ -240,8 +273,7 @@ async function runReportProblemWithFormSubmission(
240273
}
241274

242275
function loadPrivateApiKey(): Promise<PrivateApiKey> {
243-
return filesystem
244-
.readdir(".")
276+
return readdir(".")
245277
.then((files) => {
246278
const validFiles = files.filter((fileName) =>
247279
fileName.endsWith("_private_api_key.json"),
@@ -256,7 +288,7 @@ function loadPrivateApiKey(): Promise<PrivateApiKey> {
256288
return validFiles[0];
257289
})
258290
.then((privateApiKeyFileName) => {
259-
return filesystem.readFile(`./${privateApiKeyFileName}`, {
291+
return readFile(`./${privateApiKeyFileName}`, {
260292
encoding: "utf8",
261293
});
262294
})

examples/nodejs/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export enum FormSubmissionStatus {
2626

2727
export type Attachment = {
2828
name: string;
29-
base64EncodedContent: string;
29+
downloadLink: string;
3030
isPotentiallyMalicious: boolean;
3131
};
3232

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@aws-sdk/client-secrets-manager": "^3.823.0",
2525
"@aws-sdk/client-sqs": "^3.823.0",
2626
"@aws-sdk/lib-dynamodb": "^3.823.0",
27+
"@aws-sdk/s3-request-presigner": "^3.859.0",
2728
"cors": "^2.8.5",
2829
"dotenv": "^16.4.5",
2930
"express": "^4.21.0",
@@ -38,7 +39,6 @@
3839
"devDependencies": {
3940
"@aws-sdk/types": "^3.662.0",
4041
"@biomejs/biome": "^1.9.0",
41-
"@smithy/util-stream": "^4.2.2",
4242
"@types/cors": "^2.8.18",
4343
"@types/express": "^4.17.21",
4444
"@types/node": "^22.7.4",

pnpm-lock.yaml

Lines changed: 29 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/vault/getFormSubmissionAttachmentContent.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { AwsServicesConnector } from "@lib/integration/awsServicesConnector.js";
2+
import { GetObjectCommand } from "@aws-sdk/client-s3";
3+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4+
import { VAULT_FILE_STORAGE_BUCKET_NAME } from "@config";
5+
import { logMessage } from "@lib/logging/logger.js";
6+
7+
export function getFormSubmissionAttachmentDownloadLink(
8+
path: string,
9+
): Promise<string> {
10+
return getSignedUrl(
11+
AwsServicesConnector.getInstance().s3Client,
12+
new GetObjectCommand({
13+
Bucket: VAULT_FILE_STORAGE_BUCKET_NAME,
14+
Key: path,
15+
}),
16+
{ expiresIn: 10 }, // 10 seconds
17+
).catch((error) => {
18+
logMessage.info(
19+
error,
20+
`[s3] Failed to generate form submission attachment download link. Path: ${path}.`,
21+
);
22+
23+
throw error;
24+
});
25+
}

src/operations/retrieveSubmission.v1.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import { encryptResponse } from "@lib/encryption/encryptResponse.js";
44
import { auditLog } from "@lib/logging/auditLogs.js";
55
import type { ApiOperation } from "@operations/types/operation.js";
66
import { FormSubmissionNotFoundException } from "@lib/vault/types/exceptions.types.js";
7-
import { getFormSubmissionAttachmentContent } from "@lib/vault/getFormSubmissionAttachmentContent.js";
7+
import { getFormSubmissionAttachmentDownloadLink } from "@lib/vault/getFormSubmissionAttachmentDownloadLink.js";
88
import { getPublicKey } from "@lib/formsClient/getPublicKey.js";
99
import {
1010
AttachmentScanStatus,
1111
type FormSubmission,
1212
type PartialAttachment,
1313
} from "@lib/vault/types/formSubmission.types.js";
1414

15-
type CompleteFormSubmissionAttachment = PartialAttachment & {
16-
base64EncodedContent: string;
15+
type CompleteAttachment = PartialAttachment & {
16+
downloadLink: string;
1717
};
1818

1919
async function v1(
@@ -70,21 +70,21 @@ async function v1(
7070

7171
function getCompleteFormSubmissionAttachments(
7272
partialAttachments: PartialAttachment[],
73-
): Promise<CompleteFormSubmissionAttachment[]> {
73+
): Promise<CompleteAttachment[]> {
7474
return Promise.all(
7575
partialAttachments.map((partialAttachment) => {
76-
return getFormSubmissionAttachmentContent(partialAttachment.path).then(
77-
({ base64EncodedContent }) => {
78-
return { ...partialAttachment, base64EncodedContent };
79-
},
80-
);
76+
return getFormSubmissionAttachmentDownloadLink(
77+
partialAttachment.path,
78+
).then((downloadLink) => {
79+
return { ...partialAttachment, downloadLink };
80+
});
8181
}),
8282
);
8383
}
8484

8585
function buildJsonResponse(
8686
formSubmission: FormSubmission,
87-
attachments: CompleteFormSubmissionAttachment[] | undefined,
87+
attachments: CompleteAttachment[] | undefined,
8888
): string {
8989
return JSON.stringify({
9090
createdAt: formSubmission.createdAt,
@@ -95,7 +95,7 @@ function buildJsonResponse(
9595
...(attachments && {
9696
attachments: attachments.map((attachment) => ({
9797
name: attachment.name,
98-
base64EncodedContent: attachment.base64EncodedContent,
98+
downloadLink: attachment.downloadLink,
9999
isPotentiallyMalicious: isAttachmentPotentiallyMalicious(
100100
attachment.scanStatus,
101101
),

0 commit comments

Comments
 (0)