Refactor sendFileToErugo function and types

This commit is contained in:
Kosztyk 2026-01-12 00:30:54 +02:00 committed by GitHub
parent 9cd47ceeb0
commit 5b25ed8a76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,44 +1,92 @@
import { import {
ERUGO_BASE_URL,
ERUGO_API_TOKEN, ERUGO_API_TOKEN,
ERUGO_DEFAULT_EXPIRY_HOURS, ERUGO_BASE_URL,
ERUGO_CONFIGURED, ERUGO_CONFIGURED,
ERUGO_DEFAULT_EXPIRY_HOURS,
} from "./env"; } from "./env";
/** export type ErugoShareOptions = {
* 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: {
fullPath: string; fullPath: string;
filename: string; filename: string;
/**
* Name shown in Erugo UI. If not provided, defaults to filename.
*/
shareName?: string; shareName?: string;
description?: string; /**
* Optional email of the recipient to notify.
*/
recipientEmail?: string; recipientEmail?: string;
recipientName?: string; /**
* Optional description / message.
*/
description?: string;
/**
* Optional expiry in hours. Defaults to ERUGO_DEFAULT_EXPIRY_HOURS.
*/
expiryHours?: number; expiryHours?: number;
}) { };
export type ErugoShareResponse = Record<string, unknown> & {
share_url: string | null;
};
function ensureConfigured() {
if (!ERUGO_CONFIGURED) { 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<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
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<ErugoShareResponse> {
ensureConfigured();
const { const {
fullPath, fullPath,
filename, filename,
shareName, shareName,
description,
recipientEmail, recipientEmail,
recipientName, description,
expiryHours, expiryHours,
} = options; } = options;
const url = const base = ERUGO_BASE_URL.replace(/\/$/, "");
`${ERUGO_BASE_URL.replace(/\/$/, "")}` + const url = `${base}/api/integrations/convertx/share`;
`/api/integrations/convertx/share`;
const form = new FormData(); const form = new FormData();
@ -46,55 +94,54 @@ export async function sendFileToErugo(options: {
const bunFile = Bun.file(fullPath); const bunFile = Bun.file(fullPath);
form.append("file", bunFile, filename); form.append("file", bunFile, filename);
// Share name (Erugo UI uses "name") // Name
form.append("name", (shareName?.trim() || filename).toString()); form.append("name", (shareName?.trim() || filename).toString());
// Optional description // Recipient email (if supported by Erugo)
if (description && description.trim()) { if (recipientEmail && recipientEmail.trim().length > 0) {
form.append("description", description.trim()); // Some Erugo versions may expect "email" or "recipient_email".
} // Include both to maximize compatibility.
form.append("email", recipientEmail.trim());
// Recipient (THIS is what triggers email in Erugo)
if (recipientEmail && recipientEmail.trim()) {
form.append("recipient_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 = const finalExpiry =
typeof expiryHours === "number" typeof expiryHours === "number" && Number.isFinite(expiryHours)
? expiryHours ? expiryHours
: ERUGO_DEFAULT_EXPIRY_HOURS; : ERUGO_DEFAULT_EXPIRY_HOURS;
// Support multiple field names across Erugo versions
form.append("expires_in_hours", String(finalExpiry)); form.append("expires_in_hours", String(finalExpiry));
form.append("expiry_hours", String(finalExpiry)); form.append("expiry_hours", String(finalExpiry));
// 🔍 LOG BEFORE REQUEST // Log request
console.log("[ConvertX] Erugo upload ->", { console.log("[ConvertX] Erugo upload ->", {
url, url,
filename, filename,
shareName, shareName: shareName?.trim() || filename,
hasRecipient: Boolean(recipientEmail), hasRecipient: Boolean(recipientEmail && recipientEmail.trim().length > 0),
recipientMasked: recipientEmail hasDescription: Boolean(description && description.trim().length > 0),
? recipientEmail.replace(/(.{2}).+(@.*)/, "$1***$2") expiryHours: finalExpiry,
: null,
}); });
const res = await fetch(url, { const res = await fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${ERUGO_API_TOKEN}`, Authorization: `Bearer ${ERUGO_API_TOKEN}`,
// DO NOT set Content-Type manually for FormData
}, },
body: form, body: form,
}); });
const text = await res.text(); const text = await res.text();
// 🔍 LOG RESPONSE // Log response
console.log("[ConvertX] Erugo response <-", { console.log("[ConvertX] Erugo response <-", {
status: res.status, status: res.status,
contentType: res.headers.get("content-type"), contentType: res.headers.get("content-type"),
@ -102,54 +149,26 @@ export async function sendFileToErugo(options: {
}); });
if (!res.ok) { if (!res.ok) {
throw new Error( throw new Error(`Erugo share failed: HTTP ${res.status} ${text.slice(0, 800)}`);
`Erugo share failed: HTTP ${res.status} ${text.slice(0, 800)}`,
);
} }
const get = (obj: unknown, key: string): unknown => { const parsed = safeJsonParse(text);
if (!obj || typeof obj !== "object") return undefined; const rec = getRecord(parsed) ?? { value: parsed };
return (obj as Record<string, unknown>)[key];
};
const getNested = (obj: unknown, keys: string[]): unknown => { const share_url = pickFirstString([
let cur: unknown = obj; // common direct fields
for (const k of keys) { getNested(rec, ["share_url"]),
cur = get(cur, k); getNested(rec, ["share_link"]),
if (cur === undefined || cur === null) return undefined; getNested(rec, ["url"]),
} // nested patterns
return cur; getNested(rec, ["data", "url"]),
}; getNested(rec, ["data", "share", "url"]),
getNested(rec, ["data", "share_url"]),
let jsonObj: Record<string, unknown> = {}; getNested(rec, ["data", "share_link"]),
]);
try {
const parsed: unknown = text ? JSON.parse(text) : null;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
jsonObj = parsed as Record<string, unknown>;
} 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;
return { return {
...jsonObj, ...rec,
share_url, share_url,
}; };
}