import { randomUUID } from "node:crypto"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; const MAX_SIZE = 5 * 1024 * 1024; // 5MB const FETCH_TIMEOUT = 10_000; // 10 seconds interface FetchImageResult { filename: string; sourceUrl: string; } export async function fetchImageFromUrl( url: string, uploadsDir = "uploads", ): Promise { // Validate URL format let parsedUrl: URL; try { parsedUrl = new URL(url); } catch { throw new Error("Invalid URL format"); } if (!["http:", "https:"].includes(parsedUrl.protocol)) { throw new Error("URL must use HTTP or HTTPS"); } // Fetch with timeout const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT); let response: Response; try { response = await fetch(url, { signal: controller.signal }); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { throw new Error("Request timed out"); } throw new Error(`Failed to fetch image: ${(err as Error).message}`); } finally { clearTimeout(timeout); } if (!response.ok) { throw new Error(`HTTP ${response.status}: Failed to fetch image`); } // Validate content type const contentType = response.headers .get("content-type") ?.split(";")[0] .trim(); if (!contentType || !ALLOWED_TYPES.includes(contentType)) { throw new Error( `Invalid content type: ${contentType ?? "unknown"}. Accepted: jpeg, png, webp`, ); } // Check content length if available const contentLength = response.headers.get("content-length"); if (contentLength && Number.parseInt(contentLength, 10) > MAX_SIZE) { throw new Error("File too large. Maximum size is 5MB"); } // Read body and check actual size const buffer = await response.arrayBuffer(); if (buffer.byteLength > MAX_SIZE) { throw new Error("File too large. Maximum size is 5MB"); } // Determine extension const ext = contentType === "image/jpeg" ? "jpg" : contentType.split("/")[1]; const filename = `${Date.now()}-${randomUUID()}.${ext}`; // Ensure directory exists and write await mkdir(uploadsDir, { recursive: true }); await Bun.write(join(uploadsDir, filename), buffer); return { filename, sourceUrl: url }; }