From 5b25ed8a769c919012e041e557d50f02abf0355e Mon Sep 17 00:00:00 2001 From: Kosztyk <36381705+Kosztyk@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:30:54 +0200 Subject: [PATCH] Refactor sendFileToErugo function and types --- src/helpers/erugo.ts | 191 ++++++++++++++++++++++++------------------- 1 file changed, 105 insertions(+), 86 deletions(-) diff --git a/src/helpers/erugo.ts b/src/helpers/erugo.ts index 813449d..b7c6508 100644 --- a/src/helpers/erugo.ts +++ b/src/helpers/erugo.ts @@ -1,44 +1,92 @@ import { - ERUGO_BASE_URL, ERUGO_API_TOKEN, - ERUGO_DEFAULT_EXPIRY_HOURS, + ERUGO_BASE_URL, ERUGO_CONFIGURED, + ERUGO_DEFAULT_EXPIRY_HOURS, } from "./env"; -/** - * Send a local file to Erugo using Bun FormData. - * If recipient_email is provided, Erugo will send the share link via email - * (same behavior as Erugo UI). - */ -export async function sendFileToErugo(options: { +export type ErugoShareOptions = { fullPath: string; filename: string; - + /** + * Name shown in Erugo UI. If not provided, defaults to filename. + */ shareName?: string; - description?: string; - + /** + * Optional email of the recipient to notify. + */ recipientEmail?: string; - recipientName?: string; - + /** + * Optional description / message. + */ + description?: string; + /** + * Optional expiry in hours. Defaults to ERUGO_DEFAULT_EXPIRY_HOURS. + */ expiryHours?: number; -}) { +}; + +export type ErugoShareResponse = Record & { + share_url: string | null; +}; + +function ensureConfigured() { if (!ERUGO_CONFIGURED) { - throw new Error("Erugo integration is not configured"); + throw new Error( + "Erugo is not configured. Set ERUGO_BASE_URL and ERUGO_API_TOKEN.", + ); } +} + +function getRecord(value: unknown): Record | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record; +} + +function getNested(obj: unknown, path: string[]): unknown { + let cur: unknown = obj; + for (const key of path) { + const rec = getRecord(cur); + if (!rec) return undefined; + cur = rec[key]; + } + return cur; +} + +function pickFirstString(values: unknown[]): string | null { + for (const v of values) { + if (typeof v === "string" && v.trim().length > 0) return v; + } + return null; +} + +function safeJsonParse(text: string): unknown { + try { + return JSON.parse(text); + } catch { + return { raw: text }; + } +} + +/** + * Sends a file to Erugo and returns a normalized response that always includes `share_url`. + */ +export async function sendFileToErugo( + options: ErugoShareOptions, +): Promise { + ensureConfigured(); const { fullPath, filename, shareName, - description, recipientEmail, - recipientName, + description, expiryHours, } = options; - const url = - `${ERUGO_BASE_URL.replace(/\/$/, "")}` + - `/api/integrations/convertx/share`; + const base = ERUGO_BASE_URL.replace(/\/$/, ""); + const url = `${base}/api/integrations/convertx/share`; const form = new FormData(); @@ -46,55 +94,54 @@ export async function sendFileToErugo(options: { const bunFile = Bun.file(fullPath); form.append("file", bunFile, filename); - // Share name (Erugo UI uses "name") + // Name form.append("name", (shareName?.trim() || filename).toString()); - // Optional description - if (description && description.trim()) { - form.append("description", description.trim()); - } - - // Recipient (THIS is what triggers email in Erugo) - if (recipientEmail && recipientEmail.trim()) { + // Recipient email (if supported by Erugo) + if (recipientEmail && recipientEmail.trim().length > 0) { + // Some Erugo versions may expect "email" or "recipient_email". + // Include both to maximize compatibility. + form.append("email", recipientEmail.trim()); form.append("recipient_email", recipientEmail.trim()); - - if (recipientName && recipientName.trim()) { - form.append("recipient_name", recipientName.trim()); - } } - // Expiry (support both variants used in different Erugo versions) + // Description / message + if (description && description.trim().length > 0) { + form.append("description", description.trim()); + form.append("message", description.trim()); + } + + // Expiry const finalExpiry = - typeof expiryHours === "number" + typeof expiryHours === "number" && Number.isFinite(expiryHours) ? expiryHours : ERUGO_DEFAULT_EXPIRY_HOURS; + // Support multiple field names across Erugo versions form.append("expires_in_hours", String(finalExpiry)); form.append("expiry_hours", String(finalExpiry)); - // 🔍 LOG BEFORE REQUEST + // Log request console.log("[ConvertX] Erugo upload ->", { url, filename, - shareName, - hasRecipient: Boolean(recipientEmail), - recipientMasked: recipientEmail - ? recipientEmail.replace(/(.{2}).+(@.*)/, "$1***$2") - : null, + shareName: shareName?.trim() || filename, + hasRecipient: Boolean(recipientEmail && recipientEmail.trim().length > 0), + hasDescription: Boolean(description && description.trim().length > 0), + expiryHours: finalExpiry, }); const res = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${ERUGO_API_TOKEN}`, - // DO NOT set Content-Type manually for FormData }, body: form, }); const text = await res.text(); - // 🔍 LOG RESPONSE + // Log response console.log("[ConvertX] Erugo response <-", { status: res.status, contentType: res.headers.get("content-type"), @@ -102,54 +149,26 @@ export async function sendFileToErugo(options: { }); if (!res.ok) { - throw new Error( - `Erugo share failed: HTTP ${res.status} – ${text.slice(0, 800)}`, - ); + throw new Error(`Erugo share failed: HTTP ${res.status} – ${text.slice(0, 800)}`); } - const get = (obj: unknown, key: string): unknown => { - if (!obj || typeof obj !== "object") return undefined; - return (obj as Record)[key]; - }; + const parsed = safeJsonParse(text); + const rec = getRecord(parsed) ?? { value: parsed }; - const getNested = (obj: unknown, keys: string[]): unknown => { - let cur: unknown = obj; - for (const k of keys) { - cur = get(cur, k); - if (cur === undefined || cur === null) return undefined; - } - return cur; - }; - - let jsonObj: Record = {}; - - try { - const parsed: unknown = text ? JSON.parse(text) : null; - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { - jsonObj = parsed as Record; - } else { - jsonObj = { value: parsed }; - } - } catch { - jsonObj = { raw: text }; - } - - const shareUrlCandidates: unknown[] = [ - getNested(jsonObj, ["share_url"]), - getNested(jsonObj, ["share_link"]), - getNested(jsonObj, ["data", "url"]), - getNested(jsonObj, ["data", "share", "url"]), - getNested(jsonObj, ["data", "share_url"]), - getNested(jsonObj, ["data", "share_link"]), - ]; - - const share_url = - (shareUrlCandidates.find((v) => typeof v === "string") as string | undefined) ?? - null; + const share_url = pickFirstString([ + // common direct fields + getNested(rec, ["share_url"]), + getNested(rec, ["share_link"]), + getNested(rec, ["url"]), + // nested patterns + getNested(rec, ["data", "url"]), + getNested(rec, ["data", "share", "url"]), + getNested(rec, ["data", "share_url"]), + getNested(rec, ["data", "share_link"]), + ]); return { - ...jsonObj, + ...rec, share_url, }; - - +}