@push.rocks/smartarchive 4.2.3 → 5.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.
Files changed (42) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/bzip2/bititerator.d.ts +6 -1
  3. package/dist_ts/bzip2/bititerator.js +30 -24
  4. package/dist_ts/bzip2/bzip2.d.ts +27 -12
  5. package/dist_ts/bzip2/bzip2.js +325 -263
  6. package/dist_ts/bzip2/index.d.ts +4 -1
  7. package/dist_ts/bzip2/index.js +36 -38
  8. package/dist_ts/classes.archiveanalyzer.d.ts +25 -5
  9. package/dist_ts/classes.archiveanalyzer.js +19 -8
  10. package/dist_ts/classes.bzip2tools.d.ts +1 -1
  11. package/dist_ts/classes.gziptools.d.ts +39 -6
  12. package/dist_ts/classes.gziptools.js +85 -24
  13. package/dist_ts/classes.smartarchive.d.ts +101 -16
  14. package/dist_ts/classes.smartarchive.js +398 -61
  15. package/dist_ts/classes.tartools.d.ts +31 -4
  16. package/dist_ts/classes.tartools.js +97 -36
  17. package/dist_ts/classes.ziptools.d.ts +47 -12
  18. package/dist_ts/classes.ziptools.js +142 -23
  19. package/dist_ts/errors.d.ts +44 -0
  20. package/dist_ts/errors.js +62 -0
  21. package/dist_ts/index.d.ts +4 -0
  22. package/dist_ts/index.js +9 -1
  23. package/dist_ts/interfaces.d.ts +115 -0
  24. package/dist_ts/interfaces.js +2 -0
  25. package/dist_ts/plugins.d.ts +9 -3
  26. package/dist_ts/plugins.js +27 -4
  27. package/package.json +3 -4
  28. package/readme.hints.md +38 -1
  29. package/readme.md +248 -137
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/bzip2/bititerator.ts +43 -27
  32. package/ts/bzip2/bzip2.ts +289 -171
  33. package/ts/bzip2/index.ts +52 -50
  34. package/ts/classes.archiveanalyzer.ts +42 -28
  35. package/ts/classes.gziptools.ts +96 -33
  36. package/ts/classes.smartarchive.ts +472 -122
  37. package/ts/classes.tartools.ts +128 -59
  38. package/ts/classes.ziptools.ts +160 -34
  39. package/ts/errors.ts +70 -0
  40. package/ts/index.ts +11 -0
  41. package/ts/interfaces.ts +131 -0
  42. package/ts/plugins.ts +29 -3
@@ -1,75 +1,267 @@
1
1
  import * as plugins from './plugins.js';
2
- import * as paths from './paths.js';
2
+ import type {
3
+ IArchiveCreationOptions,
4
+ IArchiveEntry,
5
+ IArchiveExtractionOptions,
6
+ IArchiveEntryInfo,
7
+ IArchiveInfo,
8
+ TArchiveFormat,
9
+ TCompressionLevel,
10
+ } from './interfaces.js';
3
11
 
4
12
  import { Bzip2Tools } from './classes.bzip2tools.js';
5
13
  import { GzipTools } from './classes.gziptools.js';
6
14
  import { TarTools } from './classes.tartools.js';
7
15
  import { ZipTools } from './classes.ziptools.js';
16
+ import { ArchiveAnalyzer, type IAnalyzedResult } from './classes.archiveanalyzer.js';
8
17
 
