SmbBridge.ets 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import libentry, { NativeDirectoryEntry as LibNativeDirectoryEntry, NativeModule as LibNativeModule } from 'libentry.so';
  2. export interface SmbConnectionOptions {
  3. host: string;
  4. share: string;
  5. username: string;
  6. password: string;
  7. domain?: string;
  8. }
  9. export interface SmbListOptions extends SmbConnectionOptions {
  10. path?: string;
  11. }
  12. export interface SmbDeleteOptions extends SmbConnectionOptions {
  13. path: string;
  14. isDirectory?: boolean;
  15. }
  16. export interface SmbCreateOptions extends SmbConnectionOptions {
  17. path: string;
  18. }
  19. export interface SmbRenameOptions extends SmbConnectionOptions {
  20. path: string;
  21. newPath: string;
  22. }
  23. export interface SmbDirectoryEntry {
  24. name: string;
  25. isDirectory: boolean;
  26. isFile: boolean;
  27. size: number;
  28. }
  29. export type NativeDirectoryEntry = LibNativeDirectoryEntry;
  30. const bridge: LibNativeModule = libentry as LibNativeModule;
  31. export class NativeSambaTree {
  32. private closed = false;
  33. private readonly treeId: number;
  34. constructor(treeId: number) {
  35. this.treeId = treeId;
  36. }
  37. private normalizePath(input?: string): string {
  38. if (!input) {
  39. return '';
  40. }
  41. const cleaned = input.replace(/^[\\/]+/, '').replace(/\\/g, '/');
  42. return cleaned;
  43. }
  44. private ensureOpen(): void {
  45. if (this.closed) {
  46. throw new Error('SMB tree has been closed');
  47. }
  48. }
  49. async readDirectory(path?: string): Promise<NativeDirectoryEntry[]> {
  50. this.ensureOpen();
  51. const normalized = this.normalizePath(path);
  52. return bridge.readDirectory(this.treeId, normalized) ?? [];
  53. }
  54. deleteEntry(path?: string, isDirectory: boolean = false): void {
  55. this.ensureOpen();
  56. const normalized = this.normalizePath(path);
  57. bridge.deleteEntry(this.treeId, normalized, isDirectory);
  58. }
  59. createDirectory(path?: string): void {
  60. this.ensureOpen();
  61. const normalized = this.normalizePath(path);
  62. bridge.createDirectory(this.treeId, normalized);
  63. }
  64. renameEntry(path?: string, newPath?: string): void {
  65. this.ensureOpen();
  66. const normalized = this.normalizePath(path);
  67. const normalizedNew = this.normalizePath(newPath);
  68. bridge.renameEntry(this.treeId, normalized, normalizedNew);
  69. }
  70. async close(): Promise<void> {
  71. if (this.closed) {
  72. return;
  73. }
  74. bridge.disconnectTree(this.treeId);
  75. this.closed = true;
  76. }
  77. }
  78. export class NativeSambaSession {
  79. private closed = false;
  80. private readonly sessionId: number;
  81. constructor(sessionId: number) {
  82. this.sessionId = sessionId;
  83. }
  84. private ensureOpen(): void {
  85. if (this.closed) {
  86. throw new Error('SMB session has been closed');
  87. }
  88. }
  89. async connectTree(share: string): Promise<NativeSambaTree> {
  90. this.ensureOpen();
  91. const treeId = Number(bridge.connectTree(this.sessionId, share));
  92. return new NativeSambaTree(treeId);
  93. }
  94. async disconnect(): Promise<void> {
  95. if (this.closed) {
  96. return;
  97. }
  98. bridge.disconnectSession(this.sessionId);
  99. this.closed = true;
  100. }
  101. }
  102. export class NativeSambaClient {
  103. private readonly clientId: number;
  104. private closed = false;
  105. constructor(host: string) {
  106. this.clientId = bridge.createClient(host);
  107. }
  108. private ensureOpen(): void {
  109. if (this.closed) {
  110. throw new Error('SMB client has been closed');
  111. }
  112. }
  113. async authenticate(options: SmbConnectionOptions): Promise<NativeSambaSession> {
  114. this.ensureOpen();
  115. const sessionId = Number(
  116. bridge.authenticate(this.clientId, options.username, options.password, options.domain)
  117. );
  118. return new NativeSambaSession(sessionId);
  119. }
  120. async close(): Promise<void> {
  121. if (this.closed) {
  122. return;
  123. }
  124. bridge.destroyClient(this.clientId);
  125. this.closed = true;
  126. }
  127. }
  128. async function withSmbTree<T>(options: SmbConnectionOptions, handler: (tree: NativeSambaTree) => Promise<T>): Promise<T> {
  129. const client = new NativeSambaClient(options.host);
  130. let session: NativeSambaSession | undefined;
  131. let tree: NativeSambaTree | undefined;
  132. try {
  133. session = await client.authenticate(options);
  134. tree = await session.connectTree(options.share);
  135. return await handler(tree);
  136. } finally {
  137. await tree?.close();
  138. await session?.disconnect();
  139. await client.close();
  140. }
  141. }
  142. export async function listSmbDirectory(options: SmbListOptions): Promise<SmbDirectoryEntry[]> {
  143. const entries = await withSmbTree(options, (tree: NativeSambaTree) => tree.readDirectory(options.path));
  144. return entries.map((entry: NativeDirectoryEntry): SmbDirectoryEntry => ({
  145. name: entry.name ?? entry.fileName ?? 'unknown',
  146. isDirectory: Boolean(entry.isDirectory),
  147. isFile: Boolean(entry.isFile),
  148. size: entry.size ?? 0
  149. }));
  150. }
  151. export async function deleteSmbEntry(options: SmbDeleteOptions): Promise<void> {
  152. if (!options.path || options.path.length === 0) {
  153. throw new Error('Remote path must not be empty');
  154. }
  155. await withSmbTree(options, async (tree: NativeSambaTree) => {
  156. tree.deleteEntry(options.path, options.isDirectory === true);
  157. });
  158. }
  159. export async function createSmbDirectory(options: SmbCreateOptions): Promise<void> {
  160. if (!options.path || options.path.length === 0) {
  161. throw new Error('Remote path must not be empty');
  162. }
  163. await withSmbTree(options, async (tree: NativeSambaTree) => {
  164. tree.createDirectory(options.path);
  165. });
  166. }
  167. export async function renameSmbEntry(options: SmbRenameOptions): Promise<void> {
  168. if (!options.path || options.path.length === 0) {
  169. throw new Error('Remote path must not be empty');
  170. }
  171. if (!options.newPath || options.newPath.length === 0) {
  172. throw new Error('New remote path must not be empty');
  173. }
  174. await withSmbTree(options, async (tree: NativeSambaTree) => {
  175. tree.renameEntry(options.path, options.newPath);
  176. });
  177. }