import { THEME_CSS, escapeHtml, tabNavHTML } from "./render/layout"; import hljs from "highlight.js"; import { renderMarkdown } from "./render/markdown"; // Extension → hljs language map (mirrors fileview.ts for parity). const EXT_LANG: Record = { js: "javascript", mjs: "javascript", cjs: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript", mts: "typescript", cts: "typescript", py: "python", rb: "ruby", go: "go", rs: "rust", java: "java", kt: "kotlin", c: "c", h: "c", cpp: "cpp", cc: "cpp", cxx: "cpp", hpp: "cpp", hxx: "cpp", cs: "csharp", php: "php", swift: "swift", scala: "scala", sh: "bash", bash: "bash", zsh: "bash", fish: "bash", yml: "yaml", yaml: "yaml", json: "json", json5: "json", toml: "toml", xml: "xml", html: "xml", htm: "xml", css: "css", scss: "scss", less: "less", sql: "sql", dockerfile: "dockerfile", makefile: "makefile", lua: "lua", pl: "perl", }; function extToLang(p: string): string { const base = (p.split("/").pop() || "").toLowerCase(); if (base === "dockerfile" || base.endsWith(".dockerfile")) return "dockerfile"; if (base === "makefile" || base.endsWith(".mk")) return "makefile"; const m = base.match(/\.([a-z0-9]+)$/); const ext = m && m[1]; return ext ? (EXT_LANG[ext] || "") : ""; } function hljsHighlight(text: string, lang: string): string { try { return hljs.highlight(text, { language: lang }).value; } catch { return escapeHtml(text); } } export class RemoteFileError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = "RemoteFileError"; this.status = status; } } interface DirEntry { name: string; isDir: boolean; size: number; mtime: number; } interface DirListing { path: string; entries: DirEntry[]; } interface FileContent { path: string; size: number; rows: { n: number; t: string }[]; mode: string; byteCapped: boolean; note: string; } function fmtSize(n: number): string { if (n < 1024) return `${n}B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`; return `${(n / (1024 * 1024)).toFixed(1)}MB`; } function fmtMtime(ms: number): string { if (!ms) return ""; const diff = Date.now() - ms; if (diff < 60000) return "刚刚"; if (diff < 3600000) return `${Math.floor(diff / 60000)}分前`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}时前`; return new Date(ms).toISOString().slice(5, 16); } async function fetchDaemon( endpoint: string, apiPath: string, params: Record ): Promise { 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}

${rows}
名称大小修改时间
`; } 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(); }