feat: implement legacy imgproxy compatibility and URL mapping
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m32s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
All checks were successful
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m1s
Monorepo Pipeline / 🧹 Lint (push) Successful in 2m56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m32s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "image-service",
|
"name": "image-service",
|
||||||
"version": "1.8.16",
|
"version": "1.8.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,37 +1,45 @@
|
|||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import { processImageWithSmartCrop } from "@mintel/image-processor";
|
import {
|
||||||
|
processImageWithSmartCrop,
|
||||||
|
parseImgproxyOptions,
|
||||||
|
mapUrl,
|
||||||
|
} from "@mintel/image-processor";
|
||||||
|
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get("/unsafe/:options/:urlSafeB64", async (request, reply) => {
|
fastify.get("/unsafe/:options/:urlSafeB64", async (request, reply) => {
|
||||||
// Compatibility endpoint for old imgproxy calls (optional, but requested by some systems sometimes)
|
const { options, urlSafeB64 } = request.params as {
|
||||||
// For now, replacing logic in clients is preferred. So we just redirect or error.
|
options: string;
|
||||||
return reply
|
urlSafeB64: string;
|
||||||
.status(400)
|
|
||||||
.send({ error: "Legacy imgproxy API not supported. Use /process" });
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get("/process", async (request, reply) => {
|
|
||||||
const query = request.query as {
|
|
||||||
url?: string;
|
|
||||||
w?: string;
|
|
||||||
h?: string;
|
|
||||||
q?: string;
|
|
||||||
format?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { url } = query;
|
// urlSafeB64 might be "plain/http://..." or a Base64 string
|
||||||
const width = parseInt(query.w || "800", 10);
|
let url = "";
|
||||||
const height = parseInt(query.h || "600", 10);
|
if (urlSafeB64.startsWith("plain/")) {
|
||||||
const quality = parseInt(query.q || "80", 10);
|
url = urlSafeB64.substring(6);
|
||||||
const format = (query.format || "webp") as "webp" | "jpeg" | "png" | "avif";
|
} else {
|
||||||
|
try {
|
||||||
if (!url) {
|
url = Buffer.from(urlSafeB64, "base64").toString("utf-8");
|
||||||
return reply.status(400).send({ error: 'Parameter "url" is required' });
|
} catch (e) {
|
||||||
|
return reply.status(400).send({ error: "Invalid Base64 URL" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parsedOptions = parseImgproxyOptions(options);
|
||||||
|
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
|
||||||
|
|
||||||
|
return handleProcessing(mappedUrl, parsedOptions, reply);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to avoid duplication
|
||||||
|
async function handleProcessing(url: string, options: any, reply: any) {
|
||||||
|
const width = options.width || 800;
|
||||||
|
const height = options.height || 600;
|
||||||
|
const quality = options.quality || 80;
|
||||||
|
const format = options.format || "webp";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -60,6 +68,29 @@ fastify.get("/process", async (request, reply) => {
|
|||||||
.status(500)
|
.status(500)
|
||||||
.send({ error: "Internal Server Error processing image" });
|
.send({ error: "Internal Server Error processing image" });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fastify.get("/process", async (request, reply) => {
|
||||||
|
const query = request.query as {
|
||||||
|
url?: string;
|
||||||
|
w?: string;
|
||||||
|
h?: string;
|
||||||
|
q?: string;
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { url } = query;
|
||||||
|
const width = parseInt(query.w || "800", 10);
|
||||||
|
const height = parseInt(query.h || "600", 10);
|
||||||
|
const quality = parseInt(query.q || "80", 10);
|
||||||
|
const format = (query.format || "webp") as any;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return reply.status(400).send({ error: 'Parameter "url" is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedUrl = mapUrl(url, process.env.IMGPROXY_URL_MAPPING);
|
||||||
|
return handleProcessing(mappedUrl, { width, height, quality, format }, reply);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get("/health", async () => {
|
fastify.get("/health", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/image-processor",
|
"name": "@mintel/image-processor",
|
||||||
"version": "1.8.16",
|
"version": "1.8.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -8,6 +8,54 @@ export interface ProcessImageOptions {
|
|||||||
openRouterApiKey?: string;
|
openRouterApiKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a URL based on the IMGPROXY_URL_MAPPING environment variable.
|
||||||
|
* Format: "match1:replace1,match2:replace2"
|
||||||
|
*/
|
||||||
|
export function mapUrl(url: string, mappingString?: string): string {
|
||||||
|
if (!mappingString) return url;
|
||||||
|
|
||||||
|
const mappings = mappingString.split(",").map((m) => m.split(":"));
|
||||||
|
let mappedUrl = url;
|
||||||
|
|
||||||
|
for (const [match, replace] of mappings) {
|
||||||
|
if (match && replace && url.includes(match)) {
|
||||||
|
mappedUrl = url.replace(match, replace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses legacy imgproxy options string.
|
||||||
|
* Example: rs:fill:300:400/q:80
|
||||||
|
*/
|
||||||
|
export function parseImgproxyOptions(
|
||||||
|
optionsStr: string,
|
||||||
|
): Partial<ProcessImageOptions> {
|
||||||
|
const parts = optionsStr.split("/");
|
||||||
|
const options: Partial<ProcessImageOptions> = {};
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.startsWith("rs:")) {
|
||||||
|
const [, , w, h] = part.split(":");
|
||||||
|
if (w) options.width = parseInt(w, 10);
|
||||||
|
if (h) options.height = parseInt(h, 10);
|
||||||
|
} else if (part.startsWith("q:")) {
|
||||||
|
const q = part.split(":")[1];
|
||||||
|
if (q) options.quality = parseInt(q, 10);
|
||||||
|
} else if (part.startsWith("ext:")) {
|
||||||
|
const ext = part.split(":")[1] as any;
|
||||||
|
if (["webp", "jpeg", "png", "avif"].includes(ext)) {
|
||||||
|
options.format = ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
interface FaceDetection {
|
interface FaceDetection {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user