Skip to content

Commit

Permalink
Merge pull request #91 from ReflectionsProjections/dev/aydan/add-batc…
Browse files Browse the repository at this point in the history
…h-resumes

Add batch download support for resumes
  • Loading branch information
AydanPirani authored Jul 9, 2024
2 parents ceb18eb + 8977e06 commit 0d17813
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 46 deletions.
114 changes: 68 additions & 46 deletions src/services/s3/s3-router.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,106 @@
import { Request, Response, Router } from "express";
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import RoleChecker from "../../middleware/role-checker";
import { s3ClientMiddleware } from "../../middleware/s3";
import { StatusCodes } from "http-status-codes";
import { Config } from "../../config";
import { Role } from "../auth/auth-models";

import { GetObjectCommand, S3 } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { S3 } from "@aws-sdk/client-s3";
import { getResumeUrl, postResumeUrl } from "./s3-utils";
import BatchResumeDownloadValidator from "./s3-schema";

const s3Router: Router = Router();

s3Router.get(
"/upload/",
RoleChecker([], false),
s3ClientMiddleware,
async (_req: Request, res: Response) => {
async (_req, res, next) => {
const payload = res.locals.payload;

const s3 = res.locals.s3 as S3;
const userId: string = payload.userId;

const { url, fields } = await createPresignedPost(s3, {
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
Conditions: [
["content-length-range", 0, Config.MAX_RESUME_SIZE_BYTES], // 6 MB max
],
Fields: {
success_action_status: "201",
"Content-Type": "application/pdf",
},
Expires: Config.RESUME_URL_EXPIRY_SECONDS,
});

return res.status(StatusCodes.OK).send({ url: url, fields: fields });
try {
const { url, fields } = await postResumeUrl(userId, s3);
return res.status(StatusCodes.OK).send({ url, fields });
} catch (error) {
next(error);
}
}
);

s3Router.get(
"/download/",
RoleChecker([Role.enum.USER], false),
RoleChecker([Role.Enum.USER], false),
s3ClientMiddleware,
async (_req: Request, res: Response) => {
async (_, res, next) => {
const payload = res.locals.payload;
const userId = payload.userId;

const s3 = res.locals.s3 as S3;
const userId: string = payload.userId;

const command = new GetObjectCommand({
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
});

const downloadUrl = await getSignedUrl(s3, command, {
expiresIn: Config.RESUME_URL_EXPIRY_SECONDS,
});

return res.status(StatusCodes.OK).send({ url: downloadUrl });
try {
const downloadUrl = await getResumeUrl(userId, s3);
return res.status(StatusCodes.OK).send({ url: downloadUrl });
} catch (error) {
next(error);
}
}
);

s3Router.get(
"/download/:USERID",
RoleChecker([Role.enum.STAFF], false),
"/download/user/:USERID",
RoleChecker([Role.Enum.STAFF, Role.Enum.CORPORATE], false),
s3ClientMiddleware,
async (req: Request, res: Response) => {
const userId: string = req.params.USERID;
async (req, res, next) => {
const userId = req.params.USERID;
const s3 = res.locals.s3 as S3;

const command = new GetObjectCommand({
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
});
try {
const downloadUrl = await getResumeUrl(userId, s3);
return res.status(StatusCodes.OK).send({ url: downloadUrl });
} catch (error) {
next(error);
}
}
);

const downloadUrl = await getSignedUrl(s3, command, {
expiresIn: Config.RESUME_URL_EXPIRY_SECONDS,
});
s3Router.get(
"/download/batch/",
RoleChecker([Role.Enum.STAFF, Role.Enum.CORPORATE], false),
s3ClientMiddleware,
async (req, res, next) => {
const s3 = res.locals.s3 as S3;

return res.status(StatusCodes.OK).send({ url: downloadUrl });
try {
const { userIds } = BatchResumeDownloadValidator.parse(req.body);

const batchDownloadPromises = userIds.map((userId) =>
getResumeUrl(userId, s3)
.then((url) => ({ userId, url: url }))
.catch(() => ({ userId, url: null }))
);

const batchDownloadResults = await Promise.allSettled(
batchDownloadPromises
);

const filteredUrls = batchDownloadResults.forEach((result) => {
if (result.status === "fulfilled") {
return result.value;
}
});

const errors = batchDownloadResults.filter(
(result) => result.status === "rejected"
).length;

return res
.status(StatusCodes.OK)
.send({ data: filteredUrls, errorCount: errors });
} catch (error) {
next(error);
}
}
);

Expand Down
7 changes: 7 additions & 0 deletions src/services/s3/s3-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

const BatchResumeDownloadValidator = z.object({
userIds: z.string().array(),
});

export default BatchResumeDownloadValidator;
32 changes: 32 additions & 0 deletions src/services/s3/s3-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GetObjectCommand, S3 } from "@aws-sdk/client-s3";
import Config from "../../config";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";

export async function postResumeUrl(userId: string, client: S3) {
const { url, fields } = await createPresignedPost(client, {
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
Conditions: [
["content-length-range", 0, Config.MAX_RESUME_SIZE_BYTES], // 6 MB max
],
Fields: {
success_action_status: "201",
"Content-Type": "application/pdf",
},
Expires: Config.RESUME_URL_EXPIRY_SECONDS,
});

return { url, fields };
}

export async function getResumeUrl(userId: string, client: S3) {
const command = new GetObjectCommand({
Bucket: Config.S3_BUCKET_NAME,
Key: `${userId}.pdf`,
});

return getSignedUrl(client, command, {
expiresIn: Config.RESUME_URL_EXPIRY_SECONDS,
});
}

0 comments on commit 0d17813

Please sign in to comment.