Add R2 upload and media serving endpoints

This commit is contained in:
2026-09-12 09:58:29 +08:00
parent 558fe0487b
commit 8d1ffa9ab5
2 changed files with 84 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
import type { APIRoute } from "astro";
import { getEnv } from "../../lib/env";
import { mediaUrl } from "../../lib/media";
export const prerender = false;
const SCOPES = new Set(["settings", "cases", "posts"]);
const ALLOWED: Record<string, string> = {
"image/webp": "webp",
"image/jpeg": "jpg",
"image/png": "png",
"image/gif": "gif",
};
const MAX_BYTES = 8 * 1024 * 1024;
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
export const POST: APIRoute = async ({ request }) => {
let form: FormData;
try {
form = await request.formData();
} catch {
return json({ ok: false, error: "無法讀取上傳內容。" }, 400);
}
const file = form.get("file");
const scope = String(form.get("scope") ?? "");
if (!(file instanceof File) || file.size === 0) {
return json({ ok: false, error: "請揀選圖片檔案。" }, 400);
}
if (!SCOPES.has(scope)) {
return json({ ok: false, error: "上傳目標唔正確。" }, 400);
}
const ext = ALLOWED[file.type];
if (!ext) {
return json({ ok: false, error: "只支援 JPG / PNG / WebP / GIF 圖片。" }, 400);
}
if (file.size > MAX_BYTES) {
return json({ ok: false, error: "圖片太大(上限 8MB)。" }, 400);
}
const key = `${scope}/${crypto.randomUUID()}.${ext}`;
try {
await getEnv().MEDIA.put(key, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type },
});
} catch {
return json({ ok: false, error: "上傳失敗,請稍後再試。" }, 500);
}
return json({ ok: true, url: mediaUrl(key) });
};
+24
View File
@@ -0,0 +1,24 @@
import type { APIRoute } from "astro";
import { getEnv } from "../../lib/env";
export const prerender = false;
export const GET: APIRoute = async ({ params, request }) => {
const key = params.key ?? "";
if (!key) return new Response("Not found", { status: 404 });
const obj = await getEnv().MEDIA.get(key);
if (!obj) return new Response("Not found", { status: 404 });
const etag = obj.httpEtag;
if (etag && request.headers.get("if-none-match") === etag) {
return new Response(null, { status: 304, headers: { ETag: etag } });
}
const headers = new Headers();
headers.set("Content-Type", obj.httpMetadata?.contentType ?? "application/octet-stream");
headers.set("Cache-Control", "public, max-age=31536000, immutable");
if (etag) headers.set("ETag", etag);
return new Response(obj.body, { headers });
};