@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.
- package/README.md +268 -0
- package/package.json +48 -0
- package/src/ConfigManager.js +301 -0
- package/src/FileManager.js +526 -0
- package/src/index.js +48 -0
- package/src/providers/CompressionProvider.js +968 -0
- package/src/providers/LocalProvider.js +824 -0
- package/src/providers/RemoteProvider.js +514 -0
- package/src/providers/WatchProvider.js +611 -0
- package/src/utils/FileStreamingUtils.js +757 -0
- package/src/utils/PathUtils.js +144 -0
|
@@ -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 };
|