Skip to content
Open
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
40 changes: 30 additions & 10 deletions packages/libs/core/src/images/imageOptimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ export async function imageOptimizer(

const hash = getHash([CACHE_VERSION, href, width, quality, mimeType]);
const imagesDir = join("/tmp", "cache", "images"); // Use Lambda tmp directory
const imagesMetaDir = join("/tmp", "cache", "imageMeta");
const hashDir = join(imagesDir, hash);
const metaDir = join(imagesMetaDir, hash);
const now = Date.now();

if (fs.existsSync(hashDir)) {
Expand All @@ -223,8 +225,15 @@ export async function imageOptimizer(
const contentType = getContentType(extension);
const fsPath = join(hashDir, file);
if (now < expireAt) {
const meta = JSON.parse(
(await promises.readFile(join(metaDir, `${file}.json`))).toString()
);
if (!res.getHeader("Cache-Control")) {
res.setHeader("Cache-Control", "public, max-age=60");
if (meta.headers["Cache-Control"]) {
res.setHeader("Cache-Control", meta.headers["Cache-Control"]);
} else {
res.setHeader("Cache-Control", "public, max-age=60");
}
}
if (sendEtagResponse(req, res, etag)) {
return { finished: true };
Expand All @@ -243,6 +252,7 @@ export async function imageOptimizer(
let upstreamBuffer: Buffer | undefined;
let upstreamType: string | undefined;
let maxAge: number;
let cacheControl: string | undefined | null;

if (isAbsolute) {
const upstreamRes = await fetch(href);
Expand All @@ -256,12 +266,10 @@ export async function imageOptimizer(
res.statusCode = upstreamRes.status;
upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer());
upstreamType = upstreamRes.headers.get("Content-Type") ?? undefined;
maxAge = getMaxAge(upstreamRes.headers.get("Cache-Control") ?? undefined);
if (upstreamRes.headers.get("Cache-Control")) {
res.setHeader(
"Cache-Control",
upstreamRes.headers.get("Cache-Control") as string
);
cacheControl = upstreamRes.headers.get("Cache-Control");
maxAge = getMaxAge(cacheControl ?? undefined);
if (cacheControl) {
res.setHeader("Cache-Control", cacheControl as string);
}
} else {
let objectKey;
Expand All @@ -284,6 +292,7 @@ export async function imageOptimizer(

upstreamBuffer = response.body ?? Buffer.of();
upstreamType = response.contentType ?? undefined;
cacheControl = response.cacheControl;
maxAge = getMaxAge(response.cacheControl);

// If object response provides cache control header, use that
Expand Down Expand Up @@ -357,11 +366,22 @@ export async function imageOptimizer(
}

const optimizedBuffer = await transformer.toBuffer();
await promises.mkdir(hashDir, { recursive: true });
await Promise.all([
promises.mkdir(hashDir, { recursive: true }),
promises.mkdir(metaDir, { recursive: true })
]);
const extension = getExtension(contentType);
const etag = getHash([optimizedBuffer]);
const filename = join(hashDir, `${expireAt}.${etag}.${extension}`);
await promises.writeFile(filename, optimizedBuffer);
const fileName = `${expireAt}.${etag}.${extension}`;
const filePath = join(hashDir, fileName);
const metaFilename = join(metaDir, `${fileName}.json`);
await Promise.all([
promises.writeFile(filePath, optimizedBuffer),
promises.writeFile(
metaFilename,
JSON.stringify({ headers: { "Cache-Control": cacheControl } })
)
]);
sendResponse(req, res, contentType, optimizedBuffer);
} catch (error: any) {
console.error(
Expand Down
94 changes: 57 additions & 37 deletions packages/libs/core/tests/images/imageOptimizer.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import sharp from "sharp";
import { ImagesManifest } from "../../src";
import { ImagesManifest, PlatformClient } from "../../src";
import { imageOptimizer } from "../../src/images/imageOptimizer";
import imagesManifest from "./image-images-manifest.json";
import fs from "fs";
import url from "url";
import http from "http";
import Stream from "stream";
import { PlatformClient } from "../../src";
import { jest } from "@jest/globals";

jest.mock("node-fetch", () => require("fetch-mock-jest").sandbox());
Expand Down Expand Up @@ -103,7 +103,7 @@ describe("Image optimizer", () => {
);
};

beforeEach(async () => {
const setupPlatformClientResponse = async (cacheControlHeader?: string) => {
const imageBuffer: Buffer = await sharp({
create: {
width: 100,
Expand All @@ -122,9 +122,14 @@ describe("Image optimizer", () => {
expires: undefined,
eTag: "etag",
statusCode: 200,
cacheControl: undefined,
cacheControl: cacheControlHeader,
contentType: "image/png"
});
};

beforeEach(() => {
fs.rmSync("/tmp/cache/images", { recursive: true, force: true });
fs.rmSync("/tmp/cache/imageMeta", { recursive: true, force: true });
});

describe("Routes", () => {
Expand All @@ -137,6 +142,7 @@ describe("Image optimizer", () => {
`(
"serves image request",
async ({ imagePath, accept, expectedObjectKey }) => {
await setupPlatformClientResponse();
const { parsedUrl, req, res } = createEventByImagePath(imagePath, {
accept: accept
});
Expand All @@ -162,47 +168,61 @@ describe("Image optimizer", () => {
);

it.each`
imagePath
${"/test-image-cached.png"}
`("serves cached image on second request", async ({ imagePath }) => {
const {
parsedUrl: parsedUrl1,
req: req1,
res: res1
} = createEventByImagePath(imagePath);
const {
parsedUrl: parsedUrl2,
req: req2,
res: res2
} = createEventByImagePath(imagePath);
imagePath | cacheControlHeader
${"/test-image-cached.png"} | ${undefined}
${"/test-image-cached.png"} | ${"public,max-age=31536000,immutable"}
`(
"serves cached image on second request with $cacheControlHeader cache header",
async ({ imagePath, cacheControlHeader }) => {
await setupPlatformClientResponse(cacheControlHeader);
const {
parsedUrl: parsedUrl1,
req: req1,
res: res1
} = createEventByImagePath(imagePath);
const {
parsedUrl: parsedUrl2,
req: req2,
res: res2
} = createEventByImagePath(imagePath);

await imageOptimizer(
"",
imagesManifest as ImagesManifest,
req1,
res1,
parsedUrl1,
mockPlatformClient as PlatformClient
);
await imageOptimizer(
"",
imagesManifest as ImagesManifest,
req2,
res2,
parsedUrl2,
mockPlatformClient as PlatformClient
);
await imageOptimizer(
"",
imagesManifest as ImagesManifest,
req1,
res1,
parsedUrl1,
mockPlatformClient as PlatformClient
);
await imageOptimizer(
"",
imagesManifest as ImagesManifest,
req2,
res2,
parsedUrl2,
mockPlatformClient as PlatformClient
);

expect(res1.statusCode).toEqual(200);
expect(res2.statusCode).toEqual(200);
expect(res1.statusCode).toEqual(200);
expect(res2.statusCode).toEqual(200);

expect(mockPlatformClient.getObject).toBeCalledTimes(1);
});
let defaultCacheHeader = "public, max-age=60";
expect(res1.headers["cache-control"]).toEqual(
cacheControlHeader ?? defaultCacheHeader
);
expect(res2.headers["cache-control"]).toEqual(
cacheControlHeader ?? defaultCacheHeader
);

expect(mockPlatformClient.getObject).toBeCalledTimes(1);
}
);

it.each`
imagePath
${"/test-image-etag.png"}
`("serves 304 when etag matches", async ({ imagePath }) => {
await setupPlatformClientResponse();
const {
parsedUrl: parsedUrl1,
req: req1,
Expand Down