@urami/core
Version:
Automatic image optimization server endpoints
547 lines (530 loc) • 16.9 kB
JavaScript
// src/index.ts
import path3 from "node:path";
import { URL } from "node:url";
import isAnimated from "is-animated";
// src/constants/mimeTypes.ts
var AVIF = "image/avif";
var WEBP = "image/webp";
var PNG = "image/png";
var JPEG = "image/jpeg";
var GIF = "image/gif";
var SVG = "image/svg+xml";
// src/constants/animateableTypes.ts
var animateableTypes = [WEBP, PNG, GIF];
// src/constants/defaultConfig.ts
var defaultConfig = {
avif: false,
ttl: 1e3 * 60 * 60 * 24 * 7,
storePath: ".urami/images"
};
// src/constants/vectorTypes.ts
var vectorTypes = [SVG];
// src/functions/detectContentType.ts
var JPEG_SIG = [255, 216, 255];
var PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
var GIF_SIG = [71, 73, 70, 56];
var WEBP_SIG = [82, 73, 70, 70, 0, 0, 0, 0, 87, 69, 66, 80];
var SVG_SIG = [60, 63, 120, 109, 108];
var AVIF_SIG = [0, 0, 0, 0, 102, 116, 121, 112, 97, 118, 105, 102];
var typeCache = /* @__PURE__ */ new Map();
var detectContentType = (buffer) => {
if (!buffer || buffer.length === 0) {
return null;
}
const bytesToCheck = Math.min(16, buffer.length);
const cacheKey = Array.from(buffer.slice(0, bytesToCheck)).toString();
if (typeCache.has(cacheKey)) {
return typeCache.get(cacheKey) || null;
}
let result = null;
if (buffer.length >= 3 && buffer[0] === JPEG_SIG[0] && buffer[1] === JPEG_SIG[1] && buffer[2] === JPEG_SIG[2]) {
result = JPEG;
} else if (buffer.length >= PNG_SIG.length && PNG_SIG.every((b, i) => buffer[i] === b)) {
result = PNG;
} else if (buffer.length >= GIF_SIG.length && GIF_SIG.every((b, i) => buffer[i] === b)) {
result = GIF;
} else if (buffer.length >= WEBP_SIG.length && WEBP_SIG.every((b, i) => b === 0 || buffer[i] === b)) {
result = WEBP;
} else if (buffer.length >= SVG_SIG.length && SVG_SIG.every((b, i) => buffer[i] === b)) {
result = SVG;
} else if (buffer.length >= AVIF_SIG.length && AVIF_SIG.every((b, i) => b === 0 || buffer[i] === b)) {
result = AVIF;
}
typeCache.set(cacheKey, result);
if (typeCache.size > 1e3) {
const firstKey = typeCache.keys().next().value;
if (firstKey) {
typeCache.delete(firstKey);
}
}
return result;
};
// src/functions/error.ts
var error = (status, message) => new Response(message, {
status
});
// src/functions/getHash.ts
import { createHash } from "node:crypto";
var getHash = (items) => {
const hash = createHash("sha256");
for (let item of items) {
if (typeof item === "number") hash.update(String(item));
else {
hash.update(item);
}
}
return hash.digest("base64").replace(/\//g, "-");
};
// src/constants/cacheVersion.ts
var cacheVersion = 1;
// src/functions/getCacheKey.ts
var getCacheKey = (href, width, quality, mimeType) => {
return getHash([cacheVersion, href, width, quality, mimeType]);
};
// src/functions/getExtension.ts
import send from "send";
send.mime.define({
"image/avif": ["avif"]
});
function getExtension(contentType) {
const { mime } = send;
if ("getExtension" in mime) {
return mime.getExtension(contentType);
}
return mime.extension(contentType);
}
// src/functions/getMaxAge.ts
var parseCacheControl = (str) => {
const map = /* @__PURE__ */ new Map();
if (!str) {
return map;
}
for (let directive of str.split(",")) {
let [key, value] = directive.trim().split("=");
key = key.toLowerCase();
if (value) {
value = value.toLowerCase();
}
map.set(key, value);
}
return map;
};
var getMaxAge = (str) => {
const map = parseCacheControl(str);
if (map) {
let age = map.get("s-maxage") || map.get("max-age") || "";
if (age.startsWith('"') && age.endsWith('"')) {
age = age.slice(1, -1);
}
const n = parseInt(age, 10);
if (!isNaN(n)) {
return n;
}
}
return 0;
};
// src/functions/getSupportedMimeType.ts
import { mediaType } from "@hapi/accept";
var getSupportedMimeType = (options, accept = "") => {
const mimeType = mediaType(accept, options);
return accept.includes(mimeType) ? mimeType : "";
};
// src/functions/optimizeImage.ts
import sharp from "sharp";
var sharpInstance = null;
var metadataCache = /* @__PURE__ */ new Map();
var getImageMetadata = async (buffer, bufferHash) => {
const cachedMetadata = metadataCache.get(bufferHash);
if (cachedMetadata) return cachedMetadata;
if (!sharpInstance) {
sharpInstance = sharp(buffer);
} else {
sharpInstance.removeAlpha().withMetadata();
sharpInstance = sharp(buffer);
}
const metadata = await sharpInstance.metadata();
metadataCache.set(bufferHash, metadata);
if (metadataCache.size > 1e3) {
const firstKey = metadataCache.keys().next().value;
if (firstKey) {
metadataCache.delete(firstKey);
}
}
return metadata;
};
var optimizeImage = async (buffer, contentType, quality, width, height, bufferHash) => {
try {
const transformer = sharp(buffer, {
failOnError: false
// More resilient to slightly corrupted images
});
transformer.rotate();
if (!height && width > 0) {
const metadata = await getImageMetadata(buffer, bufferHash || "");
if (metadata.width && metadata.width > width) {
transformer.resize(width);
}
} else if (height && width) {
transformer.resize(width, height);
}
switch (contentType) {
case AVIF:
if (transformer.avif) {
const avifQuality = quality - 15;
transformer.avif({
quality: Math.max(avifQuality, 0),
chromaSubsampling: "4:2:0"
// same as webp
});
} else {
transformer.webp({ quality });
}
break;
case WEBP:
transformer.webp({ quality });
break;
case PNG:
transformer.png({ quality });
break;
case JPEG:
default:
transformer.jpeg({ quality });
break;
}
return transformer.toBuffer();
} catch (err) {
console.error("Image optimization failed:", err);
return null;
}
};
// src/functions/readImageFileSystem.ts
import fs from "node:fs";
import path from "node:path";
var cacheMap = /* @__PURE__ */ new Map();
var CLEANUP_INTERVAL = 10 * 60 * 1e3;
setInterval(() => {
const now = Date.now();
for (const [key, entry] of cacheMap.entries()) {
if (entry.expireAt < now) {
cacheMap.delete(key);
}
}
}, CLEANUP_INTERVAL);
var readImageFileSystem = async (cacheKey, cacheDirectory) => {
const now = Date.now();
const cachedInfo = cacheMap.get(cacheKey);
if (cachedInfo && cachedInfo.expireAt > now) {
try {
const buffer = await fs.promises.readFile(cachedInfo.filePath);
return {
buffer,
etag: cachedInfo.etag,
maxAge: cachedInfo.maxAge,
contentType: cachedInfo.contentType || detectContentType(buffer)
};
} catch {
cacheMap.delete(cacheKey);
}
}
try {
const requestedDirectory = path.join(cacheDirectory, cacheKey);
const files = await fs.promises.readdir(requestedDirectory);
if (files.length > 1) {
const fileStats = await Promise.all(
files.map(async (file) => {
const filePath = path.join(requestedDirectory, file);
const stats = await fs.promises.stat(filePath);
return { file, mtime: stats.mtime };
})
);
files.sort((a, b) => {
const statsA = fileStats.find((stat) => stat.file === a);
const statsB = fileStats.find((stat) => stat.file === b);
return (statsA?.mtime?.getTime() || 0) - (statsB?.mtime?.getTime() || 0);
});
}
const deletionPromises = [];
for (const file of files) {
const [maxAgeString, expireAtString, etag, extension] = file.split(".");
const filePath = path.join(requestedDirectory, file);
const expireAt = Number(expireAtString);
const maxAge = Number(maxAgeString);
if (expireAt < now) {
deletionPromises.push(fs.promises.rm(filePath).catch(() => {
}));
continue;
}
try {
const buffer = await fs.promises.readFile(filePath);
const contentType = detectContentType(buffer);
cacheMap.set(cacheKey, {
filePath,
etag,
maxAge,
expireAt,
contentType: contentType || void 0
});
if (deletionPromises.length > 0) {
Promise.all(deletionPromises).catch(() => {
});
}
return {
buffer,
etag,
maxAge,
contentType
};
} catch (err) {
continue;
}
}
if (deletionPromises.length > 0) {
Promise.all(deletionPromises).catch(() => {
});
}
} catch (err) {
}
return null;
};
// src/functions/sendResponse.ts
var sendResponse = (payload, cacheHit, extraHeaders = {}) => new Response(payload.buffer, {
headers: {
Vary: "Accept",
"Content-Type": payload.contentType ?? "",
"Cache-Control": `public, max-age=${Math.floor(payload.maxAge / 1e3)}, must-revalidate`,
"CDN-Cache-Control": `max-age=${Math.floor(payload.maxAge / 1e3)}`,
"Content-Length": Buffer.byteLength(payload.buffer).toString(),
"Content-Security-Policy": "script-src 'none'; frame-src 'none'; sandbox;",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
ETag: payload.etag,
"X-SvelteAIO-Cache": cacheHit,
...extraHeaders
}
});
// src/functions/writeImageToFileSystem.ts
import fs2 from "node:fs";
import path2 from "node:path";
var extensionCache = /* @__PURE__ */ new Map();
var pendingDirectories = /* @__PURE__ */ new Set();
var writeImageToFileSystem = async (cacheKey, contentType, maxAge, etag, buffer, cacheDirectory) => {
let extension = extensionCache.get(contentType);
if (!extension) {
extension = getExtension(contentType) || "bin";
extensionCache.set(contentType, extension);
}
const targetDirectory = path2.join(cacheDirectory, cacheKey);
const expireAt = maxAge + Date.now();
const targetFileName = `${maxAge}.${expireAt}.${etag}.${extension}`;
const targetFilePath = path2.join(targetDirectory, targetFileName);
if (pendingDirectories.has(targetDirectory)) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
pendingDirectories.add(targetDirectory);
try {
await fs2.promises.mkdir(targetDirectory, { recursive: true });
cleanExpiredFiles(targetDirectory).catch(() => {
});
await fs2.promises.writeFile(targetFilePath, buffer);
return;
} catch (err) {
try {
await fs2.promises.access(targetFilePath);
await fs2.promises.rm(targetFilePath);
} catch {
}
console.error("Cache write error:", err);
} finally {
pendingDirectories.delete(targetDirectory);
}
};
async function cleanExpiredFiles(directory) {
try {
const now = Date.now();
const files = await fs2.promises.readdir(directory);
const filesToProcess = files.slice(0, 10);
const deletionPromises = filesToProcess.map(async (file) => {
try {
const expireAtString = file.split(".")[1];
const expireAt = Number(expireAtString);
if (isNaN(expireAt) || expireAt < now) {
await fs2.promises.rm(path2.join(directory, file));
}
} catch {
}
});
await Promise.all(deletionPromises);
} catch {
}
}
// src/index.ts
var createRequestHandler = (config = {}) => async (request) => {
const mergedConfig = {
...defaultConfig,
...config
};
const targetCacheDirectory = path3.join(
process.cwd(),
mergedConfig.storePath
);
const ifNoneMatch = request.headers.get("If-None-Match");
try {
const fullRequestUrl = new URL(request.url);
let url = fullRequestUrl.searchParams.get("url") ?? "";
const width = Number(fullRequestUrl.searchParams.get("w") ?? "");
const quality = Number(fullRequestUrl.searchParams.get("q") ?? "");
let targetUrl;
let targetDomain = null;
try {
targetUrl = new URL(url);
} catch {
targetDomain = mergedConfig.defaultDomain ?? request.headers.get("referer");
if (!targetDomain) throw error(400, "missing url");
try {
targetUrl = new URL(url, targetDomain);
} catch {
throw error(400, "invalid url");
}
}
url = targetUrl.toString();
const hostname = targetUrl.hostname;
const remoteDomainAllowed = !mergedConfig.remoteDomains || mergedConfig.remoteDomains.includes(hostname);
if (!remoteDomainAllowed) {
throw error(403, "not allowed to optimize");
}
if (process.env.NODE_ENV === "production" && mergedConfig.allowedDomains) {
const referer = request.headers.get("referer");
let refererHost = "localhost";
try {
if (referer) {
refererHost = new URL(referer).hostname;
}
} catch {
}
if (!mergedConfig.allowedDomains.includes(refererHost)) {
throw error(403, "not allowed to access");
}
}
const mimeType = getSupportedMimeType(
["image/webp", ...mergedConfig.avif ? ["image/avif"] : []],
request.headers.get("accept") ?? ""
);
const cacheKey = getCacheKey(url, width, quality, mimeType);
const cacheResponse = await readImageFileSystem(
cacheKey,
targetCacheDirectory
);
if (cacheResponse !== null) {
if (ifNoneMatch === cacheResponse.etag) {
return new Response(null, {
status: 304,
headers: {
Vary: "Accept",
"Cache-Control": `public, max-age=${Math.floor(cacheResponse.maxAge / 1e3)}, must-revalidate`,
ETag: cacheResponse.etag,
"X-SvelteAIO-Cache": "HIT",
"X-Urami-Cache": "HIT"
}
});
}
return sendResponse(cacheResponse, "HIT");
}
const fetchedImage = await fetch(url);
if (!fetchedImage.ok) {
throw error(fetchedImage.status, `upstream error: ${fetchedImage.statusText}`);
}
const upstreamBuffer = Buffer.from(await fetchedImage.arrayBuffer());
const upstreamHash = getHash([upstreamBuffer]);
const upstreamType = detectContentType(upstreamBuffer) || (fetchedImage.headers.get("Content-Type") ?? "");
const maxAge = getMaxAge(fetchedImage.headers.get("Cache-Control"));
const vector = vectorTypes.includes(upstreamType);
const animated = animateableTypes.includes(upstreamType) && isAnimated(upstreamBuffer);
if (vector || animated) {
if (ifNoneMatch === upstreamHash) {
return new Response(null, {
status: 304,
headers: {
Vary: "Accept",
"Cache-Control": "public, max-age=0, must-revalidate",
ETag: upstreamHash,
"X-SvelteAIO-Cache": "MISS",
"X-Urami-Cache": "MISS",
"X-Urami-Optimization": "animate-ignore"
}
});
}
return sendResponse(
{
buffer: upstreamBuffer,
contentType: upstreamType,
maxAge: 0,
etag: upstreamHash
},
"MISS",
{
"X-Urami-Optimization": "animate-ignore"
}
);
}
let contentType;
if (mimeType) {
contentType = mimeType;
} else if (upstreamType?.startsWith("image/") && getExtension(upstreamType) && upstreamType !== WEBP && upstreamType !== AVIF) {
contentType = upstreamType;
} else {
contentType = JPEG;
}
const optimizedBuffer = await optimizeImage(
upstreamBuffer,
contentType,
quality,
width,
void 0,
// height
upstreamHash
// pass buffer hash for metadata caching
);
if (optimizedBuffer === null) {
throw error(500, "unable to optimize image");
}
const optimizedEtag = getHash([optimizedBuffer]);
const payload = {
buffer: optimizedBuffer,
contentType,
maxAge: Math.max(maxAge, mergedConfig.ttl),
etag: optimizedEtag
};
if (ifNoneMatch === optimizedEtag) {
return new Response(null, {
status: 304,
headers: {
Vary: "Accept",
"Cache-Control": `public, max-age=${Math.floor(payload.maxAge / 1e3)}, must-revalidate`,
ETag: optimizedEtag,
"X-SvelteAIO-Cache": "MISS",
"X-Urami-Cache": "MISS"
}
});
}
writeImageToFileSystem(
cacheKey,
contentType,
payload.maxAge,
payload.etag,
payload.buffer,
targetCacheDirectory
).catch((err) => {
console.error("Cache write error:", err);
});
return sendResponse(payload, "MISS");
} catch (err) {
if (err instanceof Error && "status" in err && typeof err.status === "number") {
throw err;
}
console.error("Image processing error:", err);
throw error(500, "unable to optimize image");
}
};
export {
createRequestHandler
};
//# sourceMappingURL=index.js.map