fix(rsplat): serve staged images via /api/ path to avoid Cloudflare redirect

Cloudflare/Traefik 301-redirects /data/ paths to data.rspace.online, which
fal.ai can't follow. Staged images now served at /api/files/generated/ which
passes through correctly. Added route alias for backwards compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-16 16:59:25 -07:00
parent d4c0fdf7eb
commit 3c1802ab07
1 changed files with 14 additions and 9 deletions

View File

@ -185,8 +185,9 @@ app.get("/collect.js", async (c) => {
});
});
// ── Serve generated files from /data/files/generated/ ──
app.get("/data/files/generated/:filename", async (c) => {
// ── Serve generated files from /data/files/generated/ and /api/files/generated/ ──
// The /api/ route avoids Cloudflare/Traefik redirecting /data/ paths
function serveGeneratedFile(c: any) {
const filename = c.req.param("filename");
if (!filename || filename.includes("..") || filename.includes("/")) {
return c.json({ error: "Invalid filename" }, 400);
@ -194,11 +195,15 @@ app.get("/data/files/generated/:filename", async (c) => {
const dir = resolve(process.env.FILES_DIR || "./data/files", "generated");
const filePath = resolve(dir, filename);
const file = Bun.file(filePath);
if (!(await file.exists())) return c.notFound();
const ext = filename.split(".").pop() || "";
const mimeMap: Record<string, string> = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", glb: "model/gltf-binary", gltf: "model/gltf+json" };
return new Response(file, { headers: { "Content-Type": mimeMap[ext] || "application/octet-stream", "Cache-Control": "public, max-age=86400" } });
});
return file.exists().then((exists: boolean) => {
if (!exists) return c.notFound();
const ext = filename.split(".").pop() || "";
const mimeMap: Record<string, string> = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", glb: "model/gltf-binary", gltf: "model/gltf+json" };
return new Response(file, { headers: { "Content-Type": mimeMap[ext] || "application/octet-stream", "Cache-Control": "public, max-age=86400" } });
});
}
app.get("/data/files/generated/:filename", serveGeneratedFile);
app.get("/api/files/generated/:filename", serveGeneratedFile);
// ── Link preview / unfurl API ──
const linkPreviewCache = new Map<string, { title: string; description: string; image: string | null; domain: string; fetchedAt: number }>();
@ -1212,7 +1217,7 @@ app.post("/api/image-stage", async (c) => {
const dir = resolve(process.env.FILES_DIR || "./data/files", "generated");
const filename = `stage-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.jpg`;
await Bun.write(resolve(dir, filename), buf);
return c.json({ url: `${PUBLIC_ORIGIN}/data/files/generated/${filename}` });
return c.json({ url: `${PUBLIC_ORIGIN}/api/files/generated/${filename}` });
}
return c.json({ error: "Invalid upload format. Expected multipart/form-data." }, 400);
}
@ -1244,7 +1249,7 @@ app.post("/api/image-stage", async (c) => {
const buf = Buffer.from(await file.arrayBuffer());
await Bun.write(resolve(dir, filename), buf);
return c.json({ url: `${PUBLIC_ORIGIN}/data/files/generated/${filename}` });
return c.json({ url: `${PUBLIC_ORIGIN}/api/files/generated/${filename}` });
});
// Image-to-3D via fal.ai Hunyuan3D v2.1 (async job queue)