/** * Bob — the file SERVER. Mounts a virtual filesystem rooted in this * directory's `./files-root/` dir (created on first run) and serves it * over `@shade/files` E2EE RPC. * * Demonstrates: list/stat/mkdir/delete/move + read/write inline+streams. */ import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'; import { createReadStream } from 'node:fs'; import { Readable } from 'node:stream'; import { join, basename, dirname } from 'node:path'; import { createShade } from '@shade/sdk'; import { ConflictError, NotFoundError, type FileEntry, type UserReadResult, } from '@shade/files'; const ROOT = join(import.meta.dir, 'files-root'); await mkdir(ROOT, { recursive: true }); const PREKEY = 'http://localhost:9992'; const BOB_BASE_URL = 'http://localhost:9994'; const ALICE_BASE_URL = 'http://localhost:9993'; const bob = await createShade({ prekeyServer: PREKEY, address: 'bob' }); bob.configureTransfers({ resolveBaseUrl: async (peer) => { if (peer === 'alice') return ALICE_BASE_URL; throw new Error(`bob: unknown peer ${peer}`); }, }); const app = await bob.transferRoute(); Bun.serve({ port: 9994, fetch: app.fetch }); console.log(`[bob] listening on ${BOB_BASE_URL}, files at ${ROOT}`); function safePath(remote: string): string { // Strip leading slash + reject any '..' (the dispatcher already does // this, but we defend in depth). const segments = remote.split('/').filter((s) => s !== '' && s !== '..'); return join(ROOT, ...segments); } await bob.files.serve({ list: async (ctx) => { const local = safePath(ctx.path); let names: string[]; try { names = await readdir(local); } catch { throw new NotFoundError(ctx.path); } const entries: FileEntry[] = []; for (const name of names) { const s = await stat(join(local, name)); entries.push({ name, kind: s.isDirectory() ? 'dir' : 'file', size: s.isDirectory() ? 0 : s.size, mtime: s.mtimeMs, metadata: {}, }); } return { entries, hasMore: false }; }, stat: async (ctx) => { let s: import('node:fs').Stats; try { s = await stat(safePath(ctx.path)); } catch { throw new NotFoundError(ctx.path); } return { name: basename(ctx.path), kind: s.isDirectory() ? 'dir' : 'file', size: s.isDirectory() ? 0 : s.size, mtime: s.mtimeMs, metadata: {}, }; }, mkdir: async (ctx) => { const local = safePath(ctx.path); await mkdir(local, { recursive: ctx.args.recursive }); return { entry: { name: basename(ctx.path), kind: 'dir', size: 0, mtime: Date.now(), metadata: {}, }, }; }, delete: async (ctx) => { await rm(safePath(ctx.path), { recursive: ctx.args.recursive, force: false }); return { deletedCount: 1 }; }, move: async (ctx) => { await rename(safePath(ctx.args.src), safePath(ctx.args.dst)); return { entry: { name: basename(ctx.args.dst), kind: 'file', size: 0, mtime: Date.now(), metadata: {}, }, }; }, read: async (ctx): Promise => { const local = safePath(ctx.path); const s = await stat(local); if (s.isDirectory()) throw new ConflictError(`${ctx.path} is a directory`); if (s.size <= 256 * 1024) { const bytes = new Uint8Array(await readFile(local)); return { kind: 'inline', bytes }; } // Stream the file. const sha256 = await computeSha256OfFile(local); const stream = Readable.toWeb(createReadStream(local)) as ReadableStream; return { kind: 'streams', stream, size: s.size, sha256 }; }, write: async (ctx) => { const local = safePath(ctx.args.path); await mkdir(dirname(local), { recursive: true }); if (ctx.args.content.kind === 'inline') { await writeFile(local, ctx.args.content.bytes); } else { // Drain stream → temp buffer → file. For huge files use createWriteStream. const reader = ctx.args.content.stream.getReader(); const chunks: Uint8Array[] = []; let total = 0; while (true) { const { value, done } = await reader.read(); if (done) break; if (value !== undefined) { chunks.push(value); total += value.byteLength; } } reader.releaseLock(); const out = new Uint8Array(total); let offset = 0; for (const c of chunks) { out.set(c, offset); offset += c.byteLength; } await writeFile(local, out); // Verify integrity (optional) await ctx.args.content.sha256; } const s = await stat(local); return { entry: { name: basename(ctx.args.path), kind: 'file', size: s.size, mtime: s.mtimeMs, metadata: {}, }, }; }, }); async function computeSha256OfFile(path: string): Promise { const { sha256 } = await import('@noble/hashes/sha2.js'); const data = new Uint8Array(await readFile(path)); return Array.from(sha256(data), (b) => b.toString(16).padStart(2, '0')).join(''); }