P1: typed RPC bridge + FileSystemProvider + dual-panel local browsing
- shared/types + shared/rpc: full wire contract (Result envelope, no exceptions cross the bridge) - FileSystemProvider interface, LocalProvider, SessionRegistry with built-in 'local' - walk.ts: provider-generic recursive walk + removeRecursive - rpc-handlers wired into BrowserWindow; 10min request timeout for slow SFTP ops - Angular: RpcService (Electroview bridge + plain-browser guard), PanelsService with signals-based PanelState (sort/cursor/selection), FilePanel component with keyboard nav (arrows/Space/Enter/Backspace/Tab), path bar, toasts - verified: both panels listDir home over the bridge in the running app Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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";
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export class LocalProvider implements FileSystemProvider {
|
||||
readonly kind = "local" as const;
|
||||
|
||||
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<string> {
|
||||
return os.homedir();
|
||||
}
|
||||
|
||||
async list(dir: string): Promise<FileEntry[]> {
|
||||
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<FileEntry> {
|
||||
return toEntry(path.basename(p), p, await fs.lstat(p));
|
||||
}
|
||||
|
||||
async mkdir(p: string): Promise<void> {
|
||||
await fs.mkdir(p);
|
||||
}
|
||||
|
||||
async createEmptyFile(p: string): Promise<void> {
|
||||
await fs.writeFile(p, "", { flag: "wx" });
|
||||
}
|
||||
|
||||
async rename(oldPath: string, newPath: string): Promise<void> {
|
||||
await fs.rename(oldPath, newPath);
|
||||
}
|
||||
|
||||
async deleteFile(p: string): Promise<void> {
|
||||
await fs.unlink(p);
|
||||
}
|
||||
|
||||
async deleteEmptyDir(p: string): Promise<void> {
|
||||
await fs.rmdir(p);
|
||||
}
|
||||
|
||||
async chmod(p: string, mode: number): Promise<void> {
|
||||
await fs.chmod(p, mode);
|
||||
}
|
||||
|
||||
createReadStream(p: string) {
|
||||
return createReadStream(p);
|
||||
}
|
||||
|
||||
createWriteStream(p: string) {
|
||||
return createWriteStream(p);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {}
|
||||
}
|
||||
Reference in New Issue
Block a user