import { createReadStream, createWriteStream } from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { Stats } from "node:fs"; import type { EntryType, FileEntry } from "../../shared/types"; import type { FileSystemProvider } from "./provider"; function typeOf(st: Stats): EntryType { if (st.isSymbolicLink()) return "symlink"; return st.isDirectory() ? "dir" : "file"; } /** * uid/gid -> names, cached per process (a listing usually has 1-3 owners). * * PORTABILITY: POSIX-only lookups (`id -nu` works on macOS and Linux; group * names use dscacheutil on macOS, getent elsewhere). On Windows these return * undefined and the Owner/Group columns show "—". */ const idNameCache = new Map(); function resolveId(kind: "user" | "group", id: number): string | undefined { if (process.platform === "win32") return undefined; const cacheKey = `${kind}:${id}`; if (idNameCache.has(cacheKey)) return idNameCache.get(cacheKey); let name: string | undefined; try { const cmd = kind === "user" ? ["id", "-nu", String(id)] : process.platform === "darwin" ? ["dscacheutil", "-q", "group", "-a", "gid", String(id)] : ["getent", "group", String(id)]; const proc = Bun.spawnSync(cmd, { stdout: "pipe", stderr: "ignore" }); if (proc.success) { const out = proc.stdout.toString(); if (kind === "user") name = out.trim() || undefined; else if (process.platform === "darwin") name = /^name:\s*(.+)$/m.exec(out)?.[1]?.trim(); else name = out.split(":")[0]?.trim() || undefined; } } catch { // lookup tool missing — fall through to the numeric id } name ??= String(id); idNameCache.set(cacheKey, name); return name; } function toEntry(name: string, full: string, st: Stats): FileEntry { return { name, path: full, type: typeOf(st), size: st.size, mtime: st.mtimeMs, mode: st.mode & 0o7777, owner: resolveId("user", st.uid), group: resolveId("group", st.gid), }; } export class LocalProvider implements FileSystemProvider { readonly kind = "local" as const; readonly fsKey = "local"; join = (...parts: string[]) => path.join(...parts); dirname = (p: string) => path.dirname(p); basename = (p: string) => path.basename(p); normalize = (p: string) => path.resolve(p); async homeDir(): Promise { return os.homedir(); } async list(dir: string): Promise { const names = await fs.readdir(dir); const entries: FileEntry[] = []; for (const name of names) { const full = path.join(dir, name); try { entries.push(toEntry(name, full, await fs.lstat(full))); } catch { // entry vanished or is unreadable — skip it } } return entries; } async stat(p: string): Promise { return toEntry(path.basename(p), p, await fs.lstat(p)); } async mkdir(p: string): Promise { await fs.mkdir(p); } async createEmptyFile(p: string): Promise { await fs.writeFile(p, "", { flag: "wx" }); } async rename(oldPath: string, newPath: string): Promise { await fs.rename(oldPath, newPath); } async deleteFile(p: string): Promise { await fs.unlink(p); } async deleteEmptyDir(p: string): Promise { await fs.rmdir(p); } async chmod(p: string, mode: number): Promise { await fs.chmod(p, mode); } createReadStream(p: string) { return createReadStream(p); } createWriteStream(p: string) { return createWriteStream(p); } async dispose(): Promise {} }