many updates :-)
This commit is contained in:
@@ -3,6 +3,13 @@ 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
|
||||
|
||||
/**
|
||||
* Sentinel value stored in `ShopSettings.logoUrl` when the logo was uploaded
|
||||
* directly through the settings UI (rather than fetched from a remote URL).
|
||||
* The actual bytes live in `LogoCache` for that shop.
|
||||
*/
|
||||
export const STORED_LOGO_SENTINEL = "stored://shop-logo";
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -15,6 +22,12 @@ export async function getLogoDataUrl(
|
||||
if (!logoUrl) return undefined;
|
||||
|
||||
const cached = await db.logoCache.findUnique({ where: { shopDomain } });
|
||||
|
||||
// Locally uploaded logo: bytes live in LogoCache, no HTTP fetch.
|
||||
if (logoUrl === STORED_LOGO_SENTINEL) {
|
||||
return cached ? toDataUrl(cached.bytes, cached.contentType) : undefined;
|
||||
}
|
||||
|
||||
const isFresh =
|
||||
cached &&
|
||||
cached.sourceUrl === logoUrl &&
|
||||
@@ -65,3 +78,46 @@ function guessContentType(url: string): string {
|
||||
if (lower.endsWith(".webp")) return "image/webp";
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
const ALLOWED_LOGO_MIME = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
||||
|
||||
export interface StoreUploadedLogoResult {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
contentType?: string;
|
||||
byteLength?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists an uploaded logo file directly into `LogoCache`. Caller is
|
||||
* responsible for setting `ShopSettings.logoUrl = STORED_LOGO_SENTINEL`.
|
||||
*/
|
||||
export async function storeUploadedLogo(
|
||||
shopDomain: string,
|
||||
bytes: Buffer,
|
||||
contentType: string,
|
||||
): Promise<StoreUploadedLogoResult> {
|
||||
const ct = (contentType || "").toLowerCase();
|
||||
if (!ALLOWED_LOGO_MIME.has(ct)) {
|
||||
return { ok: false, error: `Unsupported image type "${contentType || "unknown"}". Use PNG, JPEG, WebP or GIF.` };
|
||||
}
|
||||
if (bytes.byteLength === 0) {
|
||||
return { ok: false, error: "Uploaded file is empty." };
|
||||
}
|
||||
if (bytes.byteLength > MAX_BYTES) {
|
||||
return { ok: false, error: `File too large (${(bytes.byteLength / 1024 / 1024).toFixed(2)} MB). Max is ${MAX_BYTES / 1024 / 1024} MB.` };
|
||||
}
|
||||
|
||||
const bytesU8 = new Uint8Array(bytes);
|
||||
await db.logoCache.upsert({
|
||||
where: { shopDomain },
|
||||
create: { shopDomain, sourceUrl: STORED_LOGO_SENTINEL, bytes: bytesU8, contentType: ct, etag: "" },
|
||||
update: { sourceUrl: STORED_LOGO_SENTINEL, bytes: bytesU8, contentType: ct, etag: "", fetchedAt: new Date() },
|
||||
});
|
||||
|
||||
return { ok: true, contentType: ct, byteLength: bytes.byteLength };
|
||||
}
|
||||
|
||||
export async function deleteStoredLogo(shopDomain: string): Promise<void> {
|
||||
await db.logoCache.deleteMany({ where: { shopDomain } });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user