Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: serving STFC PDFs from Postgres #779

Merged
merged 1 commit into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 44 additions & 15 deletions apps/backend/src/datasources/postgres/FileDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,25 +200,54 @@ export default class PostgresFileDataSource implements FileDataSource {
}

private async retrieveBlobData(oid: number): Promise<ReadStream | null> {
const [connectionError, connection] = await to(
database.client.acquireConnection() as Promise<Client | undefined>
);
if (connectionError || !connection) {
return null;
}
return new Promise(async (resolve, reject) => {
const [connectionError, connection] = await to(
database.client.acquireConnection() as Promise<Client | undefined>
);
if (connectionError || !connection) {
return reject(
`Error ocurred while establishing connection with database \n ${connectionError} ${connection}`
);
}

const [transactionError] = await to(connection.query('BEGIN')); // start the transaction
if (transactionError) {
database.client.releaseConnection(connection);
const [transactionError] = await to(connection.query('BEGIN'));
if (transactionError) {
database.client.releaseConnection(connection);

return null;
}
const blobManager = new LargeObjectManager({ pg: connection });
return reject(`Could not begin transaction \n${transactionError}`);
}

const blobManager = new LargeObjectManager({ pg: connection });
const [streamErr, response] = await to(
blobManager.openAndReadableStreamAsync(oid)
);

if (streamErr || !response) {
await connection.query('ROLLBACK');
database.client.releaseConnection(connection);

return reject(
`Could not create readable stream \n${streamErr} ${response}`
);
}

const [, stream] = response;

stream.on('error', async (streamError) => {
await connection.query('ROLLBACK');
database.client.releaseConnection(connection);

const response = await blobManager.openAndReadableStreamAsync(oid);
reject(`Stream error: ${streamError}`);
});

const [, stream] = response;
stream.on('end', async function () {
await connection.query('COMMIT');
database.client.releaseConnection(connection);

return stream;
resolve(stream);
});

resolve(stream);
});
}
}
75 changes: 68 additions & 7 deletions apps/backend/src/factory/StfcDownloadService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import querystring from 'querystring';

import { logger } from '@user-office-software/duo-logger';
import contentDisposition from 'content-disposition';
import { Request, Response, NextFunction } from 'express';
import { container } from 'tsyringe';
Expand Down Expand Up @@ -51,33 +52,92 @@ export class StfcDownloadService implements DownloadService {
const fileDataSource = container.resolve<FileDataSource>(
Tokens.FileDataSource
);
proposalPdfData = await fileDataSource.getBlobdata(
`${facility}-${data.proposal.proposalId}.pdf`
);

fileName = `${facility}-${data.proposal.proposalId}.pdf`;

const loggingContext = {
message: `Failed to download proposal PDF ${data.proposal.proposalId} from Postgres storage`,
proposalId: data.proposal.proposalId,
callId: call?.id,
callShortCode: call?.shortCode,
facilityIdentifiedAs: facility,
storedFileName: fileName,
};

try {
proposalPdfData = await fileDataSource.getBlobdata(fileName);
} catch (error) {
next({
error: error,
...loggingContext,
});
}

if (proposalPdfData) {
isPdfAvailable = true;
fileName = `${data.proposal.proposalId}_${
data.principalInvestigator.lastname
}_${data.proposal.created.getUTCFullYear()}.pdf`;

proposalPdfData.on('error', (streamError) => {
next({
error: streamError,
...loggingContext,
formattedFilename: fileName,
});
});

isPdfAvailable = true;
} else {
// This is a reasonable case, so don't log an error
logger.logInfo(
`Proposal PDF for ${data.proposal.proposalId} not found in Postgres storage, generating via Factory instead`,
{
...loggingContext,
}
);
}
}
}
}

if (isPdfAvailable && proposalPdfData && fileName != '') {
const proposal = (properties?.data[0] as ProposalPDFData)?.proposal;

try {
res.setHeader('Content-Disposition', contentDisposition(fileName));
res.setHeader('x-download-filename', querystring.escape(fileName));
res.setHeader('content-type', 'application/pdf');

res.on('finish', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
logger.logInfo(
`Succesfully sent proposal PDF ${proposal.proposalId} from Postgres storage`,
{
proposalId: proposal.proposalId,
call: proposal.callId,
storedFileName: fileName,
statusCode: res.statusCode,
}
);
} else {
next({
message: `Failed to send proposal PDF ${proposal.proposalId} from Postgres storage`,
proposalId: proposal.proposalId,
call: proposal.callId,
storedFileName: fileName,
statusCode: res.statusCode,
});
}
});

proposalPdfData.pipe(res);
await new Promise((f) => setTimeout(f, 1000));
proposalPdfData.emit('end');
} catch (error) {
next({
error,
message: 'Could not generate proposal pdf',
message: `Failed to send proposal PDF ${proposal.proposalId} from Postgres storage`,
proposalId: proposal.proposalId,
call: proposal.callId,
storedFileName: fileName,
});
}

Expand All @@ -87,6 +147,7 @@ export class StfcDownloadService implements DownloadService {
fetchDataAndStreamResponse(downloadType, type, properties, req, res, next);
}
}

function getFacilityName(shortCode: string | undefined) {
let facility = null;
if (!shortCode) {
Expand Down
Loading