IO-3092 WIP on img proxy thumbnail generation.

This commit is contained in:
Patrick Fic
2025-02-05 11:03:29 -08:00
parent e8ee2a9416
commit 47fe1959b1
6 changed files with 489 additions and 41 deletions

View File

@@ -2252,7 +2252,7 @@ exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCriti
notcritical: update_joblines(where: {id: {_nin: $IdsToMarkCritical}, jobid: {_eq: $jobid}}, _set: {critical: false}) {
affected_rows
}
}`
}`;
exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) {
@@ -2618,7 +2618,6 @@ exports.CREATE_CONVERSATION = `mutation CREATE_CONVERSATION($conversation: [conv
}
`;
exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: timestamptz!) {
bodyshops(where: { created_at: { _gte: $period } }) {
shopname
@@ -2689,4 +2688,37 @@ exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: time
}
}
}
`
`;
exports.GET_DOCUMENTS_BY_JOB = `
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
jobs_by_pk(id: $jobId) {
id
ro_number
}
documents_aggregate(where: { jobid: { _eq: $jobId } }) {
aggregate {
sum {
size
}
}
}
documents(order_by: { takenat: desc }, where: { jobid: { _eq: $jobId } }) {
id
name
key
type
size
takenat
extension
bill {
id
invoice_number
date
vendor {
id
name
}
}
}
}`;

View File

