CODE · 2.8 KB
api/download-banner.js
Workspace snapshot · 09/04 13:35
import { callRuntimeRpc, RUNTIME_SERVICE_ID, runtimeEnvironment } from "./banner-core.js";
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const ALLOWED_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/webp"]);
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
export function safeDownloadName(value = "banner.png") {
const base = String(value)
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-")
.replace(/\s+/g, " ")
.trim()
.slice(0, 120) || "banner";
return /\.(png|jpe?g|webp)$/i.test(base) ? base : `${base}.png`;
}
export function isAllowedArtifactUrl(value) {
try {
const url = new URL(value);
return url.protocol === "https:" && url.hostname.endsWith(".supabase.co");
} catch {
return false;
}
}
export default async function handler(req, res) {
if (req.method !== "GET") return res.status(405).json({ error: "GETのみ利用できます" });
const jobId = String(req.query?.jobId || "");
if (!UUID_PATTERN.test(jobId)) return res.status(400).json({ error: "有効なjobIdが必要です" });
const runtime = runtimeEnvironment();
if (!runtime.connected) return res.status(503).json({ error: "画像生成基盤が未接続です" });
try {
const job = await callRuntimeRpc({
rpc: "avs_llm_public_get_job",
payload: { p_service_id: RUNTIME_SERVICE_ID, p_token: runtime.token, p_job_id: jobId },
});
const artifactUrl = job?.status === "COMPLETED" ? job.output?.artifacts?.[0]?.signedUrl : null;
if (!isAllowedArtifactUrl(artifactUrl)) return res.status(404).json({ error: "ダウンロード可能な画像がありません" });
const imageResponse = await fetch(artifactUrl);
if (!imageResponse.ok) return res.status(502).json({ error: "画像を取得できませんでした" });
const contentType = String(imageResponse.headers.get("content-type") || "").split(";")[0].toLowerCase();
if (!ALLOWED_IMAGE_TYPES.has(contentType)) return res.status(502).json({ error: "画像形式を確認できませんでした" });
const bytes = Buffer.from(await imageResponse.arrayBuffer());
if (!bytes.length || bytes.length > MAX_IMAGE_BYTES) return res.status(502).json({ error: "画像サイズが不正です" });
const filename = safeDownloadName(req.query?.filename);
const asciiFallback = filename.replace(/[^\x20-\x7e]/g, "_");
res.setHeader("Content-Type", contentType);
res.setHeader("Content-Length", String(bytes.length));
res.setHeader("Cache-Control", "private, no-store");
res.setHeader("Content-Disposition", `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`);
return res.status(200).send(bytes);
} catch (error) {
return res.status(error.status || 502).json({ error: error.message || "画像をダウンロードできませんでした" });
}
}