9
- import {
10
- ArchiveAnalyzer,
11
- type IAnalyzedResult,
12
- } from './classes.archiveanalyzer.js';
13
-
14
- import type { from } from '@push.rocks/smartrx/dist_ts/smartrx.plugins.rxjs.js';
15
-
18
+ /**
19
+ * Main class for archive manipulation
20
+ * Supports TAR, ZIP, GZIP, and BZIP2 formats
21
+ */
16
22
  export class SmartArchive {
17
- // STATIC
18
- public static async fromArchiveUrl(urlArg: string): Promise<SmartArchive> {
23
+ // ============================================
24
+ // STATIC FACTORY METHODS - EXTRACTION
25
+ // ============================================
26
+
27
+ /**
28
+ * Create SmartArchive from a URL
29
+ */
30
+ public static async fromUrl(urlArg: string): Promise<SmartArchive> {
19
31
  const smartArchiveInstance = new SmartArchive();
20
32
  smartArchiveInstance.sourceUrl = urlArg;
21
33
  return smartArchiveInstance;
22
34
  }
23
35
 
24
- public static async fromArchiveFile(
25
- filePathArg: string,
26
- ): Promise<SmartArchive> {
36
+ /**
37
+ * Create SmartArchive from a local file path
38
+ */
39
+ public static async fromFile(filePathArg: string): Promise<SmartArchive> {
27
40
  const smartArchiveInstance = new SmartArchive();
28
41
  smartArchiveInstance.sourceFilePath = filePathArg;
29
42
  return smartArchiveInstance;
30
43
  }
31
44
 
32
- public static async fromArchiveStream(
33
- streamArg:
34
- | plugins.stream.Readable
35
- | plugins.stream.Duplex
36
- | plugins.stream.Transform,
45
+ /**
46
+ * Create SmartArchive from a readable stream
47
+ */
48
+ public static async fromStream(
49
+ streamArg: plugins.stream.Readable | plugins.stream.Duplex | plugins.stream.Transform
37
50
  ): Promise<SmartArchive> {
38
51
  const smartArchiveInstance = new SmartArchive();
39
52
  smartArchiveInstance.sourceStream = streamArg;
40
53
  return smartArchiveInstance;
41
54
  }
42
55
 
43
- // INSTANCE
56
+ /**
57
+ * Create SmartArchive from an in-memory buffer
58
+ */
59
+ public static async fromBuffer(buffer: Buffer): Promise<SmartArchive> {
60
+ const smartArchiveInstance = new SmartArchive();
61
+ smartArchiveInstance.sourceStream = plugins.stream.Readable.from(buffer);
62
+ return smartArchiveInstance;
63
+ }
64
+
65
+ // ============================================
66
+ // STATIC FACTORY METHODS - CREATION
67
+ // ============================================
68
+
69
+ /**
70
+ * Create a new archive from a directory
71
+ */
72
+ public static async fromDirectory(
73
+ directoryPath: string,
74
+ options: IArchiveCreationOptions
75
+ ): Promise<SmartArchive> {
76
+ const smartArchiveInstance = new SmartArchive();
77
+ smartArchiveInstance.creationOptions = options;
78
+
79
+ const tarTools = new TarTools();
80
+
81
+ if (options.format === 'tar' || options.format === 'tar.gz' || options.format === 'tgz') {
82
+ if (options.format === 'tar') {
83
+ const pack = await tarTools.packDirectory(directoryPath);
84
+ pack.finalize();
85
+ smartArchiveInstance.archiveBuffer = await SmartArchive.streamToBuffer(pack);
86
+ } else {
87
+ smartArchiveInstance.archiveBuffer = await tarTools.packDirectoryToTarGz(
88
+ directoryPath,
89
+ options.compressionLevel
90
+ );
91
+ }
92
+ } else if (options.format === 'zip') {
93
+ const zipTools = new ZipTools();
94
+ const fileTree = await plugins.listFileTree(directoryPath, '**/*');
95
+ const entries: IArchiveEntry[] = [];
96
+
97
+ for (const filePath of fileTree) {
98
+ const absolutePath = plugins.path.join(directoryPath, filePath);
99
+ const content = await plugins.fsPromises.readFile(absolutePath);
100
+ entries.push({
101
+ archivePath: filePath,
102
+ content,
103
+ });
104
+ }
105
+
106
+ smartArchiveInstance.archiveBuffer = await zipTools.createZip(entries, options.compressionLevel);
107
+ } else {
108
+ throw new Error(`Unsupported format for directory packing: ${options.format}`);
109
+ }
110
+
111
+ return smartArchiveInstance;
112
+ }
113
+
114
+ /**
115
+ * Create a new archive from an array of entries
116
+ */
117
+ public static async fromFiles(
118
+ files: IArchiveEntry[],
119
+ options: IArchiveCreationOptions
120
+ ): Promise<SmartArchive> {
121
+ const smartArchiveInstance = new SmartArchive();
122
+ smartArchiveInstance.creationOptions = options;
123
+
124
+ if (options.format === 'tar' || options.format === 'tar.gz' || options.format === 'tgz') {
125
+ const tarTools = new TarTools();
126
+ if (options.format === 'tar') {
127
+ smartArchiveInstance.archiveBuffer = await tarTools.packFiles(files);
128
+ } else {
129
+ smartArchiveInstance.archiveBuffer = await tarTools.packFilesToTarGz(files, options.compressionLevel);
130
+ }
131
+ } else if (options.format === 'zip') {
132
+ const zipTools = new ZipTools();
133
+ smartArchiveInstance.archiveBuffer = await zipTools.createZip(files, options.compressionLevel);
134
+ } else if (options.format === 'gz') {
135
+ if (files.length !== 1) {
136
+ throw new Error('GZIP format only supports a single file');
137
+ }
138
+ const gzipTools = new GzipTools();
139
+ let content: Buffer;
140
+ if (typeof files[0].content === 'string') {
141
+ content = Buffer.from(files[0].content);
142
+ } else if (Buffer.isBuffer(files[0].content)) {
143
+ content = files[0].content;
144
+ } else {
145
+ throw new Error('GZIP format requires string or Buffer content');
146
+ }
147
+ smartArchiveInstance.archiveBuffer = await gzipTools.compress(content, options.compressionLevel);
148
+ } else {
149
+ throw new Error(`Unsupported format: ${options.format}`);
150
+ }
151
+
152
+ return smartArchiveInstance;
153
+ }
154
+
155
+ /**
156
+ * Start building an archive incrementally using a builder pattern
157
+ */
158
+ public static create(options: IArchiveCreationOptions): SmartArchive {
159
+ const smartArchiveInstance = new SmartArchive();
160
+ smartArchiveInstance.creationOptions = options;
161
+ smartArchiveInstance.pendingEntries = [];
162
+ return smartArchiveInstance;
163
+ }
164
+
165
+ /**
166
+ * Helper to convert a stream to buffer
167
+ */
168
+ private static async streamToBuffer(stream: plugins.stream.Readable): Promise<Buffer> {
169
+ const chunks: Buffer[] = [];
170
+ return new Promise((resolve, reject) => {
171
+ stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
172
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
173
+ stream.on('error', reject);
174
+ });
175
+ }
176
+
177
+ // ============================================
178
+ // INSTANCE PROPERTIES
179
+ // ============================================
180
+
44
181
  public tarTools = new TarTools();
45
182
  public zipTools = new ZipTools();
46
183
  public gzipTools = new GzipTools();
47
184
  public bzip2Tools = new Bzip2Tools(this);
48
185
  public archiveAnalyzer = new ArchiveAnalyzer(this);
49
186
 
50
- public sourceUrl: string;
51
- public sourceFilePath: string;
52
- public sourceStream:
53
- | plugins.stream.Readable
54
- | plugins.stream.Duplex
55
- | plugins.stream.Transform;
187
+ public sourceUrl?: string;
188
+ public sourceFilePath?: string;
189
+ public sourceStream?: plugins.stream.Readable | plugins.stream.Duplex | plugins.stream.Transform;
56
190
 
57
- public archiveName: string;
58
- public singleFileMode: boolean = false;
59
-
60
- public addedDirectories: string[] = [];
61
- public addedFiles: (
62
- | plugins.smartfile.SmartFile
63
- | plugins.smartfile.StreamFile
64
- )[] = [];
65
- public addedUrls: string[] = [];
191
+ private archiveBuffer?: Buffer;
192
+ private creationOptions?: IArchiveCreationOptions;
193
+ private pendingEntries?: IArchiveEntry[];
66
194
 
67
195
  constructor() {}
68
196
 
197
+ // ============================================
198
+ // BUILDER METHODS (for incremental creation)
199
+ // ============================================
200
+
201
+ /**
202
+ * Add a file to the archive (builder pattern)
203
+ */
204
+ public addFile(archivePath: string, content: string | Buffer): this {
205
+ if (!this.pendingEntries) {
206
+ throw new Error('addFile can only be called on archives created with SmartArchive.create()');
207
+ }
208
+ this.pendingEntries.push({ archivePath, content });
209
+ return this;
210
+ }
211
+
212
+ /**
213
+ * Add a SmartFile to the archive (builder pattern)
214
+ */
215
+ public addSmartFile(file: plugins.smartfile.SmartFile, archivePath?: string): this {
216
+ if (!this.pendingEntries) {
217
+ throw new Error('addSmartFile can only be called on archives created with SmartArchive.create()');
218
+ }
219
+ this.pendingEntries.push({
220
+ archivePath: archivePath || file.relative,
221
+ content: file,
222
+ });
223
+ return this;
224
+ }
225
+
226
+ /**
227
+ * Add a StreamFile to the archive (builder pattern)
228
+ */
229
+ public addStreamFile(file: plugins.smartfile.StreamFile, archivePath?: string): this {
230
+ if (!this.pendingEntries) {
231
+ throw new Error('addStreamFile can only be called on archives created with SmartArchive.create()');
232
+ }
233
+ this.pendingEntries.push({
234
+ archivePath: archivePath || file.relativeFilePath,
235
+ content: file,
236
+ });
237
+ return this;
238
+ }
239
+
240
+ /**
241
+ * Build the archive from pending entries
242
+ */
243
+ public async build(): Promise<SmartArchive> {
244
+ if (!this.pendingEntries || !this.creationOptions) {
245
+ throw new Error('build can only be called on archives created with SmartArchive.create()');
246
+ }
247
+
248
+ const built = await SmartArchive.fromFiles(this.pendingEntries, this.creationOptions);
249
+ this.archiveBuffer = built.archiveBuffer;
250
+ this.pendingEntries = undefined;
251
+ return this;
252
+ }
253
+
254
+ // ============================================
255
+ // EXTRACTION METHODS
256
+ // ============================================
257
+
69
258
  /**
70
- * gets the original archive stream
259
+ * Get the original archive stream
71
260
  */
72
- public async getArchiveStream() {
261
+ public async toStream(): Promise<plugins.stream.Readable> {
262
+ if (this.archiveBuffer) {
263
+ return plugins.stream.Readable.from(this.archiveBuffer);
264
+ }
73
265
  if (this.sourceStream) {
74
266
  return this.sourceStream;
75
267
  }
@@ -78,162 +270,320 @@ export class SmartArchive {
78
270
  .url(this.sourceUrl)
79
271
  .get();
80
272
  const webStream = response.stream();
81
- // @ts-ignore - Web stream to Node.js stream conversion
82
- const urlStream = plugins.stream.Readable.fromWeb(webStream);
83
- return urlStream;
273
+ return plugins.stream.Readable.fromWeb(webStream as any);
84
274
  }
85
275
  if (this.sourceFilePath) {
86
- const fileStream = plugins.smartfile.fs.toReadStream(this.sourceFilePath);
87
- return fileStream;
276
+ return plugins.fs.createReadStream(this.sourceFilePath);
277
+ }
278
+ throw new Error('No archive source configured');
279
+ }
280
+
281
+ /**
282
+ * Get archive as a Buffer
283
+ */
284
+ public async toBuffer(): Promise<Buffer> {
285
+ if (this.archiveBuffer) {
286
+ return this.archiveBuffer;
88
287
  }
288
+ const stream = await this.toStream();
289
+ return SmartArchive.streamToBuffer(stream);
89
290
  }
90
291
 
91
- public async exportToTarGzStream() {
92
- const tarPackStream = await this.tarTools.getPackStream();
93
- const gzipStream = await this.gzipTools.getCompressionStream();
94
- // const archiveStream = tarPackStream.pipe(gzipStream);
95
- // return archiveStream;
292
+ /**
293
+ * Write archive to a file
294
+ */
295
+ public async toFile(filePath: string): Promise<void> {
296
+ const buffer = await this.toBuffer();
297
+ await plugins.fsPromises.mkdir(plugins.path.dirname(filePath), { recursive: true });
298
+ await plugins.fsPromises.writeFile(filePath, buffer);
96
299
  }
97
300
 
98
- public async exportToFs(
301
+ /**
302
+ * Extract archive to filesystem
303
+ */
304
+ public async extractToDirectory(
99
305
  targetDir: string,
100
- fileNameArg?: string,
306
+ options?: Partial<IArchiveExtractionOptions>
101
307
  ): Promise<void> {
102
308
  const done = plugins.smartpromise.defer<void>();
103
- const streamFileStream = await this.exportToStreamOfStreamFiles();
309
+ const streamFileStream = await this.extractToStream();
310
+
104
311
  streamFileStream.pipe(
105
312
  new plugins.smartstream.SmartDuplex({
106
313
  objectMode: true,
107
- writeFunction: async (
108
- streamFileArg: plugins.smartfile.StreamFile,
109
- streamtools,
110
- ) => {
111
- const done = plugins.smartpromise.defer<void>();
112
- console.log(
113
- streamFileArg.relativeFilePath
114
- ? streamFileArg.relativeFilePath
115
- : 'no relative path',
116
- );
314
+ writeFunction: async (streamFileArg: plugins.smartfile.StreamFile) => {
315
+ const innerDone = plugins.smartpromise.defer<void>();
117
316
  const streamFile = streamFileArg;
317
+ let relativePath = streamFile.relativeFilePath || options?.fileName || 'extracted_file';
318
+
319
+ // Apply stripComponents if specified
320
+ if (options?.stripComponents && options.stripComponents > 0) {
321
+ const parts = relativePath.split('/');
322
+ relativePath = parts.slice(options.stripComponents).join('/');
323
+ if (!relativePath) {
324
+ innerDone.resolve();
325
+ return;
326
+ }
327
+ }
328
+
329
+ // Apply filter if specified
330
+ if (options?.filter) {
331
+ const entryInfo: IArchiveEntryInfo = {
332
+ path: relativePath,
333
+ size: 0,
334
+ isDirectory: false,
335
+ isFile: true,
336
+ };
337
+ if (!options.filter(entryInfo)) {
338
+ innerDone.resolve();
339
+ return;
340
+ }
341
+ }
342
+
118
343
  const readStream = await streamFile.createReadStream();
119
- await plugins.smartfile.fs.ensureDir(targetDir);
120
- const writePath = plugins.path.join(
121
- targetDir,
122
- streamFile.relativeFilePath || fileNameArg,
123
- );
124
- await plugins.smartfile.fs.ensureDir(plugins.path.dirname(writePath));
125
- const writeStream =
126
- plugins.smartfile.fsStream.createWriteStream(writePath);
344
+ await plugins.fsPromises.mkdir(targetDir, { recursive: true });
345
+ const writePath = plugins.path.join(targetDir, relativePath);
346
+ await plugins.fsPromises.mkdir(plugins.path.dirname(writePath), { recursive: true });
347
+ const writeStream = plugins.fs.createWriteStream(writePath);
127
348
  readStream.pipe(writeStream);
128
349
  writeStream.on('finish', () => {
129
- done.resolve();
350
+ innerDone.resolve();
130
351
  });
131
- await done.promise;
352
+ await innerDone.promise;
132
353
  },
133
354
  finalFunction: async () => {
134
355
  done.resolve();
135
356
  },
136
- }),
357
+ })
137
358
  );
359
+
138
360
  return done.promise;
139
361
  }
140
362
 
141
- public async exportToStreamOfStreamFiles() {
142
- const streamFileIntake =
143
- new plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>({
144
- objectMode: true,
145
- });
146
- const archiveStream = await this.getArchiveStream();
363
+ /**
364
+ * Extract archive to a stream of StreamFile objects
365
+ */
366
+ public async extractToStream(): Promise<plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>> {
367
+ const streamFileIntake = new plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>({
368
+ objectMode: true,
369
+ });
370
+
371
+ // Guard to prevent multiple signalEnd calls
372
+ let hasSignaledEnd = false;
373
+ const safeSignalEnd = () => {
374
+ if (!hasSignaledEnd) {
375
+ hasSignaledEnd = true;
376
+ streamFileIntake.signalEnd();
377
+ }
378
+ };
379
+
380
+ const archiveStream = await this.toStream();
147
381
  const createAnalyzedStream = () => this.archiveAnalyzer.getAnalyzedStream();
148
382
 
149
- // lets create a function that can be called multiple times to unpack layers of archives
150
383
  const createUnpackStream = () =>
151
- plugins.smartstream.createTransformFunction<IAnalyzedResult, any>(
384
+ plugins.smartstream.createTransformFunction<IAnalyzedResult, void>(
152
385
  async (analyzedResultChunk) => {
153
386
  if (analyzedResultChunk.fileType?.mime === 'application/x-tar') {
154
- const tarStream =
155
- analyzedResultChunk.decompressionStream as plugins.tarStream.Extract;
387
+ const tarStream = analyzedResultChunk.decompressionStream as plugins.tarStream.Extract;
388
+
156
389
  tarStream.on('entry', async (header, stream, next) => {
157
390
  if (header.type === 'directory') {
158
- console.log(
159
- `tar stream directory: ${header.name} ... skipping!`,
160
- );
161
- stream.resume(); // Consume directory stream
391
+ stream.resume();
162
392
  stream.on('end', () => next());
163
393
  return;
164
394
  }
165
- console.log(`tar stream file: ${header.name}`);
166
-
167
- // Create a PassThrough stream to buffer the data
395
+
168
396
  const passThrough = new plugins.stream.PassThrough();
169
- const streamfile = plugins.smartfile.StreamFile.fromStream(
170
- passThrough,
171
- header.name,
172
- );
173
-
174
- // Push the streamfile immediately
397
+ const streamfile = plugins.smartfile.StreamFile.fromStream(passThrough, header.name);
175
398
  streamFileIntake.push(streamfile);
176
-
177
- // Pipe the tar entry stream to the passthrough
178
399
  stream.pipe(passThrough);
179
-
180
- // Move to next entry when this one ends
181
400
  stream.on('end', () => {
182
401
  passThrough.end();
183
402
  next();
184
403
  });
185
404
  });
186
- tarStream.on('finish', function () {
187
- console.log('tar extraction finished');
188
- // Only signal end if this is the final stream
189
- streamFileIntake.signalEnd();
405
+
406
+ tarStream.on('finish', () => {
407
+ safeSignalEnd();
190
408
  });
191
- analyzedResultChunk.resultStream.pipe(
192
- analyzedResultChunk.decompressionStream,
193
- );
409
+
410
+ analyzedResultChunk.resultStream.pipe(analyzedResultChunk.decompressionStream);
194
411
  } else if (analyzedResultChunk.fileType?.mime === 'application/zip') {
195
412
  analyzedResultChunk.resultStream
196
413
  .pipe(analyzedResultChunk.decompressionStream)
197
414
  .pipe(
198
415
  new plugins.smartstream.SmartDuplex({
199
416
  objectMode: true,
200
- writeFunction: async (
201
- streamFileArg: plugins.smartfile.StreamFile,
202
- streamtools,
203
- ) => {
417
+ writeFunction: async (streamFileArg: plugins.smartfile.StreamFile) => {
204
418
  streamFileIntake.push(streamFileArg);
205
419
  },
206
420
  finalFunction: async () => {
207
- streamFileIntake.signalEnd();
421
+ safeSignalEnd();
208
422
  },
209
- }),
423
+ })
210
424
  );
211
- } else if (
212
- analyzedResultChunk.isArchive &&
213
- analyzedResultChunk.decompressionStream
214
- ) {
425
+ } else if (analyzedResultChunk.isArchive && analyzedResultChunk.decompressionStream) {
215
426
  // For nested archives (like gzip containing tar)
216
- const nestedStream = analyzedResultChunk.resultStream
427
+ analyzedResultChunk.resultStream
217
428
  .pipe(analyzedResultChunk.decompressionStream)
218
429
  .pipe(createAnalyzedStream())
219
430
  .pipe(createUnpackStream());
220
-
221
- // Don't signal end here - let the nested unpacker handle it
222
431
  } else {
223
432
  const streamFile = plugins.smartfile.StreamFile.fromStream(
224
433
  analyzedResultChunk.resultStream,
225
- analyzedResultChunk.fileType?.ext,
434
+ analyzedResultChunk.fileType?.ext
226
435
  );
227
436
  streamFileIntake.push(streamFile);
228
- streamFileIntake.signalEnd();
437
+ safeSignalEnd();
229
438
  }
230
439
  },
231
- {
232
- objectMode: true,
233
- },
440
+ { objectMode: true }
234
441
  );
235
442
 
236
443
  archiveStream.pipe(createAnalyzedStream()).pipe(createUnpackStream());
237
444
  return streamFileIntake;
238
445
  }
446
+
447
+ /**
448
+ * Extract archive to an array of SmartFile objects (in-memory)
449
+ */
450
+ public async extractToSmartFiles(): Promise<plugins.smartfile.SmartFile[]> {
451
+ const streamFiles = await this.extractToStream();
452
+ const smartFiles: plugins.smartfile.SmartFile[] = [];
453
+
454
+ return new Promise((resolve, reject) => {
455
+ streamFiles.on('data', async (streamFile: plugins.smartfile.StreamFile) => {
456
+ try {
457
+ const smartFile = await streamFile.toSmartFile();
458
+ smartFiles.push(smartFile);
459
+ } catch (err) {
460
+ reject(err);
461
+ }
462
+ });
463
+ streamFiles.on('end', () => resolve(smartFiles));
464
+ streamFiles.on('error', reject);
465
+ });
466
+ }
467
+
468
+ /**
469
+ * Extract a single file from the archive by path
470
+ */
471
+ public async extractFile(filePath: string): Promise<plugins.smartfile.SmartFile | null> {
472
+ const streamFiles = await this.extractToStream();
473
+
474
+ return new Promise((resolve, reject) => {
475
+ let found = false;
476
+
477
+ streamFiles.on('data', async (streamFile: plugins.smartfile.StreamFile) => {
478
+ if (streamFile.relativeFilePath === filePath || streamFile.relativeFilePath?.endsWith(filePath)) {
479
+ found = true;
480
+ try {
481
+ const smartFile = await streamFile.toSmartFile();
482
+ resolve(smartFile);
483
+ } catch (err) {
484
+ reject(err);
485
+ }
486
+ }
487
+ });
488
+
489
+ streamFiles.on('end', () => {
490
+ if (!found) {
491
+ resolve(null);
492
+ }
493
+ });
494
+
495
+ streamFiles.on('error', reject);
496
+ });
497
+ }
498
+
499
+ // ============================================
500
+ // ANALYSIS METHODS
501
+ // ============================================
502
+
503
+ /**
504
+ * Analyze the archive and return metadata
505
+ */
506
+ public async analyze(): Promise<IArchiveInfo> {
507
+ const stream = await this.toStream();
508
+ const firstChunk = await this.readFirstChunk(stream);
509
+ const fileType = await plugins.fileType.fileTypeFromBuffer(firstChunk);
510
+
511
+ let format: TArchiveFormat | null = null;
512
+ let isCompressed = false;
513
+ let isArchive = false;
514
+
515
+ if (fileType) {
516
+ switch (fileType.mime) {
517
+ case 'application/gzip':
518
+ format = 'gz';
519
+ isCompressed = true;
520
+ isArchive = true;
521
+ break;
522
+ case 'application/zip':
523
+ format = 'zip';
524
+ isCompressed = true;
525
+ isArchive = true;
526
+ break;
527
+ case 'application/x-tar':
528
+ format = 'tar';
529
+ isArchive = true;
530
+ break;
531
+ case 'application/x-bzip2':
532
+ format = 'bz2';
533
+ isCompressed = true;
534
+ isArchive = true;
535
+ break;
536
+ }
537
+ }
538
+
539
+ return {
540
+ format,
541
+ isCompressed,
542
+ isArchive,
543
+ };
544
+ }
545
+
546
+ /**
547
+ * List all entries in the archive without extracting
548
+ */
549
+ public async listEntries(): Promise<IArchiveEntryInfo[]> {
550
+ const entries: IArchiveEntryInfo[] = [];
551
+ const streamFiles = await this.extractToStream();
552
+
553
+ return new Promise((resolve, reject) => {
554
+ streamFiles.on('data', (streamFile: plugins.smartfile.StreamFile) => {
555
+ entries.push({
556
+ path: streamFile.relativeFilePath || 'unknown',
557
+ size: 0, // Size not available without reading
558
+ isDirectory: false,
559
+ isFile: true,
560
+ });
561
+ });
562
+ streamFiles.on('end', () => resolve(entries));
563
+ streamFiles.on('error', reject);
564
+ });
565
+ }
566
+
567
+ /**
568
+ * Check if a specific file exists in the archive
569
+ */
570
+ public async hasFile(filePath: string): Promise<boolean> {
571
+ const entries = await this.listEntries();
572
+ return entries.some((e) => e.path === filePath || e.path.endsWith(filePath));
573
+ }
574
+
575
+ /**
576
+ * Helper to read first chunk from stream
577
+ */
578
+ private async readFirstChunk(stream: plugins.stream.Readable): Promise<Buffer> {
579
+ return new Promise((resolve, reject) => {
580
+ const onData = (chunk: Buffer) => {
581
+ stream.removeListener('data', onData);
582
+ stream.removeListener('error', reject);
583
+ resolve(chunk);
584
+ };
585
+ stream.on('data', onData);
586
+ stream.on('error', reject);
587
+ });
588
+ }
239
589
  }