first version
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import db from "../../db.server";
|
||||
|
||||
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB cap
|
||||
const STALE_AFTER_MS = 24 * 60 * 60 * 1000; // re-fetch once a day at most
|
||||
|
||||
/**
|
||||
* Returns a `data:` URL for the shop's logo bytes, fetching from the
|
||||
* configured URL on first use (or when stale) and persisting to the
|
||||
* `LogoCache` table for subsequent renders.
|
||||
*/
|
||||
export async function getLogoDataUrl(
|
||||
shopDomain: string,
|
||||
logoUrl: string,
|
||||
): Promise<string | undefined> {
|
||||
if (!logoUrl) return undefined;
|
||||
|
||||
const cached = await db.logoCache.findUnique({ where: { shopDomain } });
|
||||
const isFresh =
|
||||
cached &&
|
||||
cached.sourceUrl === logoUrl &&
|
||||
Date.now() - cached.fetchedAt.getTime() < STALE_AFTER_MS;
|
||||
|
||||
if (isFresh && cached) {
|
||||
return toDataUrl(cached.bytes, cached.contentType);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(logoUrl);
|
||||
} catch (err) {
|
||||
console.warn(`Logo fetch failed for ${shopDomain}:`, err);
|
||||
return cached ? toDataUrl(cached.bytes, cached.contentType) : undefined;
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.warn(`Logo fetch HTTP ${response.status} for ${shopDomain}`);
|
||||
return cached ? toDataUrl(cached.bytes, cached.contentType) : undefined;
|
||||
}
|
||||
|
||||
const arrayBuf = await response.arrayBuffer();
|
||||
if (arrayBuf.byteLength > MAX_BYTES) {
|
||||
console.warn(`Logo too large (${arrayBuf.byteLength} bytes) — skipping cache.`);
|
||||
return undefined;
|
||||
}
|
||||
const bytes = Buffer.from(arrayBuf);
|
||||
const contentType = response.headers.get("content-type") || guessContentType(logoUrl);
|
||||
const etag = response.headers.get("etag") || "";
|
||||
|
||||
await db.logoCache.upsert({
|
||||
where: { shopDomain },
|
||||
create: { shopDomain, sourceUrl: logoUrl, bytes, contentType, etag },
|
||||
update: { sourceUrl: logoUrl, bytes, contentType, etag, fetchedAt: new Date() },
|
||||
});
|
||||
|
||||
return toDataUrl(bytes, contentType);
|
||||
}
|
||||
|
||||
function toDataUrl(bytes: Buffer | Uint8Array, contentType: string): string {
|
||||
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
||||
return `data:${contentType};base64,${buf.toString("base64")}`;
|
||||
}
|
||||
|
||||
function guessContentType(url: string): string {
|
||||
const lower = url.toLowerCase();
|
||||
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (lower.endsWith(".webp")) return "image/webp";
|
||||
return "image/png";
|
||||
}
|
||||
Reference in New Issue
Block a user