P4: SFTP provider, connection store, connection manager UI
- SftpProvider (ssh2): password + key auth, keepalive, clean-dispose vs unexpected-close distinction, POSIX path helpers, s->ms attr normalization - ConnectionStore: plain JSON at appDataDir, 0600, atomic writes, folders - connect/disconnect + store CRUD handlers; sessionClosed push -> panels fall back to local and in-flight jobs fail - UI: session dropdown per panel (local/saved connections/manage), disconnect button, connection manager modal (folders, edit form, save/connect) - electrobun bundler: external cpu-features/*.node (ssh2 optional natives) - RpcService: gate requests on RPC socket open; defer initial panel loads past first render - verified against 192.168.10.122: full op matrix, upload/download/remote-remote recursive transfers, roundtrip integrity, recursive remote delete, store CRUD Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+57
-14
@@ -2,6 +2,8 @@ import { BrowserView } from "electrobun/bun";
|
||||
import type { AppRPC } from "../shared/rpc";
|
||||
import type { Result } from "../shared/types";
|
||||
import type { SessionRegistry } from "./fs/registry";
|
||||
import { SftpProvider } from "./fs/sftp";
|
||||
import type { ConnectionStore } from "./store/connections";
|
||||
import type { TransferEngine } from "./transfers/engine";
|
||||
import { removeRecursive } from "./transfers/walk";
|
||||
|
||||
@@ -9,6 +11,14 @@ import { removeRecursive } from "./transfers/walk";
|
||||
// default would cut them off.
|
||||
const MAX_REQUEST_TIME_MS = 10 * 60_000;
|
||||
|
||||
export interface AppDeps {
|
||||
registry: SessionRegistry;
|
||||
engine: TransferEngine;
|
||||
store: ConnectionStore;
|
||||
/** Assigned after the rpc object exists (it needs rpc.send). */
|
||||
events: { sessionClosed: (info: { sessionId: string; reason: string }) => void };
|
||||
}
|
||||
|
||||
async function wrap<T extends object>(fn: () => Promise<T> | T): Promise<Result<T>> {
|
||||
try {
|
||||
return { ok: true, ...(await fn()) };
|
||||
@@ -17,16 +27,37 @@ async function wrap<T extends object>(fn: () => Promise<T> | T): Promise<Result<
|
||||
}
|
||||
}
|
||||
|
||||
const NOT_YET = { ok: false as const, error: "Not implemented yet" };
|
||||
|
||||
export function createAppRPC(registry: SessionRegistry, engine: TransferEngine) {
|
||||
export function createAppRPC({ registry, engine, store, events }: AppDeps) {
|
||||
return BrowserView.defineRPC<AppRPC>({
|
||||
maxRequestTime: MAX_REQUEST_TIME_MS,
|
||||
handlers: {
|
||||
requests: {
|
||||
// ---- sessions ----
|
||||
connect: ({ config }) =>
|
||||
wrap(async () => {
|
||||
let sessionId = "";
|
||||
const provider = await SftpProvider.connect(config, (reason) => {
|
||||
if (!sessionId) return;
|
||||
registry.drop(sessionId);
|
||||
engine.failSession(sessionId, reason);
|
||||
events.sessionClosed({ sessionId, reason });
|
||||
});
|
||||
sessionId = registry.add(provider);
|
||||
const homeDir = await provider.homeDir();
|
||||
console.log(`[rpc] connected ${sessionId} -> ${config.username}@${config.host}`);
|
||||
return { sessionId, homeDir };
|
||||
}),
|
||||
|
||||
disconnect: ({ sessionId }) =>
|
||||
wrap(async () => {
|
||||
await registry.remove(sessionId);
|
||||
console.log(`[rpc] disconnected ${sessionId}`);
|
||||
return {};
|
||||
}),
|
||||
|
||||
// ---- filesystem ----
|
||||
listDir: ({ sessionId, path }) =>
|
||||
wrap(async () => {
|
||||
console.log(`[rpc] listDir ${sessionId} ${path ?? "(home)"}`);
|
||||
const fs = registry.get(sessionId);
|
||||
const dir = fs.normalize(path ?? (await fs.homeDir()));
|
||||
return { path: dir, entries: await fs.list(dir) };
|
||||
@@ -68,10 +99,7 @@ export function createAppRPC(registry: SessionRegistry, engine: TransferEngine)
|
||||
return {};
|
||||
}),
|
||||
|
||||
// P4: SFTP sessions
|
||||
connect: async () => NOT_YET,
|
||||
disconnect: async () => NOT_YET,
|
||||
|
||||
// ---- transfers ----
|
||||
enqueueTransfers: (params) =>
|
||||
wrap(async () => ({ jobIds: engine.enqueue(params) })),
|
||||
|
||||
@@ -86,12 +114,27 @@ export function createAppRPC(registry: SessionRegistry, engine: TransferEngine)
|
||||
engine.dismiss(jobId);
|
||||
return {};
|
||||
}),
|
||||
// P4: connection store
|
||||
getConnections: async () => NOT_YET,
|
||||
upsertConnection: async () => NOT_YET,
|
||||
deleteConnection: async () => NOT_YET,
|
||||
upsertFolder: async () => NOT_YET,
|
||||
deleteFolder: async () => NOT_YET,
|
||||
|
||||
// ---- connection store ----
|
||||
getConnections: () => wrap(async () => ({ store: await store.load() })),
|
||||
|
||||
upsertConnection: ({ connection }) =>
|
||||
wrap(async () => ({ id: await store.upsertConnection(connection) })),
|
||||
|
||||
deleteConnection: ({ id }) =>
|
||||
wrap(async () => {
|
||||
await store.deleteConnection(id);
|
||||
return {};
|
||||
}),
|
||||
|
||||
upsertFolder: ({ folder }) =>
|
||||
wrap(async () => ({ id: await store.upsertFolder(folder) })),
|
||||
|
||||
deleteFolder: ({ id }) =>
|
||||
wrap(async () => {
|
||||
await store.deleteFolder(id);
|
||||
return {};
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user