improve favicons

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-14 23:12:10 +01:00
parent 176ddc410b
commit a1d5104166
6 changed files with 232 additions and 94 deletions

View File

@@ -1,83 +1,129 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import icoToPng from 'ico-to-png';
import sharp from 'sharp';
import { createHash } from '@mixan/common';
import { redis } from '@mixan/redis';
interface GetFaviconParams {
url: string;
}
function toBuffer(arrayBuffer: ArrayBuffer) {
const buffer = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i]!;
}
return buffer;
}
async function getImageBuffer(url: string) {
try {
const res = await fetch(url);
const contentType = res.headers.get('content-type');
async function getUrlBuffer(url: string) {
const arrayBuffer = await fetch(url).then((res) => {
if (res.ok) {
return res.arrayBuffer();
if (!contentType?.includes('image')) {
return null;
}
});
if (arrayBuffer) {
return toBuffer(arrayBuffer);
if (!res.ok) {
return null;
}
if (contentType === 'image/x-icon' || url.endsWith('.ico')) {
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return await icoToPng(buffer, 30);
}
return await sharp(await res.arrayBuffer())
.resize(30, 30, {
fit: 'cover',
})
.png()
.toBuffer();
} catch (e) {
console.log('Failed to get image from url', url);
console.log(e);
}
return null;
}
const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'];
export async function getFavicon(
request: FastifyRequest<{
Querystring: GetFaviconParams;
}>,
reply: FastifyReply
) {
function sendBuffer(buffer: Buffer, cacheKey?: string) {
if (cacheKey) {
redis.set(`favicon:${cacheKey}`, buffer.toString('base64'));
}
reply.type('image/png');
console.log('buffer', buffer.byteLength);
return reply.send(buffer);
}
if (!request.query.url) {
return reply.status(404).send('Not found');
}
function sendBuffer(buffer: Buffer, hostname?: string) {
if (hostname) {
redis.set(`favicon:${hostname}`, buffer.toString('base64'));
const url = decodeURIComponent(request.query.url);
// DIRECT IMAGE
if (imageExtensions.find((ext) => url.endsWith(ext))) {
const cacheKey = createHash(url, 32);
const cache = await redis.get(`favicon:${cacheKey}`);
if (cache) {
return sendBuffer(Buffer.from(cache, 'base64'));
}
const buffer = await getImageBuffer(url);
if (buffer && buffer.byteLength > 0) {
return sendBuffer(buffer, cacheKey);
}
reply.type('image/png');
return reply.send(buffer);
}
const url = decodeURIComponent(request.query.url);
const { hostname, origin } = new URL(url);
const cache = await redis.get(`favicon:${hostname}`);
if (cache) {
return sendBuffer(Buffer.from(cache, 'base64'));
}
// Try just get the favicon.ico
const buffer = await getUrlBuffer(`${origin}/favicon.ico`);
if (buffer) {
// TRY FAVICON.ICO
const buffer = await getImageBuffer(`${origin}/favicon.ico`);
console.log('buffer', buffer?.length);
if (buffer && buffer.byteLength > 0) {
return sendBuffer(buffer, hostname);
}
// If that didnt work try parse html
// PARSE HTML
const res = await fetch(url).then((res) => res.text());
const favicon =
res.match(/<link.*?rel="icon".*?href="(.+?)".*?>/) ||
res.match(/<link.*?rel="shortcut icon".*?href="(.+?)".*?>/);
if (favicon?.[1]) {
const faviconUrl = favicon[1].startsWith('http')
? favicon[1]
: `${origin}${favicon[1]}`;
function findFavicon(res: string) {
const match = res.match(
/(\<link(.+?)image\/x-icon(.+?)\>|\<link(.+?)shortcut\sicon(.+?)\>)/
);
if (!match) {
return null;
}
const buffer = await getUrlBuffer(faviconUrl);
return match[0].match(/href="(.+?)"/)?.[1] ?? null;
}
if (buffer) {
const favicon = findFavicon(res);
if (favicon) {
const buffer = await getImageBuffer(favicon);
if (buffer && buffer.byteLength > 0) {
return sendBuffer(buffer, hostname);
}
}
return reply.status(404).send('Not found');
}
export async function clearFavicons(
request: FastifyRequest,
reply: FastifyReply
) {
const keys = await redis.keys('favicon:*');
for (const key of keys) {
await redis.del(key);
}
return reply.status(404).send('OK');
}

View File

@@ -8,6 +8,12 @@ const miscRouter: FastifyPluginCallback = (fastify, opts, done) => {
handler: controller.getFavicon,
});
fastify.route({
method: 'GET',
url: '/favicon/clear',
handler: controller.clearFavicons,
});
done();
};