Skip to content
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
2 changes: 2 additions & 0 deletions lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ declare module NodeJS {
TF_NEXTIMAGE_DOMAINS?: string;
TF_NEXTIMAGE_DEVICE_SIZES?: string;
TF_NEXTIMAGE_IMAGE_SIZES?: string;
TF_NEXTIMAGE_SOURCE_BUCKET?: string;
__DEBUG__USE_LOCAL_BUCKET?: string;
}
}
34 changes: 32 additions & 2 deletions lib/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,31 @@ import {
APIGatewayProxyStructuredResultV2,
} from 'aws-lambda';
import { Writable } from 'stream';
import S3 from 'aws-sdk/clients/s3';

import { imageOptimizer } from './image-optimizer';
import { imageOptimizer, S3Config } from './image-optimizer';
import { normalizeHeaders } from './normalized-headers';

function generateS3Config(bucketName?: string): S3Config | undefined {
let s3: S3;

if (!bucketName) {
return undefined;
}

// Only for testing purposes when connecting against a local S3 backend
if (process.env.__DEBUG__USE_LOCAL_BUCKET) {
s3 = new S3(JSON.parse(process.env.__DEBUG__USE_LOCAL_BUCKET));
} else {
s3 = new S3();
}

return {
s3,
bucket: bucketName,
};
}

function parseFromEnv<T>(key: string, defaultValue: T) {
try {
if (key in process.env) {
Expand All @@ -38,6 +59,7 @@ const imageSizes = parseFromEnv(
'TF_NEXTIMAGE_IMAGE_SIZES',
imageConfigDefault.deviceSizes
);
const sourceBucket = process.env.TF_NEXTIMAGE_SOURCE_BUCKET ?? undefined;

const imageConfig: ImageConfig = {
...imageConfigDefault,
Expand All @@ -49,6 +71,8 @@ const imageConfig: ImageConfig = {
export async function handler(
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyStructuredResultV2> {
const s3Config = generateS3Config(sourceBucket);

const reqMock: any = {
headers: normalizeHeaders(event.headers),
method: event.requestContext.http.method,
Expand All @@ -75,7 +99,13 @@ export async function handler(
resMock._implicitHeader = () => {};

const parsedUrl = parseUrl(reqMock.url, true);
const result = await imageOptimizer(imageConfig, reqMock, resMock, parsedUrl);
const result = await imageOptimizer(
imageConfig,
reqMock,
resMock,
parsedUrl,
s3Config
);

const normalizedHeaders: Record<string, string> = {};
for (const [headerKey, headerValue] of Object.entries(mockHeaders)) {
Expand Down
45 changes: 38 additions & 7 deletions lib/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ImageConfig } from 'next/dist/next-server/server/image-config';
import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch';
import { UrlWithParsedQuery } from 'url';
import Server from 'next/dist/next-server/server/next-server';
import S3 from 'aws-sdk/clients/s3';

let originCacheControl: string | null;

Expand All @@ -21,11 +22,17 @@ async function fetchPolyfill(url: RequestInfo, init?: RequestInit) {
// Polyfill fetch used by nextImageOptimizer
global.fetch = fetchPolyfill;

interface S3Config {
s3: S3;
bucket: string;
}

async function imageOptimizer(
imageConfig: ImageConfig,
req: IncomingMessage,
res: ServerResponse,
parsedUrl: UrlWithParsedQuery
parsedUrl: UrlWithParsedQuery,
s3Config?: S3Config
) {
// Create Next Server mock
const server = ({
Expand All @@ -38,11 +45,35 @@ async function imageOptimizer(
res: ServerResponse,
url: UrlWithParsedQuery
) => {
// TODO: When deployed together with Terraform Next.js we can use
// AWS SDK here to fetch the image directly from S3 instead of
// using node - fetch
if (s3Config) {
// S3 expects keys without leading `/`
const trimmedKey = url.href.startsWith('/')
? url.href.substring(1)
: url.href;

const object = await s3Config.s3
.getObject({
Key: trimmedKey,
Bucket: s3Config.bucket,
})
.promise();

if (!object.Body) {
throw new Error(`Could not fetch image ${trimmedKey} from bucket.`);
}

res.statusCode = 200;

if (object.ContentType) {
res.setHeader('Content-Type', object.ContentType);
}

if (object.CacheControl) {
originCacheControl = object.CacheControl;
}

if (headers.referer) {
res.end(object.Body);
} else if (headers.referer) {
let upstreamBuffer: Buffer;
let upstreamType: string | null;

Expand All @@ -54,7 +85,7 @@ async function imageOptimizer(
const upstreamRes = await nodeFetch(origin);

if (!upstreamRes.ok) {
throw new Error(`Could not fetch image from ${origin}`);
throw new Error(`Could not fetch image from ${origin}.`);
}

res.statusCode = upstreamRes.status;
Expand All @@ -78,4 +109,4 @@ async function imageOptimizer(
};
}

export { imageOptimizer };
export { S3Config, imageOptimizer };
11 changes: 7 additions & 4 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ module "image_optimizer" {
publish = true

environment_variables = {
NODE_ENV = "production",
TF_NEXTIMAGE_DOMAINS = jsonencode(var.next_image_domains)
TF_NEXTIMAGE_DEVICE_SIZES = var.next_image_device_sizes != null ? jsonencode(var.next_image_device_sizes) : null
TF_NEXTIMAGE_IMAGE_SIZES = var.next_image_image_sizes != null ? jsonencode(var.next_image_image_sizes) : null
NODE_ENV = "production",
TF_NEXTIMAGE_DOMAINS = jsonencode(var.next_image_domains)
TF_NEXTIMAGE_DEVICE_SIZES = var.next_image_device_sizes != null ? jsonencode(var.next_image_device_sizes) : null
TF_NEXTIMAGE_IMAGE_SIZES = var.next_image_image_sizes != null ? jsonencode(var.next_image_image_sizes) : null
TF_NEXTIMAGE_SOURCE_BUCKET = var.source_bucket_id
}

create_package = false
Expand All @@ -47,6 +48,8 @@ module "image_optimizer" {

cloudwatch_logs_retention_in_days = 30

attach_policy_json = var.lambda_attach_policy_json
policy_json = var.lambda_policy_json
role_permissions_boundary = var.lambda_role_permissions_boundary

tags = var.tags
Expand Down
Loading