Browse Source

fix: save 3D models with correct file extension based on URL

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 <noreply@anthropic.com>
handoff-20260429-1057
gitcapoom 5 months ago
parent
commit
2e15a25eb9
  1. 46
      src/app/api/save-generation/route.ts

46
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

Loading…
Cancel
Save