Skip to content

Commit ddffa66

Browse files
committed
feat(image-processing-api): add relative url support
1 parent 7d650cd commit ddffa66

File tree

4 files changed

+80
-5
lines changed

4 files changed

+80
-5
lines changed

extensions/image-processing-api/functions/src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import sharp from 'sharp';
4040
async function processImageRequest(
4141
validatedOperations: ValidatedOperation[],
4242
res: Response,
43+
req: Request,
4344
): Promise<void> {
4445
const firstOperation = validatedOperations[0];
4546
const lastOperation = validatedOperations[validatedOperations.length - 1];
@@ -62,7 +63,7 @@ async function processImageRequest(
6263
let instance: sharp.Sharp | null = null;
6364
for (let i = 0; i < validatedOperations.length; i++) {
6465
const validatedOperation = validatedOperations[i];
65-
instance = await applyValidatedOperation(instance, validatedOperation);
66+
instance = await applyValidatedOperation(instance, validatedOperation, req);
6667
}
6768

6869
const finalFileMetadata = omitKeys(
@@ -142,7 +143,7 @@ app.get(
142143
const validatedOperations: ValidatedOperation[] =
143144
jsonAsValidatedOperations(operations);
144145
const [processError] = await a2a(
145-
processImageRequest(validatedOperations, res),
146+
processImageRequest(validatedOperations, res, req),
146147
);
147148
if (processError) {
148149
return next(processError);
@@ -160,7 +161,7 @@ app.get(
160161
const validatedOperations: ValidatedOperation[] =
161162
asValidatedOperations(operationsString);
162163
const [processError] = await a2a(
163-
processImageRequest(validatedOperations, res),
164+
processImageRequest(validatedOperations, res, req),
164165
);
165166
if (processError) {
166167
return next(processError);

extensions/image-processing-api/functions/src/operations/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import express from 'express';
1718
import { AssertionError } from 'assert';
1819
import sharp, { SharpOptions } from 'sharp';
1920
import superstruct from 'superstruct';
@@ -232,10 +233,11 @@ export function asValidatedOperations(input: string): ValidatedOperation[] {
232233
export async function asBuiltOperation(
233234
validatedOperation: ValidatedOperation,
234235
fileMetadata: sharp.Metadata | null,
236+
req?: express.Request,
235237
): Promise<BuiltOperation> {
236238
const actionBuilder =
237239
builderForOperation(validatedOperation)?.build || defaultActionsBuilder;
238-
let builtActions = actionBuilder(validatedOperation, fileMetadata);
240+
let builtActions = actionBuilder(validatedOperation, fileMetadata, req);
239241
if (builtActions instanceof Promise) {
240242
builtActions = await builtActions;
241243
}
@@ -248,6 +250,7 @@ export async function asBuiltOperation(
248250
export async function applyValidatedOperation(
249251
instance: sharp.Sharp | null,
250252
validatedOperation: ValidatedOperation,
253+
req?: express.Request,
251254
): Promise<sharp.Sharp> {
252255
let currentInstance = instance;
253256
const currentMetadata = currentInstance
@@ -256,6 +259,7 @@ export async function applyValidatedOperation(
256259
const builtOperation = await asBuiltOperation(
257260
validatedOperation,
258261
currentMetadata,
262+
req,
259263
);
260264
for (let i = 0; i < builtOperation.actions.length; i++) {
261265
const action = builtOperation.actions[i];

extensions/image-processing-api/functions/src/operations/input.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import * as superstruct from 'superstruct';
18+
import express from 'express';
1819

1920
import * as utils from '../utils';
2021
import { Operation, OperationAction, OperationBuilder } from '../types';
@@ -48,6 +49,15 @@ const structFromUrl = superstruct.object({
4849
url: superstruct.string(),
4950
});
5051

52+
/**
53+
* A path url of an image. The request hostname will be prepended to this path.
54+
*/
55+
const structFromPath = superstruct.object({
56+
operation: superstruct.literal(name),
57+
type: superstruct.literal('path'),
58+
path: superstruct.string(),
59+
});
60+
5161
/**
5262
* Create a new image.
5363
*/
@@ -110,18 +120,21 @@ const structCreateNewImage = superstruct.object({
110120
const struct = superstruct.union([
111121
structFromGcs,
112122
structFromUrl,
123+
structFromPath,
113124
structCreateNewImage,
114125
]);
115126

116127
export type OperationInputGcs = superstruct.Infer<typeof structFromGcs>;
117128
export type OperationInputUrl = superstruct.Infer<typeof structFromUrl>;
129+
export type OperationInputPath = superstruct.Infer<typeof structFromPath>;
118130
export type OperationInputCreateNew = superstruct.Infer<
119131
typeof structCreateNewImage
120132
>;
121133

122134
export type OperationInput =
123135
| OperationInputGcs
124136
| OperationInputUrl
137+
| OperationInputPath
125138
| OperationInputCreateNew;
126139

127140
export const operationInput: OperationBuilder = {
@@ -135,12 +148,14 @@ export const operationInput: OperationBuilder = {
135148
return structFromGcs.create(rawOptions);
136149
case 'url':
137150
return structFromUrl.create(rawOptions);
151+
case 'path':
152+
return structFromPath.create(rawOptions);
138153
}
139154
throw new AssertionError({
140155
message: `'${rawOptions.type}' is not a valid input 'type'.`,
141156
});
142157
},
143-
async build(operation) {
158+
async build(operation, _fileMetadata, req) {
144159
const options = operation.options as OperationInput;
145160

146161
switch (options.type) {
@@ -150,6 +165,8 @@ export const operationInput: OperationBuilder = {
150165
return await fetchGcsFile(options);
151166
case 'url':
152167
return await fetchUrl(options);
168+
case 'path':
169+
return await fetchPathUrl(options, req);
153170
}
154171
},
155172
};
@@ -169,6 +186,57 @@ async function fetchUrl(options: OperationInput): Promise<OperationAction[]> {
169186
];
170187
}
171188

189+
async function fetchPathUrl(
190+
options: OperationInput,
191+
req?: express.Request,
192+
): Promise<OperationAction[]> {
193+
if (options.type !== 'path') return [];
194+
195+
if (!req) {
196+
throw new AssertionError({
197+
message: 'Request object is required for path type inputs.',
198+
});
199+
}
200+
201+
// Extract hostname from request
202+
const origin = (req.headers.origin as string) || '';
203+
const referer = (req.headers.referer as string) || '';
204+
const host = req.headers.host || '';
205+
206+
let baseUrl = '';
207+
208+
// Try to get the base URL from various sources
209+
if (origin) {
210+
baseUrl = origin;
211+
} else if (referer) {
212+
try {
213+
const url = new URL(referer);
214+
baseUrl = `${url.protocol}//${url.host}`;
215+
} catch (e) {
216+
// Invalid URL format in referer
217+
}
218+
} else if (host) {
219+
const protocol = req.secure ? 'https:' : 'http:';
220+
baseUrl = `${protocol}//${host}`;
221+
}
222+
223+
if (!baseUrl) {
224+
throw new AssertionError({
225+
message: `Could not determine request hostname for path URL.`,
226+
});
227+
}
228+
229+
// Construct the full URL
230+
const fullUrl = `${baseUrl}${options.path}`;
231+
232+
return [
233+
{
234+
method: 'constructor',
235+
arguments: [await fetchImageBufferFromUrl(fullUrl)],
236+
},
237+
];
238+
}
239+
172240
async function fetchGcsFile(
173241
options: OperationInput,
174242
): Promise<OperationAction[]> {

extensions/image-processing-api/functions/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
import * as superstruct from 'superstruct';
1717
import sharp from 'sharp';
18+
import express from 'express';
1819

1920
export type ActionBuilder = (
2021
validatedOperation: ValidatedOperation,
2122
imageMetadata: sharp.Metadata | null,
23+
req?: express.Request,
2224
) => OperationAction[] | Promise<OperationAction[]>;
2325

2426
export type OperationBuilder = {

0 commit comments

Comments
 (0)