import * as plugins from './plugins.js'; import * as paths from './paths.js'; export type TSambaShareAccess = 'read' | 'readWrite'; export interface ISambaAuthOptions { username: string; password: string; domain?: string; } export interface ISambaClientOptions { host: string; port?: number; auth?: Partial; timeoutMs?: number; compression?: boolean; dfsEnabled?: boolean; } export interface ISambaServerUser { username: string; password: string; } export interface ISambaServerShareUser { username: string; access?: TSambaShareAccess; } export interface ISambaServerShare { name: string; path: string; readOnly?: boolean; public?: boolean; users?: ISambaServerShareUser[]; createIfMissing?: boolean; } export interface ISambaServerOptions { host?: string; port?: number; netbiosName?: string; users?: ISambaServerUser[]; shares: ISambaServerShare[]; } export interface ISambaServerStartResult { host: string; port: number; address: string; shares: string[]; } export interface ISambaServerStatus { running: boolean; host?: string; port?: number; address?: string; shares: string[]; } export interface ISambaDirectoryEntry { name: string; size: number; isDirectory: boolean; createdFiletime: number; modifiedFiletime: number; } export interface ISambaFileInfo { size: number; isDirectory: boolean; createdFiletime: number; modifiedFiletime: number; accessedFiletime: number; } export interface ISambaShareInfo { name: string; shareType: number; comment: string; } interface IRustSambaConnectionConfig { host: string; port?: number; username?: string; password?: string; domain?: string; timeoutMs?: number; compression?: boolean; dfsEnabled?: boolean; } type TRustSambaCommands = { startServer: { params: { config: ISambaServerOptions }; result: ISambaServerStartResult }; stopServer: { params: Record; result: Record }; getServerStatus: { params: Record; result: ISambaServerStatus }; listShares: { params: { connection: IRustSambaConnectionConfig }; result: { shares: ISambaShareInfo[] } }; listDirectory: { params: { connection: IRustSambaConnectionConfig; share: string; path: string }; result: { entries: ISambaDirectoryEntry[] }; }; readFile: { params: { connection: IRustSambaConnectionConfig; share: string; path: string }; result: { dataBase64: string; size: number }; }; writeFile: { params: { connection: IRustSambaConnectionConfig; share: string; path: string; dataBase64: string }; result: { bytesWritten: number }; }; deleteFile: { params: { connection: IRustSambaConnectionConfig; share: string; path: string }; result: Record; }; createDirectory: { params: { connection: IRustSambaConnectionConfig; share: string; path: string }; result: Record; }; rename: { params: { connection: IRustSambaConnectionConfig; share: string; from: string; to: string }; result: Record; }; stat: { params: { connection: IRustSambaConnectionConfig; share: string; path: string }; result: ISambaFileInfo; }; }; function getTsrustPlatformSuffix(): string | null { const archMap: Record = { x64: 'amd64', arm64: 'arm64' }; const osMap: Record = { linux: 'linux', darwin: 'macos' }; const os = osMap[process.platform]; const arch = archMap[process.arch]; return os && arch ? `${os}_${arch}` : null; } function buildLocalRustPaths(): string[] { const suffix = getTsrustPlatformSuffix(); const localPaths: string[] = []; if (suffix) { localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', `rustsamba_${suffix}`)); } localPaths.push(plugins.path.join(paths.packageDir, 'dist_rust', 'rustsamba')); localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'release', 'rustsamba')); localPaths.push(plugins.path.join(paths.packageDir, 'rust', 'target', 'debug', 'rustsamba')); return localPaths; } class SambaBridge { private bridge = new plugins.smartrust.RustBridge({ binaryName: 'rustsamba', envVarName: 'SMARTSAMBA_RUST_BINARY', platformPackagePrefix: '@push.rocks/smartsamba', localPaths: buildLocalRustPaths(), readyTimeoutMs: 30000, requestTimeoutMs: 300000, maxPayloadSize: 128 * 1024 * 1024, }); public async ensureRunning(): Promise { if (this.bridge.running) { return; } const spawned = await this.bridge.spawn(); if (!spawned) { throw new Error('Failed to spawn rustsamba binary. Run pnpm build or pnpm run test:before first.'); } } public async sendCommand( method: K, params: TRustSambaCommands[K]['params'], ): Promise { await this.ensureRunning(); return this.bridge.sendCommand(method, params); } public kill(): void { this.bridge.kill(); } } function normalizeClientOptions(optionsArg: ISambaClientOptions): IRustSambaConnectionConfig { return { host: optionsArg.host, ...(optionsArg.port ? { port: optionsArg.port } : {}), ...(optionsArg.auth?.username ? { username: optionsArg.auth.username } : {}), ...(optionsArg.auth?.password ? { password: optionsArg.auth.password } : {}), ...(optionsArg.auth?.domain ? { domain: optionsArg.auth.domain } : {}), ...(optionsArg.timeoutMs ? { timeoutMs: optionsArg.timeoutMs } : {}), ...(typeof optionsArg.compression === 'boolean' ? { compression: optionsArg.compression } : {}), ...(typeof optionsArg.dfsEnabled === 'boolean' ? { dfsEnabled: optionsArg.dfsEnabled } : {}), }; } export class SambaClient { private bridge = new SambaBridge(); private connection: IRustSambaConnectionConfig; constructor(optionsArg: ISambaClientOptions) { this.connection = normalizeClientOptions(optionsArg); } public async start(): Promise { await this.bridge.ensureRunning(); } public async stop(): Promise { this.bridge.kill(); } public async listShares(): Promise { const result = await this.bridge.sendCommand('listShares', { connection: this.connection }); return result.shares; } public async listDirectory(shareArg: string, pathArg = ''): Promise { const result = await this.bridge.sendCommand('listDirectory', { connection: this.connection, share: shareArg, path: pathArg, }); return result.entries; } public async readFile(shareArg: string, pathArg: string): Promise { const result = await this.bridge.sendCommand('readFile', { connection: this.connection, share: shareArg, path: pathArg, }); return plugins.buffer.Buffer.from(result.dataBase64, 'base64'); } public async readFileAsString(shareArg: string, pathArg: string, encoding: BufferEncoding = 'utf8') { const buffer = await this.readFile(shareArg, pathArg); return buffer.toString(encoding); } public async writeFile(shareArg: string, pathArg: string, dataArg: Buffer | string): Promise { const buffer = typeof dataArg === 'string' ? plugins.buffer.Buffer.from(dataArg) : dataArg; const result = await this.bridge.sendCommand('writeFile', { connection: this.connection, share: shareArg, path: pathArg, dataBase64: buffer.toString('base64'), }); return result.bytesWritten; } public async deleteFile(shareArg: string, pathArg: string): Promise { await this.bridge.sendCommand('deleteFile', { connection: this.connection, share: shareArg, path: pathArg, }); } public async createDirectory(shareArg: string, pathArg: string): Promise { await this.bridge.sendCommand('createDirectory', { connection: this.connection, share: shareArg, path: pathArg, }); } public async rename(shareArg: string, fromArg: string, toArg: string): Promise { await this.bridge.sendCommand('rename', { connection: this.connection, share: shareArg, from: fromArg, to: toArg, }); } public async stat(shareArg: string, pathArg: string): Promise { return this.bridge.sendCommand('stat', { connection: this.connection, share: shareArg, path: pathArg, }); } } export class SambaServer { private bridge = new SambaBridge(); private config: ISambaServerOptions; private startResult?: ISambaServerStartResult; constructor(optionsArg: ISambaServerOptions) { this.config = { host: '127.0.0.1', port: 445, ...optionsArg, }; } public async start(): Promise { this.startResult = await this.bridge.sendCommand('startServer', { config: this.config }); return this.startResult; } public async stop(): Promise { try { await this.bridge.sendCommand('stopServer', {} as Record); } finally { this.bridge.kill(); this.startResult = undefined; } } public async status(): Promise { return this.bridge.sendCommand('getServerStatus', {} as Record); } public getConnectionOptions(authArg?: Partial): ISambaClientOptions { if (!this.startResult) { throw new Error('SambaServer is not started'); } return { host: this.startResult.host, port: this.startResult.port, auth: authArg, }; } }