import libentry, { NativeDirectoryEntry as LibNativeDirectoryEntry, NativeModule as LibNativeModule } from 'libentry.so'; export interface SmbConnectionOptions { host: string; share: string; username: string; password: string; domain?: string; } export interface SmbListOptions extends SmbConnectionOptions { path?: string; } export interface SmbDeleteOptions extends SmbConnectionOptions { path: string; isDirectory?: boolean; } export interface SmbCreateOptions extends SmbConnectionOptions { path: string; } export interface SmbRenameOptions extends SmbConnectionOptions { path: string; newPath: string; } export interface SmbDirectoryEntry { name: string; isDirectory: boolean; isFile: boolean; size: number; } export type NativeDirectoryEntry = LibNativeDirectoryEntry; const bridge: LibNativeModule = libentry as LibNativeModule; export class NativeSambaTree { private closed = false; private readonly treeId: number; constructor(treeId: number) { this.treeId = treeId; } private normalizePath(input?: string): string { if (!input) { return ''; } const cleaned = input.replace(/^[\\/]+/, '').replace(/\\/g, '/'); return cleaned; } private ensureOpen(): void { if (this.closed) { throw new Error('SMB tree has been closed'); } } async readDirectory(path?: string): Promise { this.ensureOpen(); const normalized = this.normalizePath(path); return bridge.readDirectory(this.treeId, normalized) ?? []; } deleteEntry(path?: string, isDirectory: boolean = false): void { this.ensureOpen(); const normalized = this.normalizePath(path); bridge.deleteEntry(this.treeId, normalized, isDirectory); } createDirectory(path?: string): void { this.ensureOpen(); const normalized = this.normalizePath(path); bridge.createDirectory(this.treeId, normalized); } renameEntry(path?: string, newPath?: string): void { this.ensureOpen(); const normalized = this.normalizePath(path); const normalizedNew = this.normalizePath(newPath); bridge.renameEntry(this.treeId, normalized, normalizedNew); } async close(): Promise { if (this.closed) { return; } bridge.disconnectTree(this.treeId); this.closed = true; } } export class NativeSambaSession { private closed = false; private readonly sessionId: number; constructor(sessionId: number) { this.sessionId = sessionId; } private ensureOpen(): void { if (this.closed) { throw new Error('SMB session has been closed'); } } async connectTree(share: string): Promise { this.ensureOpen(); const treeId = Number(bridge.connectTree(this.sessionId, share)); return new NativeSambaTree(treeId); } async disconnect(): Promise { if (this.closed) { return; } bridge.disconnectSession(this.sessionId); this.closed = true; } } export class NativeSambaClient { private readonly clientId: number; private closed = false; constructor(host: string) { this.clientId = bridge.createClient(host); } private ensureOpen(): void { if (this.closed) { throw new Error('SMB client has been closed'); } } async authenticate(options: SmbConnectionOptions): Promise { this.ensureOpen(); const sessionId = Number( bridge.authenticate(this.clientId, options.username, options.password, options.domain) ); return new NativeSambaSession(sessionId); } async close(): Promise { if (this.closed) { return; } bridge.destroyClient(this.clientId); this.closed = true; } } async function withSmbTree(options: SmbConnectionOptions, handler: (tree: NativeSambaTree) => Promise): Promise { const client = new NativeSambaClient(options.host); let session: NativeSambaSession | undefined; let tree: NativeSambaTree | undefined; try { session = await client.authenticate(options); tree = await session.connectTree(options.share); return await handler(tree); } finally { await tree?.close(); await session?.disconnect(); await client.close(); } } export async function listSmbDirectory(options: SmbListOptions): Promise { const entries = await withSmbTree(options, (tree: NativeSambaTree) => tree.readDirectory(options.path)); return entries.map((entry: NativeDirectoryEntry): SmbDirectoryEntry => ({ name: entry.name ?? entry.fileName ?? 'unknown', isDirectory: Boolean(entry.isDirectory), isFile: Boolean(entry.isFile), size: entry.size ?? 0 })); } export async function deleteSmbEntry(options: SmbDeleteOptions): Promise { if (!options.path || options.path.length === 0) { throw new Error('Remote path must not be empty'); } await withSmbTree(options, async (tree: NativeSambaTree) => { tree.deleteEntry(options.path, options.isDirectory === true); }); } export async function createSmbDirectory(options: SmbCreateOptions): Promise { if (!options.path || options.path.length === 0) { throw new Error('Remote path must not be empty'); } await withSmbTree(options, async (tree: NativeSambaTree) => { tree.createDirectory(options.path); }); } export async function renameSmbEntry(options: SmbRenameOptions): Promise { if (!options.path || options.path.length === 0) { throw new Error('Remote path must not be empty'); } if (!options.newPath || options.newPath.length === 0) { throw new Error('New remote path must not be empty'); } await withSmbTree(options, async (tree: NativeSambaTree) => { tree.renameEntry(options.path, options.newPath); }); }