/** * Server-side image generation and file upload helpers. * * These functions require filesystem access and FAL_KEY, * so they stay server-only (not bundled into web components). */ import { resolve } from "node:path"; import { mkdir, writeFile, unlink } from "node:fs/promises"; const GEN_DIR = resolve(process.env.FILES_DIR || "./data/files", "generated"); async function ensureGenDir(): Promise { await mkdir(GEN_DIR, { recursive: true }); return GEN_DIR; } // ── fal.ai image generation ── export async function generateImageFromPrompt(prompt: string): Promise { const FAL_KEY = process.env.FAL_KEY || ""; if (!FAL_KEY) return null; const falRes = await fetch("https://fal.run/fal-ai/flux-pro/v1.1", { method: "POST", headers: { Authorization: `Key ${FAL_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ prompt, image_size: "landscape_4_3", num_images: 1, safety_tolerance: "2", }), }); if (!falRes.ok) { console.error("[image-gen] fal.ai error:", await falRes.text()); return null; } const falData = await falRes.json() as { images?: { url: string }[]; output?: { url: string } }; return falData.images?.[0]?.url || falData.output?.url || null; } export async function downloadAndSaveImage(cdnUrl: string, filename: string): Promise { const imgRes = await fetch(cdnUrl); if (!imgRes.ok) return null; const imgBuffer = await imgRes.arrayBuffer(); const dir = await ensureGenDir(); await writeFile(resolve(dir, filename), Buffer.from(imgBuffer)); return `/data/files/generated/${filename}`; } // ── File upload handling ── const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"]; const MAX_SIZE = 5 * 1024 * 1024; // 5MB export function validateImageFile(file: File): string | null { if (!ALLOWED_TYPES.includes(file.type)) { return "Invalid file type. Allowed: png, jpg, webp, gif"; } if (file.size > MAX_SIZE) { return "File too large. Maximum 5MB"; } return null; } export function safeExtension(filename: string): string { const ext = filename.split(".").pop()?.toLowerCase() || "png"; return ["png", "jpg", "jpeg", "webp", "gif"].includes(ext) ? ext : "png"; } export async function saveUploadedFile(buffer: Buffer, filename: string): Promise { const dir = await ensureGenDir(); await writeFile(resolve(dir, filename), buffer); return `/data/files/generated/${filename}`; } // ── Cleanup helpers ── export async function deleteImageFile(imageUrl: string): Promise { const fname = imageUrl.split("/").pop(); if (!fname) return; try { await unlink(resolve(GEN_DIR, fname)); } catch { /* ignore */ } } export async function deleteOldImage(oldUrl: string | undefined, newFilename: string): Promise { if (!oldUrl) return; const oldFilename = oldUrl.split("/").pop(); if (oldFilename && oldFilename !== newFilename) { try { await unlink(resolve(GEN_DIR, oldFilename)); } catch { /* ignore */ } } }