From 2e15a25eb9e660d05f20767179cce28db76e1060 Mon Sep 17 00:00:00 2001 From: gitcapoom Date: Wed, 18 Feb 2026 09:36:29 +0300 Subject: [PATCH] fix: save 3D models with correct file extension based on URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract file extension from fal.ai CDN URLs instead of always defaulting to .glb. Adds getExtensionFromUrl() helper that recognizes glb, gltf, obj, fbx, usdz, stl, ply, and zip extensions. Also expands MIME type mappings for additional 3D formats. Priority chain: URL extension → content-type header → MIME fallback → .glb Co-Authored-By: Claude Opus 4.6 --- src/app/api/save-generation/route.ts | 46 +++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/app/api/save-generation/route.ts b/src/app/api/save-generation/route.ts index 01443f35..52460966 100644 --- a/src/app/api/save-generation/route.ts +++ b/src/app/api/save-generation/route.ts @@ -16,6 +16,11 @@ function getExtensionFromMime(mimeType: string): string { "video/webm": "webm", "video/quicktime": "mov", "model/gltf-binary": "glb", + "model/gltf+json": "gltf", + "model/obj": "obj", + "model/vnd.usdz+zip": "usdz", + "model/fbx": "fbx", + "model/stl": "stl", }; // Check explicit mapping first @@ -43,6 +48,25 @@ function isHttpUrl(str: string): boolean { return str.startsWith("http://") || str.startsWith("https://"); } +// Known file extensions for 3D models and common media +const KNOWN_3D_EXTENSIONS = new Set(["glb", "gltf", "obj", "fbx", "usdz", "stl", "ply", "zip"]); +const KNOWN_MEDIA_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "mp4", "webm", "mov"]); + +// Helper to extract a recognized file extension from a URL pathname +function getExtensionFromUrl(url: string): string | null { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const lastDot = pathname.lastIndexOf("."); + if (lastDot === -1 || lastDot === pathname.length - 1) return null; + const ext = pathname.substring(lastDot + 1).toLowerCase(); + if (KNOWN_3D_EXTENSIONS.has(ext) || KNOWN_MEDIA_EXTENSIONS.has(ext)) return ext; + return null; + } catch { + return null; + } +} + // Helper to compute MD5 hash of buffer content function computeContentHash(buffer: Buffer): string { return crypto.createHash("md5").update(buffer).digest("hex"); @@ -173,10 +197,18 @@ export async function POST(request: NextRequest) { } const rawSaveContentType = response.headers.get("content-type"); - const contentType = (rawSaveContentType && (rawSaveContentType.startsWith("video/") || rawSaveContentType.startsWith("image/") || rawSaveContentType.startsWith("model/"))) - ? rawSaveContentType - : (isModel ? "model/gltf-binary" : isVideo ? "video/mp4" : "image/png"); - extension = getExtensionFromMime(contentType); + + // For 3D models, try extracting extension from URL first (most reliable with CDN URLs) + const urlExtension = isModel ? getExtensionFromUrl(content) : null; + + if (urlExtension) { + extension = urlExtension; + } else { + const contentType = (rawSaveContentType && (rawSaveContentType.startsWith("video/") || rawSaveContentType.startsWith("image/") || rawSaveContentType.startsWith("model/"))) + ? rawSaveContentType + : (isModel ? "model/gltf-binary" : isVideo ? "video/mp4" : "image/png"); + extension = getExtensionFromMime(contentType); + } const arrayBuffer = await response.arrayBuffer(); @@ -210,7 +242,11 @@ export async function POST(request: NextRequest) { // Safety net: if extension resolved to "bin" but we know the media type, use correct extension if (extension === "bin") { - extension = isModel ? "glb" : isVideo ? "mp4" : "png"; + if (isModel && isHttpUrl(content)) { + extension = getExtensionFromUrl(content) || "glb"; + } else { + extension = isModel ? "glb" : isVideo ? "mp4" : "png"; + } } // Compute content hash for deduplication