@kadi.build/file-manager 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,824 @@
1
+ import { promises as fs } from 'fs';
2
+ import fsSync from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { EventEmitter } from 'events';
6
+
7
+ class LocalProvider extends EventEmitter {
8
+ constructor(config) {
9
+ super();
10
+ this.config = config || {};
11
+ this.localRoot = this.config.localRoot || process.cwd();
12
+ this.maxFileSize = this.config.maxFileSize || 1073741824; // 1GB
13
+ this.chunkSize = this.config.chunkSize || 8388608; // 8MB
14
+ this.allowSymlinks = this.config.allowSymlinks || false;
15
+ this.restrictToBasePath = this.config.restrictToBasePath !== false; // Default true
16
+ this.maxPathLength = this.config.maxPathLength || 255;
17
+ }
18
+
19
+ // ============================================================================
20
+ // PATH VALIDATION AND UTILITIES
21
+ // ============================================================================
22
+
23
+ normalizePath(inputPath) {
24
+ if (!inputPath || inputPath === '/') {
25
+ return this.localRoot;
26
+ }
27
+
28
+ // Handle absolute paths
29
+ if (path.isAbsolute(inputPath)) {
30
+ const normalizedPath = path.normalize(inputPath);
31
+
32
+ // Security check: ensure absolute path is within base path if restriction is enabled
33
+ if (this.restrictToBasePath) {
34
+ const resolvedLocalRoot = path.resolve(this.localRoot);
35
+ if (!normalizedPath.startsWith(resolvedLocalRoot)) {
36
+ throw new Error(`Path '${inputPath}' is outside the allowed base path '${this.localRoot}'`);
37
+ }
38
+ }
39
+
40
+ return normalizedPath;
41
+ }
42
+
43
+ // Handle relative paths - resolve them relative to localRoot
44
+ const resolvedLocalRoot = path.resolve(this.localRoot);
45
+ const normalizedPath = path.resolve(resolvedLocalRoot, inputPath);
46
+
47
+ // Security check: ensure resolved path is within base path if restriction is enabled
48
+ if (this.restrictToBasePath) {
49
+ const relativePath = path.relative(resolvedLocalRoot, normalizedPath);
50
+
51
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
52
+ throw new Error(`Path '${inputPath}' is outside the allowed base path '${this.localRoot}'`);
53
+ }
54
+ }
55
+
56
+ return normalizedPath;
57
+ }
58
+
59
+ validatePath(inputPath) {
60
+ if (!inputPath) {
61
+ throw new Error('Path cannot be empty');
62
+ }
63
+
64
+ if (inputPath.length > this.maxPathLength) {
65
+ throw new Error(`Path length exceeds maximum of ${this.maxPathLength} characters`);
66
+ }
67
+
68
+ // Check for invalid characters (Windows + Unix)
69
+ if (/[<>:"|?*\x00-\x1f]/.test(inputPath)) {
70
+ throw new Error(`Path contains invalid characters: ${inputPath}`);
71
+ }
72
+
73
+ return true;
74
+ }
75
+
76
+ async ensureDirectory(dirPath) {
77
+ try {
78
+ await fs.mkdir(dirPath, { recursive: true });
79
+ } catch (error) {
80
+ if (error.code !== 'EEXIST') {
81
+ throw new Error(`Failed to create directory '${dirPath}': ${error.message}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ // ============================================================================
87
+ // CONNECTION AND VALIDATION
88
+ // ============================================================================
89
+
90
+ async testConnection() {
91
+ try {
92
+ const stats = await fs.stat(this.localRoot);
93
+ if (!stats.isDirectory()) {
94
+ throw new Error(`Local root '${this.localRoot}' is not a directory`);
95
+ }
96
+
97
+ const testFile = path.join(this.localRoot, '.local-provider-test');
98
+ await fs.writeFile(testFile, 'test');
99
+ await fs.unlink(testFile);
100
+
101
+ const totalSize = await this.getDirectorySize(this.localRoot);
102
+
103
+ return {
104
+ provider: 'local',
105
+ localRoot: this.localRoot,
106
+ accessible: true,
107
+ writable: true,
108
+ totalSize: totalSize,
109
+ freeSpace: await this.getFreeSpace()
110
+ };
111
+ } catch (error) {
112
+ throw new Error(`Local provider connection test failed: ${error.message}`);
113
+ }
114
+ }
115
+
116
+ validateConfig() {
117
+ const errors = [];
118
+ const warnings = [];
119
+
120
+ if (!this.localRoot) {
121
+ errors.push('Local root directory is required');
122
+ }
123
+
124
+ if (this.maxFileSize <= 0) {
125
+ errors.push('Max file size must be positive');
126
+ }
127
+
128
+ if (this.chunkSize <= 0) {
129
+ errors.push('Chunk size must be positive');
130
+ }
131
+
132
+ if (this.chunkSize > this.maxFileSize) {
133
+ warnings.push('Chunk size is larger than max file size');
134
+ }
135
+
136
+ if (this.allowSymlinks) {
137
+ warnings.push('Allowing symlinks may pose security risks');
138
+ }
139
+
140
+ return {
141
+ isValid: errors.length === 0,
142
+ errors,
143
+ warnings
144
+ };
145
+ }
146
+
147
+ // ============================================================================
148
+ // FILE OPERATIONS (CRUD)
149
+ // ============================================================================
150
+
151
+ async uploadFile(sourcePath, targetPath) {
152
+ this.validatePath(sourcePath);
153
+ this.validatePath(targetPath);
154
+
155
+ const resolvedSourcePath = this.normalizePath(sourcePath);
156
+ const resolvedTargetPath = this.normalizePath(targetPath);
157
+
158
+ this.emit('progress', { operation: 'upload', source: sourcePath, target: targetPath });
159
+
160
+ try {
161
+ const sourceStats = await fs.stat(resolvedSourcePath);
162
+ if (!sourceStats.isFile()) {
163
+ throw new Error(`Source '${sourcePath}' is not a file`);
164
+ }
165
+
166
+ if (sourceStats.size > this.maxFileSize) {
167
+ throw new Error(`File size ${this.formatBytes(sourceStats.size)} exceeds maximum of ${this.formatBytes(this.maxFileSize)}`);
168
+ }
169
+
170
+ const targetDir = path.dirname(resolvedTargetPath);
171
+ await this.ensureDirectory(targetDir);
172
+
173
+ await fs.copyFile(resolvedSourcePath, resolvedTargetPath);
174
+
175
+ const targetStats = await fs.stat(resolvedTargetPath);
176
+
177
+ this.emit('progress', { operation: 'upload', status: 'complete', target: targetPath });
178
+
179
+ return {
180
+ name: path.basename(targetPath),
181
+ path: targetPath,
182
+ size: targetStats.size,
183
+ modifiedTime: targetStats.mtime.toISOString(),
184
+ hash: await this.calculateChecksum(resolvedTargetPath)
185
+ };
186
+ } catch (error) {
187
+ if (this.isFileNotFoundError(error)) {
188
+ throw new Error(`Source file not found: ${sourcePath}`);
189
+ }
190
+ throw new Error(`Upload failed: ${error.message}`);
191
+ }
192
+ }
193
+
194
+ async downloadFile(sourcePath, targetPath) {
195
+ this.validatePath(sourcePath);
196
+ this.validatePath(targetPath);
197
+
198
+ const resolvedSourcePath = this.normalizePath(sourcePath);
199
+ const resolvedTargetPath = this.normalizePath(targetPath);
200
+
201
+ this.emit('progress', { operation: 'download', source: sourcePath, target: targetPath });
202
+
203
+ try {
204
+ const sourceStats = await fs.stat(resolvedSourcePath);
205
+ if (!sourceStats.isFile()) {
206
+ throw new Error(`Source '${sourcePath}' is not a file`);
207
+ }
208
+
209
+ const targetDir = path.dirname(resolvedTargetPath);
210
+ await this.ensureDirectory(targetDir);
211
+
212
+ await fs.copyFile(resolvedSourcePath, resolvedTargetPath);
213
+
214
+ this.emit('progress', { operation: 'download', status: 'complete', target: targetPath });
215
+
216
+ return {
217
+ path: targetPath,
218
+ size: sourceStats.size,
219
+ sourcePath: sourcePath
220
+ };
221
+ } catch (error) {
222
+ if (this.isFileNotFoundError(error)) {
223
+ throw new Error(`Source file not found: ${sourcePath}`);
224
+ }
225
+ throw new Error(`Download failed: ${error.message}`);
226
+ }
227
+ }
228
+
229
+ async getFile(filePath) {
230
+ this.validatePath(filePath);
231
+ const resolvedPath = this.normalizePath(filePath);
232
+
233
+ try {
234
+ const stats = await fs.stat(resolvedPath);
235
+ if (!stats.isFile()) {
236
+ throw new Error(`Path '${filePath}' is not a file`);
237
+ }
238
+
239
+ return {
240
+ name: path.basename(filePath),
241
+ path: filePath,
242
+ size: stats.size,
243
+ modifiedTime: stats.mtime.toISOString(),
244
+ createdTime: stats.birthtime.toISOString(),
245
+ isDirectory: false,
246
+ isFile: true,
247
+ hash: await this.calculateChecksum(resolvedPath),
248
+ permissions: stats.mode
249
+ };
250
+ } catch (error) {
251
+ if (this.isFileNotFoundError(error)) {
252
+ throw new Error(`File not found: ${filePath}`);
253
+ }
254
+ throw error;
255
+ }
256
+ }
257
+
258
+ async listFiles(directoryPath = '/', options = {}) {
259
+ this.validatePath(directoryPath);
260
+ const resolvedPath = this.normalizePath(directoryPath);
261
+
262
+ const {
263
+ recursive = false,
264
+ includeHidden = false,
265
+ fileTypesOnly = true
266
+ } = options;
267
+
268
+ try {
269
+ const stats = await fs.stat(resolvedPath);
270
+ if (!stats.isDirectory()) {
271
+ throw new Error(`Path '${directoryPath}' is not a directory`);
272
+ }
273
+
274
+ const files = [];
275
+ await this.collectFiles(resolvedPath, directoryPath, files, recursive, includeHidden, fileTypesOnly);
276
+
277
+ return files;
278
+ } catch (error) {
279
+ if (this.isFileNotFoundError(error)) {
280
+ throw new Error(`Directory not found: ${directoryPath}`);
281
+ }
282
+ throw error;
283
+ }
284
+ }
285
+
286
+ async collectFiles(resolvedPath, relativePath, files, recursive, includeHidden, fileTypesOnly) {
287
+ const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
288
+
289
+ for (const entry of entries) {
290
+ if (!includeHidden && entry.name.startsWith('.')) {
291
+ continue;
292
+ }
293
+
294
+ const fullPath = path.join(resolvedPath, entry.name);
295
+ const relativeFilePath = path.join(relativePath, entry.name);
296
+
297
+ try {
298
+ const stats = await fs.stat(fullPath);
299
+
300
+ if (entry.isFile() || (!fileTypesOnly && !entry.isDirectory())) {
301
+ files.push({
302
+ name: entry.name,
303
+ path: relativeFilePath.replace(/\\/g, '/'),
304
+ size: stats.size,
305
+ modifiedTime: stats.mtime.toISOString(),
306
+ createdTime: stats.birthtime.toISOString(),
307
+ isDirectory: false,
308
+ isFile: true,
309
+ permissions: stats.mode
310
+ });
311
+ }
312
+
313
+ if (recursive && entry.isDirectory()) {
314
+ await this.collectFiles(fullPath, relativeFilePath, files, recursive, includeHidden, fileTypesOnly);
315
+ }
316
+ } catch (error) {
317
+ // Skip files we can't read
318
+ this.emit('warning', { message: `Could not read ${relativeFilePath}: ${error.message}` });
319
+ }
320
+ }
321
+ }
322
+
323
+ async deleteFile(filePath) {
324
+ this.validatePath(filePath);
325
+ const resolvedPath = this.normalizePath(filePath);
326
+
327
+ try {
328
+ const stats = await fs.stat(resolvedPath);
329
+ if (!stats.isFile()) {
330
+ throw new Error(`Path '${filePath}' is not a file`);
331
+ }
332
+
333
+ await fs.unlink(resolvedPath);
334
+
335
+ return {
336
+ deleted: true,
337
+ path: filePath
338
+ };
339
+ } catch (error) {
340
+ if (this.isFileNotFoundError(error)) {
341
+ return {
342
+ deleted: true,
343
+ path: filePath
344
+ };
345
+ }
346
+ throw new Error(`Delete failed: ${error.message}`);
347
+ }
348
+ }
349
+
350
+ async renameFile(oldPath, newName) {
351
+ this.validatePath(oldPath);
352
+ this.validatePath(newName);
353
+
354
+ const resolvedOldPath = this.normalizePath(oldPath);
355
+ const directory = path.dirname(resolvedOldPath);
356
+ const resolvedNewPath = path.join(directory, newName);
357
+
358
+ try {
359
+ const stats = await fs.stat(resolvedOldPath);
360
+ if (!stats.isFile()) {
361
+ throw new Error(`Path '${oldPath}' is not a file`);
362
+ }
363
+
364
+ await fs.rename(resolvedOldPath, resolvedNewPath);
365
+
366
+ const newRelativePath = path.join(path.dirname(oldPath), newName);
367
+
368
+ return {
369
+ name: newName,
370
+ oldPath: oldPath,
371
+ newPath: newRelativePath
372
+ };
373
+ } catch (error) {
374
+ if (this.isFileNotFoundError(error)) {
375
+ throw new Error(`File not found: ${oldPath}`);
376
+ }
377
+ throw new Error(`Rename failed: ${error.message}`);
378
+ }
379
+ }
380
+
381
+ async copyFile(sourcePath, targetPath) {
382
+ this.validatePath(sourcePath);
383
+ this.validatePath(targetPath);
384
+
385
+ const resolvedSourcePath = this.normalizePath(sourcePath);
386
+ const resolvedTargetPath = this.normalizePath(targetPath);
387
+
388
+ try {
389
+ const stats = await fs.stat(resolvedSourcePath);
390
+ if (!stats.isFile()) {
391
+ throw new Error(`Source '${sourcePath}' is not a file`);
392
+ }
393
+
394
+ const targetDir = path.dirname(resolvedTargetPath);
395
+ await this.ensureDirectory(targetDir);
396
+
397
+ await fs.copyFile(resolvedSourcePath, resolvedTargetPath);
398
+
399
+ return {
400
+ name: path.basename(targetPath),
401
+ sourcePath: sourcePath,
402
+ targetPath: targetPath,
403
+ size: stats.size
404
+ };
405
+ } catch (error) {
406
+ if (this.isFileNotFoundError(error)) {
407
+ throw new Error(`Source file not found: ${sourcePath}`);
408
+ }
409
+ throw new Error(`Copy failed: ${error.message}`);
410
+ }
411
+ }
412
+
413
+ async moveFile(sourcePath, targetPath) {
414
+ this.validatePath(sourcePath);
415
+ this.validatePath(targetPath);
416
+
417
+ const resolvedSourcePath = this.normalizePath(sourcePath);
418
+ const resolvedTargetPath = this.normalizePath(targetPath);
419
+
420
+ try {
421
+ const stats = await fs.stat(resolvedSourcePath);
422
+ if (!stats.isFile()) {
423
+ throw new Error(`Source '${sourcePath}' is not a file`);
424
+ }
425
+
426
+ const targetDir = path.dirname(resolvedTargetPath);
427
+ await this.ensureDirectory(targetDir);
428
+
429
+ await fs.rename(resolvedSourcePath, resolvedTargetPath);
430
+
431
+ return {
432
+ name: path.basename(targetPath),
433
+ oldPath: sourcePath,
434
+ newPath: targetPath,
435
+ size: stats.size
436
+ };
437
+ } catch (error) {
438
+ if (this.isFileNotFoundError(error)) {
439
+ throw new Error(`Source file not found: ${sourcePath}`);
440
+ }
441
+ throw new Error(`Move failed: ${error.message}`);
442
+ }
443
+ }
444
+
445
+ // ============================================================================
446
+ // FOLDER OPERATIONS (CRUD)
447
+ // ============================================================================
448
+
449
+ async createFolder(folderPath) {
450
+ this.validatePath(folderPath);
451
+ const resolvedPath = this.normalizePath(folderPath);
452
+
453
+ try {
454
+ await fs.mkdir(resolvedPath, { recursive: true });
455
+
456
+ return {
457
+ name: path.basename(folderPath),
458
+ path: folderPath,
459
+ created: true
460
+ };
461
+ } catch (error) {
462
+ if (error.code === 'EEXIST') {
463
+ return {
464
+ name: path.basename(folderPath),
465
+ path: folderPath,
466
+ created: false,
467
+ message: 'Folder already exists'
468
+ };
469
+ }
470
+ throw new Error(`Create folder failed: ${error.message}`);
471
+ }
472
+ }
473
+
474
+ async getFolder(folderPath) {
475
+ this.validatePath(folderPath);
476
+ const resolvedPath = this.normalizePath(folderPath);
477
+
478
+ try {
479
+ const stats = await fs.stat(resolvedPath);
480
+ if (!stats.isDirectory()) {
481
+ throw new Error(`Path '${folderPath}' is not a directory`);
482
+ }
483
+
484
+ const entries = await fs.readdir(resolvedPath);
485
+ const itemCount = entries.length;
486
+
487
+ return {
488
+ name: path.basename(folderPath) || 'Root',
489
+ path: folderPath,
490
+ itemCount: itemCount,
491
+ modifiedTime: stats.mtime.toISOString(),
492
+ createdTime: stats.birthtime.toISOString(),
493
+ isDirectory: true,
494
+ isFile: false,
495
+ permissions: stats.mode
496
+ };
497
+ } catch (error) {
498
+ if (this.isFileNotFoundError(error)) {
499
+ throw new Error(`Folder not found: ${folderPath}`);
500
+ }
501
+ throw error;
502
+ }
503
+ }
504
+
505
+ async listFolders(directoryPath = '/') {
506
+ this.validatePath(directoryPath);
507
+ const resolvedPath = this.normalizePath(directoryPath);
508
+
509
+ try {
510
+ const stats = await fs.stat(resolvedPath);
511
+ if (!stats.isDirectory()) {
512
+ throw new Error(`Path '${directoryPath}' is not a directory`);
513
+ }
514
+
515
+ const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
516
+ const folders = [];
517
+
518
+ for (const entry of entries) {
519
+ if (entry.isDirectory()) {
520
+ try {
521
+ const fullPath = path.join(resolvedPath, entry.name);
522
+ const folderStats = await fs.stat(fullPath);
523
+ const subEntries = await fs.readdir(fullPath);
524
+
525
+ folders.push({
526
+ name: entry.name,
527
+ path: path.join(directoryPath, entry.name).replace(/\\/g, '/'),
528
+ itemCount: subEntries.length,
529
+ modifiedTime: folderStats.mtime.toISOString(),
530
+ createdTime: folderStats.birthtime.toISOString(),
531
+ isDirectory: true,
532
+ isFile: false,
533
+ permissions: folderStats.mode
534
+ });
535
+ } catch (error) {
536
+ this.emit('warning', { message: `Could not read folder ${entry.name}: ${error.message}` });
537
+ }
538
+ }
539
+ }
540
+
541
+ return folders;
542
+ } catch (error) {
543
+ if (this.isFileNotFoundError(error)) {
544
+ throw new Error(`Directory not found: ${directoryPath}`);
545
+ }
546
+ throw error;
547
+ }
548
+ }
549
+
550
+ async deleteFolder(folderPath, recursive = false) {
551
+ this.validatePath(folderPath);
552
+ const resolvedPath = this.normalizePath(folderPath);
553
+
554
+ try {
555
+ const stats = await fs.stat(resolvedPath);
556
+ if (!stats.isDirectory()) {
557
+ throw new Error(`Path '${folderPath}' is not a directory`);
558
+ }
559
+
560
+ if (recursive) {
561
+ await fs.rm(resolvedPath, { recursive: true, force: true });
562
+ } else {
563
+ await fs.rmdir(resolvedPath);
564
+ }
565
+
566
+ return {
567
+ deleted: true,
568
+ path: folderPath,
569
+ recursive: recursive
570
+ };
571
+ } catch (error) {
572
+ if (this.isFileNotFoundError(error)) {
573
+ return {
574
+ deleted: true,
575
+ path: folderPath,
576
+ recursive: recursive
577
+ };
578
+ }
579
+ throw new Error(`Delete folder failed: ${error.message}`);
580
+ }
581
+ }
582
+
583
+ async renameFolder(oldPath, newName) {
584
+ this.validatePath(oldPath);
585
+ this.validatePath(newName);
586
+
587
+ const resolvedOldPath = this.normalizePath(oldPath);
588
+ const directory = path.dirname(resolvedOldPath);
589
+ const resolvedNewPath = path.join(directory, newName);
590
+
591
+ try {
592
+ const stats = await fs.stat(resolvedOldPath);
593
+ if (!stats.isDirectory()) {
594
+ throw new Error(`Path '${oldPath}' is not a directory`);
595
+ }
596
+
597
+ await fs.rename(resolvedOldPath, resolvedNewPath);
598
+
599
+ const newRelativePath = path.join(path.dirname(oldPath), newName);
600
+
601
+ return {
602
+ name: newName,
603
+ oldPath: oldPath,
604
+ newPath: newRelativePath
605
+ };
606
+ } catch (error) {
607
+ if (this.isFileNotFoundError(error)) {
608
+ throw new Error(`Folder not found: ${oldPath}`);
609
+ }
610
+ throw new Error(`Rename folder failed: ${error.message}`);
611
+ }
612
+ }
613
+
614
+ async copyFolder(sourcePath, targetPath) {
615
+ this.validatePath(sourcePath);
616
+ this.validatePath(targetPath);
617
+
618
+ const resolvedSourcePath = this.normalizePath(sourcePath);
619
+ const resolvedTargetPath = this.normalizePath(targetPath);
620
+
621
+ try {
622
+ const stats = await fs.stat(resolvedSourcePath);
623
+ if (!stats.isDirectory()) {
624
+ throw new Error(`Source '${sourcePath}' is not a directory`);
625
+ }
626
+
627
+ await fs.mkdir(resolvedTargetPath, { recursive: true });
628
+
629
+ await this.copyFolderRecursive(resolvedSourcePath, resolvedTargetPath);
630
+
631
+ return {
632
+ name: path.basename(targetPath),
633
+ sourcePath: sourcePath,
634
+ targetPath: targetPath
635
+ };
636
+ } catch (error) {
637
+ if (this.isFileNotFoundError(error)) {
638
+ throw new Error(`Source folder not found: ${sourcePath}`);
639
+ }
640
+ throw new Error(`Copy folder failed: ${error.message}`);
641
+ }
642
+ }
643
+
644
+ async copyFolderRecursive(sourceDir, targetDir) {
645
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
646
+
647
+ for (const entry of entries) {
648
+ const sourcePath = path.join(sourceDir, entry.name);
649
+ const targetPath = path.join(targetDir, entry.name);
650
+
651
+ if (entry.isDirectory()) {
652
+ await fs.mkdir(targetPath, { recursive: true });
653
+ await this.copyFolderRecursive(sourcePath, targetPath);
654
+ } else {
655
+ await fs.copyFile(sourcePath, targetPath);
656
+ }
657
+ }
658
+ }
659
+
660
+ async moveFolder(sourcePath, targetPath) {
661
+ this.validatePath(sourcePath);
662
+ this.validatePath(targetPath);
663
+
664
+ const resolvedSourcePath = this.normalizePath(sourcePath);
665
+ const resolvedTargetPath = this.normalizePath(targetPath);
666
+
667
+ try {
668
+ const stats = await fs.stat(resolvedSourcePath);
669
+ if (!stats.isDirectory()) {
670
+ throw new Error(`Source '${sourcePath}' is not a directory`);
671
+ }
672
+
673
+ const targetParent = path.dirname(resolvedTargetPath);
674
+ await this.ensureDirectory(targetParent);
675
+
676
+ await fs.rename(resolvedSourcePath, resolvedTargetPath);
677
+
678
+ return {
679
+ name: path.basename(targetPath),
680
+ oldPath: sourcePath,
681
+ newPath: targetPath
682
+ };
683
+ } catch (error) {
684
+ if (this.isFileNotFoundError(error)) {
685
+ throw new Error(`Source folder not found: ${sourcePath}`);
686
+ }
687
+ throw new Error(`Move folder failed: ${error.message}`);
688
+ }
689
+ }
690
+
691
+ // ============================================================================
692
+ // SEARCH OPERATIONS
693
+ // ============================================================================
694
+
695
+ async searchFiles(query, options = {}) {
696
+ const {
697
+ directory = '/',
698
+ recursive = true,
699
+ caseSensitive = false,
700
+ fileTypesOnly = true,
701
+ limit = 100
702
+ } = options;
703
+
704
+ this.validatePath(directory);
705
+ const resolvedDir = this.normalizePath(directory);
706
+
707
+ try {
708
+ const results = [];
709
+ await this.searchInDirectory(resolvedDir, directory, query, results, recursive, caseSensitive, fileTypesOnly);
710
+
711
+ return results.slice(0, limit);
712
+ } catch (error) {
713
+ if (this.isFileNotFoundError(error)) {
714
+ throw new Error(`Search directory not found: ${directory}`);
715
+ }
716
+ throw new Error(`Search failed: ${error.message}`);
717
+ }
718
+ }
719
+
720
+ async searchInDirectory(resolvedDir, relativeDir, query, results, recursive, caseSensitive, fileTypesOnly) {
721
+ const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
722
+ const searchQuery = caseSensitive ? query : query.toLowerCase();
723
+
724
+ for (const entry of entries) {
725
+ const fullPath = path.join(resolvedDir, entry.name);
726
+ const relativePath = path.join(relativeDir, entry.name);
727
+ const fileName = caseSensitive ? entry.name : entry.name.toLowerCase();
728
+
729
+ try {
730
+ if (fileName.includes(searchQuery)) {
731
+ const stats = await fs.stat(fullPath);
732
+
733
+ if (entry.isFile() || (!fileTypesOnly && !entry.isDirectory())) {
734
+ results.push({
735
+ name: entry.name,
736
+ path: relativePath.replace(/\\/g, '/'),
737
+ size: stats.size,
738
+ modifiedTime: stats.mtime.toISOString(),
739
+ isDirectory: entry.isDirectory(),
740
+ isFile: entry.isFile()
741
+ });
742
+ }
743
+ }
744
+
745
+ if (recursive && entry.isDirectory()) {
746
+ await this.searchInDirectory(fullPath, relativePath, query, results, recursive, caseSensitive, fileTypesOnly);
747
+ }
748
+ } catch (error) {
749
+ this.emit('warning', { message: `Could not search in ${relativePath}: ${error.message}` });
750
+ }
751
+ }
752
+ }
753
+
754
+ // ============================================================================
755
+ // UTILITY METHODS
756
+ // ============================================================================
757
+
758
+ async calculateChecksum(filePath, algorithm = 'sha256') {
759
+ return new Promise((resolve, reject) => {
760
+ const hash = crypto.createHash(algorithm);
761
+ const stream = fsSync.createReadStream(filePath);
762
+
763
+ stream.on('error', reject);
764
+ stream.on('data', chunk => hash.update(chunk));
765
+ stream.on('end', () => resolve(hash.digest('hex')));
766
+ });
767
+ }
768
+
769
+ async getDirectorySize(dirPath) {
770
+ let totalSize = 0;
771
+
772
+ try {
773
+ const files = await this.listFiles(path.relative(this.localRoot, dirPath) || '/', { recursive: true });
774
+ totalSize = files.reduce((sum, file) => sum + (file.size || 0), 0);
775
+ } catch (error) {
776
+ totalSize = 0;
777
+ }
778
+
779
+ return totalSize;
780
+ }
781
+
782
+ async getFreeSpace() {
783
+ try {
784
+ if (fs.statfs) {
785
+ const stats = await fs.statfs(this.localRoot);
786
+ return stats.bavail * stats.bsize;
787
+ }
788
+ } catch (error) {
789
+ // Fallback
790
+ }
791
+ return Number.MAX_SAFE_INTEGER;
792
+ }
793
+
794
+ formatBytes(bytes) {
795
+ if (bytes === 0) return '0 Bytes';
796
+ const k = 1024;
797
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
798
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
799
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
800
+ }
801
+
802
+ // ============================================================================
803
+ // ERROR HANDLING HELPERS
804
+ // ============================================================================
805
+
806
+ isFileNotFoundError(error) {
807
+ return error.code === 'ENOENT' ||
808
+ error.message.includes('no such file') ||
809
+ error.message.includes('not found');
810
+ }
811
+
812
+ isPermissionError(error) {
813
+ return error.code === 'EACCES' ||
814
+ error.code === 'EPERM' ||
815
+ error.message.includes('permission denied');
816
+ }
817
+
818
+ isDirectoryNotEmptyError(error) {
819
+ return error.code === 'ENOTEMPTY' ||
820
+ error.message.includes('directory not empty');
821
+ }
822
+ }
823
+
824
+ export { LocalProvider };