{
const base = /^https?:\/\//.test(endpoint) ? endpoint : `http://${endpoint}`;
const url = new URL(`${base}${apiPath}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const resp = await fetch(url.toString(), { signal: AbortSignal.timeout(10_000) });
return resp;
}
export async function browseRemoteFile(
endpoint: string,
rawPath: string,
mode: string,
order: string,
viewerLogin?: string
): Promise<{ html: string }> {
const daemonLabel = escapeHtml(endpoint);
const listResp = await fetchDaemon(endpoint, "/api/files/list", { path: rawPath });
if (listResp.ok) {
const data = (await listResp.json()) as DirListing;
return { html: renderDirListing(data, endpoint, daemonLabel, viewerLogin) };
}
if (listResp.status !== 400) {
const err = await listResp.json().catch(() => ({ error: "unknown" })) as { error?: string };
throw new RemoteFileError(err.error ?? `daemon returned ${listResp.status}`, listResp.status);
}
const readResp = await fetchDaemon(endpoint, "/api/files/read", {
path: rawPath,
mode: mode === "head" ? "head" : "tail",
order: order === "asc" ? "asc" : "desc",
});
if (!readResp.ok) {
const err = await readResp.json().catch(() => ({ error: "unknown" })) as { error?: string };
throw new RemoteFileError(err.error ?? `daemon returned ${readResp.status}`, readResp.status);
}
const data = (await readResp.json()) as FileContent;
return { html: renderFileContent(data, rawPath, endpoint, daemonLabel, viewerLogin) };
}
function renderDirListing(
data: DirListing,
endpoint: string,
daemonLabel: string,
viewerLogin?: string
): string {
const enc = encodeURIComponent;
const daemonParam = `&daemon=${enc(endpoint)}`;
const parentPath = data.path.split("/").slice(0, -1).join("/") || "/";
const rows = data.entries.map((e) => {
const childPath = `${data.path.replace(/\/$/, "")}/${e.name}`;
const icon = e.isDir ? "📁" : "📄";
const size = e.isDir ? "—" : fmtSize(e.size);
return `
| ${icon} ${escapeHtml(e.name)} |
${size} |
${fmtMtime(e.mtime)} |
`;
}).join("") || `| 空目录 |
`;
const user = viewerLogin ? { login: viewerLogin, is_admin: 0 } : undefined;
return `
ework-web · ${escapeHtml(data.path)}
${tabNavHTML("sessions", user)}
${escapeHtml(data.path)}远程 ${daemonLabel}
`;
}
function renderFileContent(
data: FileContent,
rawPath: string,
endpoint: string,
daemonLabel: string,
viewerLogin?: string
): string {
const enc = encodeURIComponent;
const daemonParam = `&daemon=${enc(endpoint)}`;
const user = viewerLogin ? { login: viewerLogin, is_admin: 0 } : undefined;
const ext = (rawPath.split(".").pop() || "").toLowerCase();
const isMd = ext === "md" || ext === "markdown";
const lang = extToLang(rawPath);
const sorted = [...data.rows].sort((a, b) => a.n - b.n);
const fullText = sorted.map((r) => r.t).join("\n");
const shownBytes = sorted.reduce((s, r) => s + r.t.length + 1, 0);
let body: string;
if (isMd) {
body = `${renderMarkdown(fullText, "")}
`;
} else if (lang && shownBytes <= 256000) {
const nums = sorted.map((r) => r.n).join("\n");
body = `${escapeHtml(nums)}${hljsHighlight(fullText, lang)}
`;
} else {
body = `${sorted.map((r) => `${r.n}${escapeHtml(r.t)}`).join("")}
`;
}
return `
ework-web · ${escapeHtml(data.path)}
${tabNavHTML("sessions", user)}
${escapeHtml(data.path)}远程 ${daemonLabel}
${data.note ? `${escapeHtml(data.note)}
` : ""}
${body}
`;
}
export async function proxyFileSince(
endpoint: string,
rawPath: string,
after: number
): Promise<{ rows: { n: number; t: string }[]; size: number; rotated: boolean; capped: boolean }> {
const resp = await fetchDaemon(endpoint, "/api/files/since", {
path: rawPath,
after: String(after),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({ error: "unknown" })) as { error?: string };
throw new RemoteFileError(err.error ?? `daemon returned ${resp.status}`, resp.status);
}
return await resp.json();
}