feat: ship minecraft theme updates and platform workflow improvements
这个提交包含在:
@@ -8,6 +8,16 @@ export const runtime = "nodejs";
|
||||
|
||||
const CACHE_DIR = process.env.CSP_IMAGE_CACHE_DIR ?? "/tmp/csp-image-cache";
|
||||
const MAX_BYTES = 5 * 1024 * 1024;
|
||||
const IMAGE_EXT_TO_TYPE: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".bmp": "image/bmp",
|
||||
".ico": "image/x-icon",
|
||||
};
|
||||
|
||||
function toArrayBuffer(view: Uint8Array): ArrayBuffer {
|
||||
return view.buffer.slice(
|
||||
@@ -28,6 +38,25 @@ function pickExt(urlObj: URL, contentType: string): string {
|
||||
return ".img";
|
||||
}
|
||||
|
||||
function inferImageType(urlObj: URL, contentType: string): string {
|
||||
const raw = contentType.split(";")[0].trim().toLowerCase();
|
||||
if (raw.startsWith("image/")) return raw;
|
||||
const ext = path.extname(urlObj.pathname || "").toLowerCase();
|
||||
return IMAGE_EXT_TO_TYPE[ext] ?? raw;
|
||||
}
|
||||
|
||||
function looksLikeImage(urlObj: URL, contentType: string): boolean {
|
||||
if (contentType.startsWith("image/")) return true;
|
||||
const ext = path.extname(urlObj.pathname || "").toLowerCase();
|
||||
return Boolean(IMAGE_EXT_TO_TYPE[ext]);
|
||||
}
|
||||
|
||||
function redirectToTarget(target: URL): NextResponse {
|
||||
const res = NextResponse.redirect(target.toString(), 307);
|
||||
res.headers.set("Cache-Control", "no-store");
|
||||
return res;
|
||||
}
|
||||
|
||||
async function readCachedByKey(
|
||||
key: string
|
||||
): Promise<{ data: Uint8Array; contentType: string } | null> {
|
||||
@@ -94,14 +123,12 @@ export async function GET(req: NextRequest) {
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: `fetch image failed: HTTP ${resp.status}` },
|
||||
{ status: 502 }
|
||||
);
|
||||
return redirectToTarget(target);
|
||||
}
|
||||
|
||||
const contentType = (resp.headers.get("content-type") ?? "").toLowerCase();
|
||||
if (!contentType.startsWith("image/")) {
|
||||
const headerType = (resp.headers.get("content-type") ?? "").toLowerCase();
|
||||
const contentType = inferImageType(target, headerType);
|
||||
if (!looksLikeImage(target, contentType)) {
|
||||
return NextResponse.json({ ok: false, error: "url is not an image" }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -125,11 +152,8 @@ export async function GET(req: NextRequest) {
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: `fetch image failed: ${String(e)}` },
|
||||
{ status: 502 }
|
||||
);
|
||||
} catch {
|
||||
return redirectToTarget(target);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户