Refactor sendFileToErugo function and types
This commit is contained in:
parent
9cd47ceeb0
commit
5b25ed8a76
1 changed files with 105 additions and 86 deletions
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue