import { randomUUID } from "node:crypto"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { z } from "zod"; import { fetchImageFromUrl } from "../services/image.service"; const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; const MAX_SIZE = 5 * 1024 * 1024; // 5MB const fromUrlSchema = z.object({ url: z.string().url("Invalid URL") }); const app = new Hono(); app.post("/from-url", zValidator("json", fromUrlSchema), async (c) => { const { url } = c.req.valid("json"); try { const result = await fetchImageFromUrl(url); return c.json(result, 201); } catch (err) { const message = (err as Error).message; // Known validation errors from the service const isValidationError = message.startsWith("Invalid") || message.startsWith("URL must") || message.startsWith("File too") || message.startsWith("HTTP "); return c.json({ error: message }, isValidationError ? 400 : 500); } }); app.post("/", async (c) => { const body = await c.req.parseBody(); const file = body.image; if (!file || typeof file === "string") { return c.json({ error: "No image file provided" }, 400); } // Validate file type if (!ALLOWED_TYPES.includes(file.type)) { return c.json( { error: "Invalid file type. Accepted: jpeg, png, webp" }, 400, ); } // Validate file size if (file.size > MAX_SIZE) { return c.json({ error: "File too large. Maximum size is 5MB" }, 400); } // Generate unique filename const ext = file.type.split("/")[1] === "jpeg" ? "jpg" : file.type.split("/")[1]; const filename = `${Date.now()}-${randomUUID()}.${ext}`; // Ensure uploads directory exists await mkdir("uploads", { recursive: true }); // Write file const buffer = await file.arrayBuffer(); await Bun.write(join("uploads", filename), buffer); return c.json({ filename }, 201); }); export { app as imageRoutes };