@@ -3,13 +3,16 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const logger = require("../utils/logger");
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
const { S3Client, PutObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const crypto = require("crypto");
const { InstanceRegion } = require("../utils/instanceMgr");
const { GET_DOCUMENTS_BY_JOB } = require("../graphql-client/queries");
//TODO: Remove hardcoded values.
const imgproxyBaseUrl = process.env.IMGPROXY_BASE_URL || `https://d3ictiiutovkvi.cloudfront.net`;
const imgproxyBaseUrl =
process.env.IMGPROXY_BASE_URL ||
// `https://k2car6fha7w5cbgry3j2td56ra0kdmwn.lambda-url.ca-central-1.on.aws` ||
`https://d3ictiiutovkvi.cloudfront.net`;
const imgproxyKey = process.env.IMGPROXY_KEY || `secret`;
const imgproxySalt = process.env.IMGPROXY_SALT || `salt`;
const imgproxyDestinationBucket = process.env.IMGPROXY_DESTINATION_BUCKET || `imex-shop-media`;
@@ -36,7 +39,7 @@ exports.generateSignedUploadUrls = async (req, res) => {
const client = new S3Client({ region: InstanceRegion() });
const command = new PutObjectCommand({ Bucket: imgproxyDestinationBucket, Key: key });
const presignedUrl = await getSignedUrl(client, command, { expiresIn: 360 });
signedUrls.push({ filename, presignedUrl });
signedUrls.push({ filename, presignedUrl, key });
}
logger.log("imgproxy-upload-success", "DEBUG", req.user?.email, jobid, { signedUrls });
@@ -58,30 +61,25 @@ exports.generateSignedUploadUrls = async (req, res) => {
};
exports.getThumbnailUrls = async (req, res) => {
const { jobid } = req.body;
const { jobid, billid } = req.body;
try {
//TODO: Query for all documents related to the job.
//Delayed as the key structure may change slightly from what it is currently and will require evaluating mobile components.
// const { data } = await client.query({
// query: queries.GET_DOCUMENTS_BY_JOBID,
// variables: { jobid }
// });
//Mocked Keys.
const keys = [
"shopid/jobid/test2.jpg-1737502469411",
"shopid/jobid/test2.jpg-1737502469411",
"shopid/jobid/movie.mov-1737504997897",
"shopid/jobid/pdf.pdf-1737504944260"
];
const client = req.userGraphQLClient;
const data = await client.request(GET_DOCUMENTS_BY_JOB, { jobId: jobid });
const thumbResizeParams = `rs:fill:250:250:1/g:ce`;
const proxiedUrls = keys.map((key) => {
const s3client = new S3Client({ region: InstanceRegion() });
const proxiedUrls = [];
for (const document of data.documents) {
//Format to follow:
//<Cloudfront_to_lambdal>/<hmac with SHA of entire request URI path (with base64 encoded URL if needed), beginning with unencoded/unhashed Salt>/<remainder of url - resize params >/< base 64 URL encoded to image path>
// Build the S3 path to the object.
const fullS3Path = `s3://${imgproxyDestinationBucket}/${key}`;
const fullS3Path = `s3://${imgproxyDestinationBucket}/${document.key}`;
const base64UrlEncodedKeyString = base64UrlEncode(fullS3Path);
//Thumbnail Generation Block
const thumbProxyPath = `${thumbResizeParams}/${base64UrlEncodedKeyString}`;
@@ -92,15 +90,30 @@ exports.getThumbnailUrls = async (req, res) => {
const fullSizeProxyPath = `${base64UrlEncodedKeyString}`;
const fullSizeHmacSalt = createHmacSha256(`${imgproxySalt}/${fullSizeProxyPath}`);
//If not a picture, we need to get a signed download link to the file using S3 (or cloudfront preferably)
const s3Props = {};
if (!document.type.startsWith("image")) {
//If not a picture, we need to get a signed download link to the file using S3 (or cloudfront preferably)
const command = new GetObjectCommand({ Bucket: imgproxyDestinationBucket, Key: document.key });
const presignedGetUrl = await getSignedUrl(s3client, command, { expiresIn: 360 });
s3Props.presignedGetUrl = presignedGetUrl;
return {
const originalProxyPath = `raw:1/${base64UrlEncodedKeyString}`;
const originalHmacSalt = createHmacSha256(`${imgproxySalt}/${originalProxyPath}`);
s3Props.originalUrlViaProxyPath = `${imgproxyBaseUrl}/${originalHmacSalt}/${originalProxyPath}`;
}
proxiedUrls.push({
originalUrl: `${imgproxyBaseUrl}/${fullSizeHmacSalt}/${fullSizeProxyPath}`,
thumbnailUrl: `${imgproxyBaseUrl}/${thumbHmacSalt}/${thumbProxyPath}`
};
});
thumbnailUrl: `${imgproxyBaseUrl}/${thumbHmacSalt}/${thumbProxyPath}`,
fullS3Path,
base64UrlEncodedKeyString,
thumbProxyPath,
...s3Props,
...document
});
}
res.json({ proxiedUrls });
res.json(proxiedUrls);
//Iterate over them, build the link based on the media type, and return the array.
} catch (error) {
logger.log("imgproxy-get-proxied-urls-error", "ERROR", req.user?.email, jobid, {
@@ -124,8 +137,12 @@ exports.deleteFiles = async (req, res) => {
//Mark as deleted from the documents section of the database.
};
//Gerneate a key for the s3 bucket by popping off the extension, add a timestamp, and add back the extension.
//This is to prevent any collisions/duplicates in the bucket.
function GenerateKey({ bodyshopid, jobid, filename }) {
return `${bodyshopid}/${jobid}/${filename}-${Date.now()}`;
let nameArray = filename.split(".");
let extension = nameArray.pop();
return `${bodyshopid}/${jobid}/${nameArray.join(".")}-${Date.now()}.${extension}`;
}
function base64UrlEncode(str) {

View File

@@ -1,13 +1,12 @@
const express = require("express");
const router = express.Router();
const { createSignedUploadURL, downloadFiles, renameKeys, deleteFiles } = require("../media/media");
const {
generateSignedUploadUrls,
getThumbnailUrls
} = require("../media/imgprox-media");
const { generateSignedUploadUrls, getThumbnailUrls } = require("../media/imgprox-media");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.use(withUserGraphQLClientMiddleware);
router.post("/sign", createSignedUploadURL);
router.post("/download", downloadFiles);
@@ -15,6 +14,6 @@ router.post("/rename", renameKeys);
router.post("/delete", deleteFiles);
router.post("/proxy/sign", generateSignedUploadUrls);
router.post("/proxy/get", getThumbnailUrls);
router.post("/proxy/thumbnails", getThumbnailUrls);
module.exports = router;