Add R2 upload and media serving endpoints
This commit is contained in:
@@ -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) });
|
||||
};
|
||||
@@ -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 });
|
||||
};
|
||||
Reference in New Issue
Block a user