@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,968 @@
1
+ import { promises as fs } from 'fs';
2
+ import fsSync from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import archiver from 'archiver';
6
+ import unzipper from 'unzipper';
7
+ import tar from 'tar';
8
+ import { EventEmitter } from 'events';
9
+
10
+ class CompressionProvider extends EventEmitter {
11
+ constructor(config) {
12
+ super();
13
+ this.config = config || {};
14
+ this.localRoot = this.config.localRoot || process.cwd();
15
+ this.enabled = this.config.enabled !== false;
16
+ this.compressionLevel = this.config.level || 6;
17
+ this.defaultFormat = this.config.format || 'zip';
18
+ this.maxFileSize = this.config.maxFileSize || 1073741824; // 1GB
19
+ this.chunkSize = this.config.chunkSize || 8388608; // 8MB
20
+ this.enableProgressTracking = this.config.enableProgressTracking !== false;
21
+ this.enableChecksumVerification = this.config.enableChecksumVerification !== false;
22
+
23
+ this.activeOperations = new Map();
24
+ this.operationCount = 0;
25
+ }
26
+
27
+ // ============================================================================
28
+ // CONNECTION AND VALIDATION
29
+ // ============================================================================
30
+
31
+ async testConnection() {
32
+ try {
33
+ const stats = await fs.stat(this.localRoot);
34
+ if (!stats.isDirectory()) {
35
+ throw new Error(`Local root '${this.localRoot}' is not a directory`);
36
+ }
37
+
38
+ const testFile = path.join(this.localRoot, '.compression-provider-test');
39
+ await fs.writeFile(testFile, 'test');
40
+ await fs.unlink(testFile);
41
+
42
+ await this.testCompressionLibraries();
43
+
44
+ return {
45
+ provider: 'compression',
46
+ localRoot: this.localRoot,
47
+ enabled: this.enabled,
48
+ supportedFormats: ['zip', 'tar.gz'],
49
+ defaultFormat: this.defaultFormat,
50
+ compressionLevel: this.compressionLevel,
51
+ activeOperations: this.activeOperations.size,
52
+ enableProgressTracking: this.enableProgressTracking,
53
+ enableChecksumVerification: this.enableChecksumVerification
54
+ };
55
+ } catch (error) {
56
+ throw new Error(`Compression provider connection test failed: ${error.message}`);
57
+ }
58
+ }
59
+
60
+ async testCompressionLibraries() {
61
+ try {
62
+ const testArchiver = archiver('zip', { zlib: { level: 1 } });
63
+ if (!testArchiver) {
64
+ throw new Error('Archiver library not available');
65
+ }
66
+
67
+ if (!unzipper.Extract) {
68
+ throw new Error('Unzipper library not available');
69
+ }
70
+
71
+ if (!tar.create) {
72
+ throw new Error('Tar library not available');
73
+ }
74
+
75
+ return true;
76
+ } catch (error) {
77
+ throw new Error(`Compression library test failed: ${error.message}`);
78
+ }
79
+ }
80
+
81
+ validateConfig() {
82
+ const errors = [];
83
+ const warnings = [];
84
+
85
+ if (!this.localRoot) {
86
+ errors.push('Local root directory is required for compression operations');
87
+ }
88
+
89
+ if (this.compressionLevel < 0 || this.compressionLevel > 9) {
90
+ errors.push('Compression level must be between 0 and 9');
91
+ }
92
+
93
+ if (!['zip', 'tar.gz'].includes(this.defaultFormat)) {
94
+ errors.push('Default format must be either "zip" or "tar.gz"');
95
+ }
96
+
97
+ if (this.maxFileSize <= 0) {
98
+ errors.push('Max file size must be positive');
99
+ }
100
+
101
+ if (this.chunkSize <= 0) {
102
+ errors.push('Chunk size must be positive');
103
+ }
104
+
105
+ if (this.chunkSize > this.maxFileSize) {
106
+ warnings.push('Chunk size is larger than max file size');
107
+ }
108
+
109
+ if (!this.enabled) {
110
+ warnings.push('Compression operations are disabled');
111
+ }
112
+
113
+ if (this.compressionLevel > 6) {
114
+ warnings.push('High compression level (>6) may significantly impact performance');
115
+ }
116
+
117
+ return {
118
+ isValid: errors.length === 0,
119
+ errors,
120
+ warnings
121
+ };
122
+ }
123
+
124
+ // ============================================================================
125
+ // PATH MANAGEMENT METHODS
126
+ // ============================================================================
127
+
128
+ normalizePath(inputPath) {
129
+ if (!inputPath || inputPath === '/') {
130
+ return this.localRoot;
131
+ }
132
+
133
+ if (path.isAbsolute(inputPath)) {
134
+ return path.normalize(inputPath);
135
+ }
136
+
137
+ const resolvedLocalRoot = path.resolve(this.localRoot);
138
+ return path.resolve(resolvedLocalRoot, inputPath);
139
+ }
140
+
141
+ validatePath(inputPath) {
142
+ if (!inputPath) {
143
+ throw new Error('Path cannot be empty');
144
+ }
145
+
146
+ if (/[<>:"|?*\x00-\x1f]/.test(inputPath)) {
147
+ throw new Error(`Path contains invalid characters: ${inputPath}`);
148
+ }
149
+
150
+ return true;
151
+ }
152
+
153
+ generateOperationId() {
154
+ this.operationCount++;
155
+ return `compress_${Date.now()}_${this.operationCount}`;
156
+ }
157
+
158
+ // ============================================================================
159
+ // COMPRESSION OPERATIONS
160
+ // ============================================================================
161
+
162
+ async compressFile(inputPath, outputPath, options = {}) {
163
+ if (!this.enabled) {
164
+ throw new Error('Compression operations are disabled in configuration');
165
+ }
166
+
167
+ this.validatePath(inputPath);
168
+ this.validatePath(outputPath);
169
+
170
+ const resolvedInputPath = this.normalizePath(inputPath);
171
+ const resolvedOutputPath = this.normalizePath(outputPath);
172
+
173
+ const {
174
+ format = this.defaultFormat,
175
+ level = this.compressionLevel,
176
+ includeRoot = false,
177
+ password = null
178
+ } = options;
179
+
180
+ const validatedLevel = Math.max(1, Math.min(9, parseInt(level) || this.compressionLevel));
181
+
182
+ const operationId = this.generateOperationId();
183
+
184
+ try {
185
+ const inputStats = await fs.stat(resolvedInputPath);
186
+
187
+ if (inputStats.isFile() && inputStats.size > this.maxFileSize) {
188
+ throw new Error(`File size ${this.formatBytes(inputStats.size)} exceeds maximum of ${this.formatBytes(this.maxFileSize)}`);
189
+ }
190
+
191
+ const outputDir = path.dirname(resolvedOutputPath);
192
+ await fs.mkdir(outputDir, { recursive: true });
193
+
194
+ this.activeOperations.set(operationId, {
195
+ type: 'compress',
196
+ inputPath: resolvedInputPath,
197
+ outputPath: resolvedOutputPath,
198
+ format,
199
+ level: validatedLevel,
200
+ startedAt: new Date().toISOString(),
201
+ progress: { processed: 0, total: 0 }
202
+ });
203
+
204
+ let result;
205
+
206
+ if (format === 'zip') {
207
+ result = await this.compressToZip(resolvedInputPath, resolvedOutputPath, {
208
+ level: validatedLevel,
209
+ includeRoot,
210
+ password,
211
+ operationId
212
+ });
213
+ } else if (format === 'tar.gz') {
214
+ result = await this.compressToTarGz(resolvedInputPath, resolvedOutputPath, {
215
+ level: validatedLevel,
216
+ includeRoot,
217
+ operationId
218
+ });
219
+ } else {
220
+ throw new Error(`Unsupported compression format: ${format}`);
221
+ }
222
+
223
+ this.activeOperations.delete(operationId);
224
+
225
+ return {
226
+ operationId,
227
+ name: path.basename(outputPath),
228
+ inputPath,
229
+ outputPath,
230
+ format,
231
+ level: validatedLevel,
232
+ size: result.size,
233
+ originalSize: result.originalSize,
234
+ compressionRatio: result.compressionRatio,
235
+ hash: result.hash,
236
+ completedAt: new Date().toISOString()
237
+ };
238
+
239
+ } catch (error) {
240
+ this.activeOperations.delete(operationId);
241
+
242
+ if (this.isFileNotFoundError(error)) {
243
+ throw new Error(`Input not found: ${inputPath}`);
244
+ }
245
+ throw new Error(`Compression failed: ${error.message}`);
246
+ }
247
+ }
248
+
249
+ // Alias for API compatibility
250
+ async compress(inputPath, outputPath, options = {}) {
251
+ return this.compressFile(inputPath, outputPath, options);
252
+ }
253
+
254
+ async compressToZip(inputPath, outputPath, options = {}) {
255
+ const { level, includeRoot, password, operationId } = options;
256
+
257
+ return new Promise(async (resolve, reject) => {
258
+ try {
259
+ const output = fsSync.createWriteStream(outputPath);
260
+ const archive = archiver('zip', {
261
+ zlib: { level: Math.max(1, Math.min(9, level || this.compressionLevel)) }
262
+ });
263
+
264
+ let totalBytes = 0;
265
+ let processedBytes = 0;
266
+
267
+ if (this.enableProgressTracking) {
268
+ totalBytes = await this.calculateTotalSize(inputPath);
269
+ this.updateOperationProgress(operationId, 0, totalBytes);
270
+ }
271
+
272
+ archive.on('progress', (progress) => {
273
+ if (this.enableProgressTracking && operationId) {
274
+ processedBytes = progress.entries.processed;
275
+ this.updateOperationProgress(operationId, processedBytes, totalBytes);
276
+
277
+ this.emit('compressionProgress', {
278
+ operationId,
279
+ processed: processedBytes,
280
+ total: totalBytes,
281
+ percentage: totalBytes > 0 ? Math.round((processedBytes / totalBytes) * 100) : 0
282
+ });
283
+ }
284
+ });
285
+
286
+ archive.on('error', reject);
287
+ output.on('error', reject);
288
+
289
+ output.on('close', async () => {
290
+ try {
291
+ const outputStats = await fs.stat(outputPath);
292
+ const inputStats = await fs.stat(inputPath);
293
+ const originalSize = inputStats.isDirectory() ? totalBytes : inputStats.size;
294
+
295
+ const result = {
296
+ name: path.basename(outputPath),
297
+ size: outputStats.size,
298
+ originalSize: originalSize,
299
+ compressionRatio: originalSize > 0 ? Math.max(0, (originalSize - outputStats.size) / originalSize) : 0
300
+ };
301
+
302
+ if (this.enableChecksumVerification) {
303
+ result.hash = await this.calculateChecksum(outputPath);
304
+ }
305
+
306
+ resolve(result);
307
+ } catch (error) {
308
+ reject(error);
309
+ }
310
+ });
311
+
312
+ archive.pipe(output);
313
+
314
+ const inputStats = await fs.stat(inputPath);
315
+
316
+ if (inputStats.isFile()) {
317
+ const fileName = path.basename(inputPath);
318
+ archive.file(inputPath, { name: fileName });
319
+ } else if (inputStats.isDirectory()) {
320
+ const baseDir = includeRoot ? path.basename(inputPath) : false;
321
+ archive.directory(inputPath, baseDir);
322
+ }
323
+
324
+ await archive.finalize();
325
+
326
+ } catch (error) {
327
+ reject(error);
328
+ }
329
+ });
330
+ }
331
+
332
+ async compressToTarGz(inputPath, outputPath, options = {}) {
333
+ const { level, includeRoot, operationId } = options;
334
+
335
+ try {
336
+ const inputStats = await fs.stat(inputPath);
337
+ let totalBytes = 0;
338
+
339
+ if (this.enableProgressTracking) {
340
+ totalBytes = await this.calculateTotalSize(inputPath);
341
+ this.updateOperationProgress(operationId, 0, totalBytes);
342
+ }
343
+
344
+ const tarOptions = {
345
+ file: outputPath,
346
+ gzip: {
347
+ level: level || this.compressionLevel
348
+ },
349
+ cwd: inputStats.isDirectory() ? (includeRoot ? path.dirname(inputPath) : inputPath) : path.dirname(inputPath)
350
+ };
351
+
352
+ let processedBytes = 0;
353
+ if (this.enableProgressTracking) {
354
+ tarOptions.filter = (filePath, stat) => {
355
+ processedBytes += stat.size || 0;
356
+ this.updateOperationProgress(operationId, processedBytes, totalBytes);
357
+
358
+ this.emit('compressionProgress', {
359
+ operationId,
360
+ processed: processedBytes,
361
+ total: totalBytes,
362
+ percentage: totalBytes > 0 ? Math.round((processedBytes / totalBytes) * 100) : 0
363
+ });
364
+
365
+ return true;
366
+ };
367
+ }
368
+
369
+ if (inputStats.isFile()) {
370
+ await tar.create(tarOptions, [path.basename(inputPath)]);
371
+ } else {
372
+ const targetPath = includeRoot ? path.basename(inputPath) : '.';
373
+ await tar.create(tarOptions, [targetPath]);
374
+ }
375
+
376
+ const outputStats = await fs.stat(outputPath);
377
+ const originalSize = inputStats.isDirectory() ? totalBytes : inputStats.size;
378
+
379
+ const result = {
380
+ name: path.basename(outputPath),
381
+ size: outputStats.size,
382
+ originalSize: originalSize,
383
+ compressionRatio: originalSize > 0 ? Math.max(0, (originalSize - outputStats.size) / originalSize) : 0
384
+ };
385
+
386
+ if (this.enableChecksumVerification) {
387
+ result.hash = await this.calculateChecksum(outputPath);
388
+ }
389
+
390
+ return result;
391
+
392
+ } catch (error) {
393
+ throw new Error(`TAR.GZ compression failed: ${error.message}`);
394
+ }
395
+ }
396
+
397
+ // ============================================================================
398
+ // DECOMPRESSION OPERATIONS
399
+ // ============================================================================
400
+
401
+ async decompressFile(inputPath, outputDirectory, options = {}) {
402
+ if (!this.enabled) {
403
+ throw new Error('Compression operations are disabled in configuration');
404
+ }
405
+
406
+ this.validatePath(inputPath);
407
+ this.validatePath(outputDirectory);
408
+
409
+ const resolvedInputPath = this.normalizePath(inputPath);
410
+ const resolvedOutputDir = this.normalizePath(outputDirectory);
411
+
412
+ const {
413
+ format = this.detectFormat(inputPath),
414
+ overwrite = false,
415
+ preservePermissions = true,
416
+ password = null
417
+ } = options;
418
+
419
+ const operationId = this.generateOperationId();
420
+
421
+ try {
422
+ const inputStats = await fs.stat(resolvedInputPath);
423
+ if (!inputStats.isFile()) {
424
+ throw new Error(`Input '${inputPath}' is not a file`);
425
+ }
426
+
427
+ if (inputStats.size > this.maxFileSize) {
428
+ throw new Error(`Archive size ${this.formatBytes(inputStats.size)} exceeds maximum of ${this.formatBytes(this.maxFileSize)}`);
429
+ }
430
+
431
+ await fs.mkdir(resolvedOutputDir, { recursive: true });
432
+
433
+ this.activeOperations.set(operationId, {
434
+ type: 'decompress',
435
+ inputPath: resolvedInputPath,
436
+ outputPath: resolvedOutputDir,
437
+ format,
438
+ startedAt: new Date().toISOString(),
439
+ progress: { processed: 0, total: inputStats.size }
440
+ });
441
+
442
+ let result;
443
+
444
+ if (format === 'zip') {
445
+ result = await this.decompressFromZip(resolvedInputPath, resolvedOutputDir, {
446
+ overwrite,
447
+ password,
448
+ operationId
449
+ });
450
+ } else if (format === 'tar.gz') {
451
+ result = await this.decompressFromTarGz(resolvedInputPath, resolvedOutputDir, {
452
+ overwrite,
453
+ preservePermissions,
454
+ operationId
455
+ });
456
+ } else {
457
+ throw new Error(`Unsupported decompression format: ${format}`);
458
+ }
459
+
460
+ this.activeOperations.delete(operationId);
461
+
462
+ return {
463
+ operationId,
464
+ inputPath,
465
+ outputDirectory,
466
+ format,
467
+ extractedFiles: result.extractedFiles,
468
+ extractedDirectories: result.extractedDirectories,
469
+ totalSize: result.totalSize,
470
+ hash: result.hash,
471
+ completedAt: new Date().toISOString()
472
+ };
473
+
474
+ } catch (error) {
475
+ this.activeOperations.delete(operationId);
476
+
477
+ if (this.isFileNotFoundError(error)) {
478
+ throw new Error(`Archive not found: ${inputPath}`);
479
+ }
480
+ throw new Error(`Decompression failed: ${error.message}`);
481
+ }
482
+ }
483
+
484
+ // Alias for API compatibility
485
+ async decompress(inputPath, outputDirectory, options = {}) {
486
+ return this.decompressFile(inputPath, outputDirectory, options);
487
+ }
488
+
489
+ async decompressFromZip(inputPath, outputDir, options = {}) {
490
+ const { overwrite, password, operationId } = options;
491
+
492
+ try {
493
+ const inputStats = await fs.stat(inputPath);
494
+
495
+ await fs.mkdir(outputDir, { recursive: true });
496
+
497
+ let extractedFiles = 0;
498
+ let extractedDirectories = 0;
499
+ let totalSize = 0;
500
+
501
+ try {
502
+ const directory = await unzipper.Open.file(inputPath);
503
+
504
+ for (const file of directory.files) {
505
+ if (file.type === 'Directory') {
506
+ extractedDirectories++;
507
+ const dirPath = path.join(outputDir, file.path);
508
+ await fs.mkdir(dirPath, { recursive: true });
509
+ } else if (file.type === 'File') {
510
+ const filePath = path.join(outputDir, file.path);
511
+
512
+ const fileExists = await fs.access(filePath).then(() => true).catch(() => false);
513
+ if (!overwrite && fileExists) {
514
+ continue;
515
+ }
516
+
517
+ const fileDir = path.dirname(filePath);
518
+ await fs.mkdir(fileDir, { recursive: true });
519
+
520
+ const content = await file.buffer();
521
+ await fs.writeFile(filePath, content);
522
+
523
+ extractedFiles++;
524
+ totalSize += content.length;
525
+
526
+ if (this.enableProgressTracking && operationId) {
527
+ this.updateOperationProgress(operationId, totalSize, inputStats.size);
528
+
529
+ this.emit('decompressionProgress', {
530
+ operationId,
531
+ processed: totalSize,
532
+ total: inputStats.size,
533
+ currentFile: file.path,
534
+ percentage: Math.round((totalSize / inputStats.size) * 100)
535
+ });
536
+ }
537
+ }
538
+ }
539
+
540
+ const result = {
541
+ extractedFiles,
542
+ extractedDirectories,
543
+ totalSize
544
+ };
545
+
546
+ if (this.enableChecksumVerification) {
547
+ try {
548
+ result.hash = await this.calculateChecksum(inputPath);
549
+ } catch (error) {
550
+ // Checksum calculation failed, continue without it
551
+ }
552
+ }
553
+
554
+ return result;
555
+
556
+ } catch (openError) {
557
+ // Fallback: use unzipper.Extract
558
+ return new Promise((resolve, reject) => {
559
+ let extractedFiles = 0;
560
+ let extractedDirectories = 0;
561
+ let totalSize = 0;
562
+
563
+ const extract = unzipper.Extract({
564
+ path: outputDir,
565
+ overwrite: overwrite
566
+ });
567
+
568
+ extract.on('entry', (entry) => {
569
+ if (entry.type === 'Directory') {
570
+ extractedDirectories++;
571
+ } else {
572
+ extractedFiles++;
573
+ totalSize += entry.vars.uncompressedSize || 0;
574
+ }
575
+ });
576
+
577
+ extract.on('close', async () => {
578
+ const result = {
579
+ extractedFiles,
580
+ extractedDirectories,
581
+ totalSize
582
+ };
583
+
584
+ if (this.enableChecksumVerification) {
585
+ try {
586
+ result.hash = await this.calculateChecksum(inputPath);
587
+ } catch (error) {
588
+ // Continue without checksum
589
+ }
590
+ }
591
+
592
+ resolve(result);
593
+ });
594
+
595
+ extract.on('error', (extractError) => {
596
+ reject(new Error(`ZIP extraction failed: ${extractError.message}`));
597
+ });
598
+
599
+ const readStream = fsSync.createReadStream(inputPath);
600
+ readStream.pipe(extract);
601
+
602
+ setTimeout(() => {
603
+ reject(new Error('ZIP extraction timeout after 30 seconds'));
604
+ }, 30000);
605
+ });
606
+ }
607
+
608
+ } catch (error) {
609
+ throw new Error(`ZIP decompression failed: ${error.message}`);
610
+ }
611
+ }
612
+
613
+ async decompressFromTarGz(inputPath, outputDir, options = {}) {
614
+ const { overwrite, preservePermissions, operationId } = options;
615
+
616
+ try {
617
+ let extractedFiles = 0;
618
+ let extractedDirectories = 0;
619
+ let totalSize = 0;
620
+ let processedBytes = 0;
621
+
622
+ const extractOptions = {
623
+ file: inputPath,
624
+ cwd: outputDir,
625
+ preservePaths: true,
626
+ unlink: overwrite,
627
+ preserveOwner: preservePermissions
628
+ };
629
+
630
+ if (this.enableProgressTracking) {
631
+ extractOptions.onentry = (entry) => {
632
+ const size = entry.size || 0;
633
+
634
+ if (entry.type === 'Directory') {
635
+ extractedDirectories++;
636
+ } else {
637
+ extractedFiles++;
638
+ totalSize += size;
639
+ }
640
+
641
+ processedBytes += size;
642
+ this.updateOperationProgress(operationId, processedBytes, totalSize);
643
+
644
+ this.emit('decompressionProgress', {
645
+ operationId,
646
+ processed: processedBytes,
647
+ total: totalSize,
648
+ currentFile: entry.path,
649
+ percentage: totalSize > 0 ? Math.round((processedBytes / totalSize) * 100) : 0
650
+ });
651
+ };
652
+ }
653
+
654
+ await tar.extract(extractOptions);
655
+
656
+ const result = {
657
+ extractedFiles,
658
+ extractedDirectories,
659
+ totalSize
660
+ };
661
+
662
+ if (this.enableChecksumVerification) {
663
+ result.hash = await this.calculateChecksum(inputPath);
664
+ }
665
+
666
+ return result;
667
+
668
+ } catch (error) {
669
+ throw new Error(`TAR.GZ decompression failed: ${error.message}`);
670
+ }
671
+ }
672
+
673
+ // ============================================================================
674
+ // BATCH OPERATIONS
675
+ // ============================================================================
676
+
677
+ async compressMultipleFiles(fileList, outputDirectory, options = {}) {
678
+ const {
679
+ format = this.defaultFormat,
680
+ level = this.compressionLevel,
681
+ namingPattern = '{name}.{format}'
682
+ } = options;
683
+
684
+ const results = [];
685
+ const errors = [];
686
+
687
+ for (let i = 0; i < fileList.length; i++) {
688
+ const inputPath = fileList[i];
689
+ try {
690
+ const baseName = path.parse(inputPath).name;
691
+ const outputName = namingPattern
692
+ .replace('{name}', baseName)
693
+ .replace('{format}', format);
694
+ const outputPath = path.join(outputDirectory, outputName);
695
+
696
+ const result = await this.compressFile(inputPath, outputPath, { format, level });
697
+
698
+ results.push({ inputPath, outputPath, result });
699
+ } catch (error) {
700
+ errors.push({ inputPath, error: error.message });
701
+ }
702
+ }
703
+
704
+ return {
705
+ successful: results,
706
+ failed: errors,
707
+ summary: {
708
+ total: fileList.length,
709
+ successful: results.length,
710
+ failed: errors.length,
711
+ successRate: (results.length / fileList.length * 100).toFixed(1) + '%'
712
+ }
713
+ };
714
+ }
715
+
716
+ async decompressMultipleFiles(fileList, outputDirectory, options = {}) {
717
+ const results = [];
718
+ const errors = [];
719
+
720
+ for (let i = 0; i < fileList.length; i++) {
721
+ const inputPath = fileList[i];
722
+ try {
723
+ const archiveName = path.parse(inputPath).name;
724
+ const archiveOutputDir = path.join(outputDirectory, archiveName);
725
+
726
+ const result = await this.decompressFile(inputPath, archiveOutputDir, options);
727
+
728
+ results.push({ inputPath, outputDirectory: archiveOutputDir, result });
729
+ } catch (error) {
730
+ errors.push({ inputPath, error: error.message });
731
+ }
732
+ }
733
+
734
+ return {
735
+ successful: results,
736
+ failed: errors,
737
+ summary: {
738
+ total: fileList.length,
739
+ successful: results.length,
740
+ failed: errors.length,
741
+ successRate: (results.length / fileList.length * 100).toFixed(1) + '%'
742
+ }
743
+ };
744
+ }
745
+
746
+ // ============================================================================
747
+ // UTILITY METHODS
748
+ // ============================================================================
749
+
750
+ detectFormat(filePath) {
751
+ const ext = path.extname(filePath).toLowerCase();
752
+ const fileName = path.basename(filePath).toLowerCase();
753
+
754
+ if (ext === '.zip') {
755
+ return 'zip';
756
+ } else if (fileName.endsWith('.tar.gz') || fileName.endsWith('.tgz')) {
757
+ return 'tar.gz';
758
+ } else {
759
+ return this.detectFormatByHeader(filePath);
760
+ }
761
+ }
762
+
763
+ async detectFormatByHeader(filePath) {
764
+ try {
765
+ const buffer = Buffer.alloc(10);
766
+ const fd = await fs.open(filePath, 'r');
767
+ await fd.read(buffer, 0, 10, 0);
768
+ await fd.close();
769
+
770
+ if (buffer[0] === 0x50 && buffer[1] === 0x4B) {
771
+ return 'zip';
772
+ }
773
+
774
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
775
+ return 'tar.gz';
776
+ }
777
+
778
+ return 'zip';
779
+ } catch (error) {
780
+ return 'zip';
781
+ }
782
+ }
783
+
784
+ getSupportedFormats() {
785
+ return ['zip', 'tar.gz'];
786
+ }
787
+
788
+ async calculateTotalSize(inputPath) {
789
+ const stats = await fs.stat(inputPath);
790
+
791
+ if (stats.isFile()) {
792
+ return stats.size;
793
+ } else if (stats.isDirectory()) {
794
+ return await this.calculateDirectorySize(inputPath);
795
+ }
796
+
797
+ return 0;
798
+ }
799
+
800
+ async calculateDirectorySize(dirPath) {
801
+ let totalSize = 0;
802
+
803
+ try {
804
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
805
+
806
+ for (const entry of entries) {
807
+ const fullPath = path.join(dirPath, entry.name);
808
+
809
+ if (entry.isFile()) {
810
+ const stats = await fs.stat(fullPath);
811
+ totalSize += stats.size;
812
+ } else if (entry.isDirectory()) {
813
+ totalSize += await this.calculateDirectorySize(fullPath);
814
+ }
815
+ }
816
+ } catch (error) {
817
+ // Skip directories we can't read
818
+ }
819
+
820
+ return totalSize;
821
+ }
822
+
823
+ async calculateChecksum(filePath) {
824
+ return new Promise((resolve, reject) => {
825
+ const hash = crypto.createHash('sha256');
826
+ const stream = fsSync.createReadStream(filePath);
827
+
828
+ stream.on('error', reject);
829
+ stream.on('data', chunk => hash.update(chunk));
830
+ stream.on('end', () => resolve(hash.digest('hex')));
831
+ });
832
+ }
833
+
834
+ updateOperationProgress(operationId, processed, total) {
835
+ const operation = this.activeOperations.get(operationId);
836
+ if (operation) {
837
+ operation.progress = { processed, total };
838
+ operation.lastUpdated = new Date().toISOString();
839
+ }
840
+ }
841
+
842
+ // ============================================================================
843
+ // INFORMATION AND STATUS METHODS
844
+ // ============================================================================
845
+
846
+ listActiveOperations() {
847
+ const operations = [];
848
+
849
+ for (const [operationId, operationInfo] of this.activeOperations) {
850
+ operations.push({
851
+ operationId,
852
+ type: operationInfo.type,
853
+ inputPath: operationInfo.inputPath,
854
+ outputPath: operationInfo.outputPath,
855
+ format: operationInfo.format,
856
+ startedAt: operationInfo.startedAt,
857
+ progress: operationInfo.progress,
858
+ duration: Date.now() - new Date(operationInfo.startedAt).getTime()
859
+ });
860
+ }
861
+
862
+ return operations;
863
+ }
864
+
865
+ getCompressionStatus() {
866
+ return {
867
+ enabled: this.enabled,
868
+ activeOperations: this.activeOperations.size,
869
+ supportedFormats: ['zip', 'tar.gz'],
870
+ defaultFormat: this.defaultFormat,
871
+ compressionLevel: this.compressionLevel,
872
+ enableProgressTracking: this.enableProgressTracking,
873
+ enableChecksumVerification: this.enableChecksumVerification,
874
+ config: {
875
+ maxFileSize: this.maxFileSize,
876
+ chunkSize: this.chunkSize
877
+ }
878
+ };
879
+ }
880
+
881
+ // ============================================================================
882
+ // UTILITY METHODS
883
+ // ============================================================================
884
+
885
+ formatBytes(bytes) {
886
+ if (bytes === 0) return '0 Bytes';
887
+ const k = 1024;
888
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
889
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
890
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
891
+ }
892
+
893
+ formatDuration(milliseconds) {
894
+ const seconds = Math.floor(milliseconds / 1000);
895
+ const minutes = Math.floor(seconds / 60);
896
+ const hours = Math.floor(minutes / 60);
897
+
898
+ if (hours > 0) {
899
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
900
+ } else if (minutes > 0) {
901
+ return `${minutes}m ${seconds % 60}s`;
902
+ } else {
903
+ return `${seconds}s`;
904
+ }
905
+ }
906
+
907
+ // ============================================================================
908
+ // ERROR HANDLING HELPERS
909
+ // ============================================================================
910
+
911
+ isFileNotFoundError(error) {
912
+ return error.code === 'ENOENT' ||
913
+ error.message.includes('no such file') ||
914
+ error.message.includes('not found');
915
+ }
916
+
917
+ isPermissionError(error) {
918
+ return error.code === 'EACCES' ||
919
+ error.code === 'EPERM' ||
920
+ error.message.includes('permission denied');
921
+ }
922
+
923
+ isSpaceError(error) {
924
+ return error.code === 'ENOSPC' ||
925
+ error.message.includes('no space left') ||
926
+ error.message.includes('disk full');
927
+ }
928
+
929
+ isCorruptedArchiveError(error) {
930
+ return error.message.includes('invalid zip file') ||
931
+ error.message.includes('corrupted') ||
932
+ error.message.includes('bad archive') ||
933
+ error.message.includes('unexpected end of archive');
934
+ }
935
+
936
+ // ============================================================================
937
+ // CLEANUP AND SHUTDOWN
938
+ // ============================================================================
939
+
940
+ async shutdown() {
941
+ try {
942
+ const activeOps = Array.from(this.activeOperations.keys());
943
+
944
+ if (activeOps.length > 0) {
945
+ const timeout = setTimeout(() => {
946
+ this.activeOperations.clear();
947
+ }, 30000);
948
+
949
+ while (this.activeOperations.size > 0) {
950
+ await new Promise(resolve => setTimeout(resolve, 1000));
951
+ }
952
+
953
+ clearTimeout(timeout);
954
+ }
955
+
956
+ this.removeAllListeners();
957
+
958
+ return {
959
+ operationsCompleted: activeOps.length,
960
+ shutdownAt: new Date().toISOString()
961
+ };
962
+ } catch (error) {
963
+ throw error;
964
+ }
965
+ }
966
+ }
967
+
968
+ export { CompressionProvider };