@push.rocks/smartarchive 4.2.4 → 5.0.1
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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/bzip2/bititerator.d.ts +6 -1
- package/dist_ts/bzip2/bititerator.js +30 -24
- package/dist_ts/bzip2/bzip2.d.ts +27 -12
- package/dist_ts/bzip2/bzip2.js +325 -263
- package/dist_ts/bzip2/index.d.ts +4 -1
- package/dist_ts/bzip2/index.js +36 -38
- package/dist_ts/classes.archiveanalyzer.d.ts +25 -5
- package/dist_ts/classes.archiveanalyzer.js +19 -8
- package/dist_ts/classes.bzip2tools.d.ts +1 -1
- package/dist_ts/classes.gziptools.d.ts +43 -6
- package/dist_ts/classes.gziptools.js +76 -24
- package/dist_ts/classes.smartarchive.d.ts +198 -16
- package/dist_ts/classes.smartarchive.js +652 -79
- package/dist_ts/classes.tartools.d.ts +31 -4
- package/dist_ts/classes.tartools.js +93 -32
- package/dist_ts/classes.ziptools.d.ts +47 -12
- package/dist_ts/classes.ziptools.js +128 -23
- package/dist_ts/errors.d.ts +44 -0
- package/dist_ts/errors.js +62 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +9 -1
- package/dist_ts/interfaces.d.ts +119 -0
- package/dist_ts/interfaces.js +2 -0
- package/package.json +1 -1
- package/readme.hints.md +69 -23
- package/readme.md +360 -274
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/bzip2/bititerator.ts +43 -27
- package/ts/bzip2/bzip2.ts +289 -171
- package/ts/bzip2/index.ts +52 -50
- package/ts/classes.archiveanalyzer.ts +42 -28
- package/ts/classes.gziptools.ts +91 -33
- package/ts/classes.smartarchive.ts +765 -140
- package/ts/classes.tartools.ts +124 -52
- package/ts/classes.ziptools.ts +147 -34
- package/ts/errors.ts +70 -0
- package/ts/index.ts +11 -0
- package/ts/interfaces.ts +136 -0
|
@@ -1,238 +1,863 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import
|
|
2
|
+
import type {
|
|
3
|
+
IArchiveEntry,
|
|
4
|
+
IArchiveEntryInfo,
|
|
5
|
+
IArchiveInfo,
|
|
6
|
+
TArchiveFormat,
|
|
7
|
+
TCompressionLevel,
|
|
8
|
+
TEntryFilter,
|
|
9
|
+
} from './interfaces.js';
|
|
3
10
|
|
|
4
11
|
import { Bzip2Tools } from './classes.bzip2tools.js';
|
|
5
12
|
import { GzipTools } from './classes.gziptools.js';
|
|
6
13
|
import { TarTools } from './classes.tartools.js';
|
|
7
14
|
import { ZipTools } from './classes.ziptools.js';
|
|
15
|
+
import { ArchiveAnalyzer, type IAnalyzedResult } from './classes.archiveanalyzer.js';
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Pending directory entry for async resolution
|
|
19
|
+
*/
|
|
20
|
+
interface IPendingDirectory {
|
|
21
|
+
sourcePath: string;
|
|
22
|
+
archiveBase?: string;
|
|
23
|
+
}
|
|
15
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Main class for archive manipulation with fluent API
|
|
27
|
+
* Supports TAR, ZIP, GZIP, and BZIP2 formats
|
|
28
|
+
*
|
|
29
|
+
* @example Extraction from URL
|
|
30
|
+
* ```typescript
|
|
31
|
+
* await SmartArchive.create()
|
|
32
|
+
* .url('https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz')
|
|
33
|
+
* .stripComponents(1)
|
|
34
|
+
* .extract('./node_modules/lodash');
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example Creation with thenable
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const archive = await SmartArchive.create()
|
|
40
|
+
* .format('tar.gz')
|
|
41
|
+
* .compression(9)
|
|
42
|
+
* .entry('config.json', JSON.stringify(config))
|
|
43
|
+
* .directory('./src');
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
16
46
|
export class SmartArchive {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
smartArchiveInstance.sourceUrl = urlArg;
|
|
21
|
-
return smartArchiveInstance;
|
|
22
|
-
}
|
|
47
|
+
// ============================================
|
|
48
|
+
// STATIC ENTRY POINT
|
|
49
|
+
// ============================================
|
|
23
50
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return smartArchiveInstance;
|
|
51
|
+
/**
|
|
52
|
+
* Create a new SmartArchive instance for fluent configuration
|
|
53
|
+
*/
|
|
54
|
+
public static create(): SmartArchive {
|
|
55
|
+
return new SmartArchive();
|
|
30
56
|
}
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
| plugins.stream.Duplex
|
|
36
|
-
| plugins.stream.Transform,
|
|
37
|
-
): Promise<SmartArchive> {
|
|
38
|
-
const smartArchiveInstance = new SmartArchive();
|
|
39
|
-
smartArchiveInstance.sourceStream = streamArg;
|
|
40
|
-
return smartArchiveInstance;
|
|
41
|
-
}
|
|
58
|
+
// ============================================
|
|
59
|
+
// TOOLS (public for internal use)
|
|
60
|
+
// ============================================
|
|
42
61
|
|
|
43
|
-
// INSTANCE
|
|
44
62
|
public tarTools = new TarTools();
|
|
45
63
|
public zipTools = new ZipTools();
|
|
46
64
|
public gzipTools = new GzipTools();
|
|
47
65
|
public bzip2Tools = new Bzip2Tools(this);
|
|
48
66
|
public archiveAnalyzer = new ArchiveAnalyzer(this);
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
// ============================================
|
|
69
|
+
// SOURCE STATE (extraction mode)
|
|
70
|
+
// ============================================
|
|
71
|
+
|
|
72
|
+
private sourceUrl?: string;
|
|
73
|
+
private sourceFilePath?: string;
|
|
74
|
+
private sourceStream?: plugins.stream.Readable | plugins.stream.Duplex | plugins.stream.Transform;
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// CREATION STATE
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
private archiveBuffer?: Buffer;
|
|
81
|
+
private creationFormat?: TArchiveFormat;
|
|
82
|
+
private _compressionLevel: TCompressionLevel = 6;
|
|
83
|
+
private pendingEntries: IArchiveEntry[] = [];
|
|
84
|
+
private pendingDirectories: IPendingDirectory[] = [];
|
|
56
85
|
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
// ============================================
|
|
87
|
+
// FLUENT STATE
|
|
88
|
+
// ============================================
|
|
59
89
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
private _mode: 'extract' | 'create' | null = null;
|
|
91
|
+
private _filters: TEntryFilter[] = [];
|
|
92
|
+
private _excludePatterns: RegExp[] = [];
|
|
93
|
+
private _includePatterns: RegExp[] = [];
|
|
94
|
+
private _stripComponents: number = 0;
|
|
95
|
+
private _overwrite: boolean = false;
|
|
96
|
+
private _fileName?: string;
|
|
66
97
|
|
|
67
98
|
constructor() {}
|
|
68
99
|
|
|
100
|
+
// ============================================
|
|
101
|
+
// SOURCE METHODS (set extraction mode)
|
|
102
|
+
// ============================================
|
|
103
|
+
|
|
69
104
|
/**
|
|
70
|
-
*
|
|
105
|
+
* Load archive from URL
|
|
71
106
|
*/
|
|
72
|
-
public
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
public url(urlArg: string): this {
|
|
108
|
+
this.ensureNotInCreateMode('url');
|
|
109
|
+
this._mode = 'extract';
|
|
110
|
+
this.sourceUrl = urlArg;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Load archive from file path
|
|
116
|
+
*/
|
|
117
|
+
public file(pathArg: string): this {
|
|
118
|
+
this.ensureNotInCreateMode('file');
|
|
119
|
+
this._mode = 'extract';
|
|
120
|
+
this.sourceFilePath = pathArg;
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load archive from readable stream
|
|
126
|
+
*/
|
|
127
|
+
public stream(streamArg: plugins.stream.Readable | plugins.stream.Duplex | plugins.stream.Transform): this {
|
|
128
|
+
this.ensureNotInCreateMode('stream');
|
|
129
|
+
this._mode = 'extract';
|
|
130
|
+
this.sourceStream = streamArg;
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load archive from buffer
|
|
136
|
+
*/
|
|
137
|
+
public buffer(bufferArg: Buffer): this {
|
|
138
|
+
this.ensureNotInCreateMode('buffer');
|
|
139
|
+
this._mode = 'extract';
|
|
140
|
+
this.sourceStream = plugins.stream.Readable.from(bufferArg);
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================
|
|
145
|
+
// FORMAT METHODS (set creation mode)
|
|
146
|
+
// ============================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Set output format for archive creation
|
|
150
|
+
*/
|
|
151
|
+
public format(fmt: TArchiveFormat): this {
|
|
152
|
+
this.ensureNotInExtractMode('format');
|
|
153
|
+
this._mode = 'create';
|
|
154
|
+
this.creationFormat = fmt;
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Set compression level (0-9)
|
|
160
|
+
*/
|
|
161
|
+
public compression(level: TCompressionLevel): this {
|
|
162
|
+
this._compressionLevel = level;
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================
|
|
167
|
+
// CONTENT METHODS (creation mode)
|
|
168
|
+
// ============================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add a single file entry to the archive
|
|
172
|
+
*/
|
|
173
|
+
public entry(archivePath: string, content: string | Buffer): this {
|
|
174
|
+
this.ensureNotInExtractMode('entry');
|
|
175
|
+
if (!this._mode) this._mode = 'create';
|
|
176
|
+
this.pendingEntries.push({ archivePath, content });
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Add multiple entries to the archive
|
|
182
|
+
*/
|
|
183
|
+
public entries(entriesArg: Array<{ archivePath: string; content: string | Buffer }>): this {
|
|
184
|
+
this.ensureNotInExtractMode('entries');
|
|
185
|
+
if (!this._mode) this._mode = 'create';
|
|
186
|
+
for (const e of entriesArg) {
|
|
187
|
+
this.pendingEntries.push({ archivePath: e.archivePath, content: e.content });
|
|
88
188
|
}
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Add an entire directory to the archive (queued, resolved at build time)
|
|
194
|
+
*/
|
|
195
|
+
public directory(sourcePath: string, archiveBase?: string): this {
|
|
196
|
+
this.ensureNotInExtractMode('directory');
|
|
197
|
+
if (!this._mode) this._mode = 'create';
|
|
198
|
+
this.pendingDirectories.push({ sourcePath, archiveBase });
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Add a SmartFile to the archive
|
|
204
|
+
*/
|
|
205
|
+
public addSmartFile(fileArg: plugins.smartfile.SmartFile, archivePath?: string): this {
|
|
206
|
+
this.ensureNotInExtractMode('addSmartFile');
|
|
207
|
+
if (!this._mode) this._mode = 'create';
|
|
208
|
+
this.pendingEntries.push({
|
|
209
|
+
archivePath: archivePath || fileArg.relative,
|
|
210
|
+
content: fileArg,
|
|
211
|
+
});
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Add a StreamFile to the archive
|
|
217
|
+
*/
|
|
218
|
+
public addStreamFile(fileArg: plugins.smartfile.StreamFile, archivePath?: string): this {
|
|
219
|
+
this.ensureNotInExtractMode('addStreamFile');
|
|
220
|
+
if (!this._mode) this._mode = 'create';
|
|
221
|
+
this.pendingEntries.push({
|
|
222
|
+
archivePath: archivePath || fileArg.relativeFilePath,
|
|
223
|
+
content: fileArg,
|
|
224
|
+
});
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================
|
|
229
|
+
// FILTER METHODS (both modes)
|
|
230
|
+
// ============================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Filter entries by predicate function
|
|
234
|
+
*/
|
|
235
|
+
public filter(predicate: TEntryFilter): this {
|
|
236
|
+
this._filters.push(predicate);
|
|
237
|
+
return this;
|
|
89
238
|
}
|
|
90
239
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Include only entries matching the pattern
|
|
242
|
+
*/
|
|
243
|
+
public include(pattern: string | RegExp): this {
|
|
244
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
245
|
+
this._includePatterns.push(regex);
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Exclude entries matching the pattern
|
|
251
|
+
*/
|
|
252
|
+
public exclude(pattern: string | RegExp): this {
|
|
253
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
254
|
+
this._excludePatterns.push(regex);
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================
|
|
259
|
+
// EXTRACTION OPTIONS
|
|
260
|
+
// ============================================
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Strip N leading path components from extracted files
|
|
264
|
+
*/
|
|
265
|
+
public stripComponents(n: number): this {
|
|
266
|
+
this._stripComponents = n;
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Overwrite existing files during extraction
|
|
272
|
+
*/
|
|
273
|
+
public overwrite(value: boolean = true): this {
|
|
274
|
+
this._overwrite = value;
|
|
275
|
+
return this;
|
|
96
276
|
}
|
|
97
277
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
):
|
|
278
|
+
/**
|
|
279
|
+
* Set output filename for single-file archives (gz, bz2)
|
|
280
|
+
*/
|
|
281
|
+
public fileName(name: string): this {
|
|
282
|
+
this._fileName = name;
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================
|
|
287
|
+
// TERMINAL METHODS - EXTRACTION
|
|
288
|
+
// ============================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Extract archive to filesystem directory
|
|
292
|
+
*/
|
|
293
|
+
public async extract(targetDir: string): Promise<void> {
|
|
294
|
+
this.ensureExtractionSource();
|
|
102
295
|
const done = plugins.smartpromise.defer<void>();
|
|
103
|
-
const streamFileStream = await this.
|
|
296
|
+
const streamFileStream = await this.toStreamFiles();
|
|
297
|
+
|
|
104
298
|
streamFileStream.pipe(
|
|
105
299
|
new plugins.smartstream.SmartDuplex({
|
|
106
300
|
objectMode: true,
|
|
107
|
-
writeFunction: async (
|
|
108
|
-
|
|
109
|
-
streamtools,
|
|
110
|
-
) => {
|
|
111
|
-
const done = plugins.smartpromise.defer<void>();
|
|
112
|
-
console.log(
|
|
113
|
-
streamFileArg.relativeFilePath
|
|
114
|
-
? streamFileArg.relativeFilePath
|
|
115
|
-
: 'no relative path',
|
|
116
|
-
);
|
|
301
|
+
writeFunction: async (streamFileArg: plugins.smartfile.StreamFile) => {
|
|
302
|
+
const innerDone = plugins.smartpromise.defer<void>();
|
|
117
303
|
const streamFile = streamFileArg;
|
|
304
|
+
let relativePath = streamFile.relativeFilePath || this._fileName || 'extracted_file';
|
|
305
|
+
|
|
306
|
+
// Apply stripComponents
|
|
307
|
+
if (this._stripComponents > 0) {
|
|
308
|
+
const parts = relativePath.split('/');
|
|
309
|
+
relativePath = parts.slice(this._stripComponents).join('/');
|
|
310
|
+
if (!relativePath) {
|
|
311
|
+
innerDone.resolve();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Apply filter
|
|
317
|
+
const filterFn = this.buildFilterFunction();
|
|
318
|
+
if (filterFn) {
|
|
319
|
+
const entryInfo: IArchiveEntryInfo = {
|
|
320
|
+
path: relativePath,
|
|
321
|
+
size: 0,
|
|
322
|
+
isDirectory: false,
|
|
323
|
+
isFile: true,
|
|
324
|
+
};
|
|
325
|
+
if (!filterFn(entryInfo)) {
|
|
326
|
+
innerDone.resolve();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
118
331
|
const readStream = await streamFile.createReadStream();
|
|
119
332
|
await plugins.fsPromises.mkdir(targetDir, { recursive: true });
|
|
120
|
-
const writePath = plugins.path.join(
|
|
121
|
-
targetDir,
|
|
122
|
-
streamFile.relativeFilePath || fileNameArg,
|
|
123
|
-
);
|
|
333
|
+
const writePath = plugins.path.join(targetDir, relativePath);
|
|
124
334
|
await plugins.fsPromises.mkdir(plugins.path.dirname(writePath), { recursive: true });
|
|
125
335
|
const writeStream = plugins.fs.createWriteStream(writePath);
|
|
126
336
|
readStream.pipe(writeStream);
|
|
127
337
|
writeStream.on('finish', () => {
|
|
128
|
-
|
|
338
|
+
innerDone.resolve();
|
|
129
339
|
});
|
|
130
|
-
await
|
|
340
|
+
await innerDone.promise;
|
|
131
341
|
},
|
|
132
342
|
finalFunction: async () => {
|
|
133
343
|
done.resolve();
|
|
134
344
|
},
|
|
135
|
-
})
|
|
345
|
+
})
|
|
136
346
|
);
|
|
347
|
+
|
|
137
348
|
return done.promise;
|
|
138
349
|
}
|
|
139
350
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Extract archive to a stream of StreamFile objects
|
|
353
|
+
*/
|
|
354
|
+
public async toStreamFiles(): Promise<plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>> {
|
|
355
|
+
this.ensureExtractionSource();
|
|
356
|
+
|
|
357
|
+
const streamFileIntake = new plugins.smartstream.StreamIntake<plugins.smartfile.StreamFile>({
|
|
358
|
+
objectMode: true,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Guard to prevent multiple signalEnd calls
|
|
362
|
+
let hasSignaledEnd = false;
|
|
363
|
+
const safeSignalEnd = () => {
|
|
364
|
+
if (!hasSignaledEnd) {
|
|
365
|
+
hasSignaledEnd = true;
|
|
366
|
+
streamFileIntake.signalEnd();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const archiveStream = await this.getSourceStream();
|
|
146
371
|
const createAnalyzedStream = () => this.archiveAnalyzer.getAnalyzedStream();
|
|
147
372
|
|
|
148
|
-
// lets create a function that can be called multiple times to unpack layers of archives
|
|
149
373
|
const createUnpackStream = () =>
|
|
150
|
-
plugins.smartstream.createTransformFunction<IAnalyzedResult,
|
|
374
|
+
plugins.smartstream.createTransformFunction<IAnalyzedResult, void>(
|
|
151
375
|
async (analyzedResultChunk) => {
|
|
152
376
|
if (analyzedResultChunk.fileType?.mime === 'application/x-tar') {
|
|
153
|
-
const tarStream =
|
|
154
|
-
|
|
377
|
+
const tarStream = analyzedResultChunk.decompressionStream as plugins.tarStream.Extract;
|
|
378
|
+
|
|
155
379
|
tarStream.on('entry', async (header, stream, next) => {
|
|
156
380
|
if (header.type === 'directory') {
|
|
157
|
-
|
|
158
|
-
`tar stream directory: ${header.name} ... skipping!`,
|
|
159
|
-
);
|
|
160
|
-
stream.resume(); // Consume directory stream
|
|
381
|
+
stream.resume();
|
|
161
382
|
stream.on('end', () => next());
|
|
162
383
|
return;
|
|
163
384
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Create a PassThrough stream to buffer the data
|
|
385
|
+
|
|
167
386
|
const passThrough = new plugins.stream.PassThrough();
|
|
168
|
-
const streamfile = plugins.smartfile.StreamFile.fromStream(
|
|
169
|
-
passThrough,
|
|
170
|
-
header.name,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
// Push the streamfile immediately
|
|
387
|
+
const streamfile = plugins.smartfile.StreamFile.fromStream(passThrough, header.name);
|
|
174
388
|
streamFileIntake.push(streamfile);
|
|
175
|
-
|
|
176
|
-
// Pipe the tar entry stream to the passthrough
|
|
177
389
|
stream.pipe(passThrough);
|
|
178
|
-
|
|
179
|
-
// Move to next entry when this one ends
|
|
180
390
|
stream.on('end', () => {
|
|
181
391
|
passThrough.end();
|
|
182
392
|
next();
|
|
183
393
|
});
|
|
184
394
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
streamFileIntake.signalEnd();
|
|
395
|
+
|
|
396
|
+
tarStream.on('finish', () => {
|
|
397
|
+
safeSignalEnd();
|
|
189
398
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
399
|
+
|
|
400
|
+
analyzedResultChunk.resultStream.pipe(analyzedResultChunk.decompressionStream);
|
|
193
401
|
} else if (analyzedResultChunk.fileType?.mime === 'application/zip') {
|
|
194
402
|
analyzedResultChunk.resultStream
|
|
195
403
|
.pipe(analyzedResultChunk.decompressionStream)
|
|
196
404
|
.pipe(
|
|
197
405
|
new plugins.smartstream.SmartDuplex({
|
|
198
406
|
objectMode: true,
|
|
199
|
-
writeFunction: async (
|
|
200
|
-
streamFileArg: plugins.smartfile.StreamFile,
|
|
201
|
-
streamtools,
|
|
202
|
-
) => {
|
|
407
|
+
writeFunction: async (streamFileArg: plugins.smartfile.StreamFile) => {
|
|
203
408
|
streamFileIntake.push(streamFileArg);
|
|
204
409
|
},
|
|
205
410
|
finalFunction: async () => {
|
|
206
|
-
|
|
411
|
+
safeSignalEnd();
|
|
207
412
|
},
|
|
208
|
-
})
|
|
413
|
+
})
|
|
209
414
|
);
|
|
210
|
-
} else if (
|
|
211
|
-
analyzedResultChunk.isArchive &&
|
|
212
|
-
analyzedResultChunk.decompressionStream
|
|
213
|
-
) {
|
|
415
|
+
} else if (analyzedResultChunk.isArchive && analyzedResultChunk.decompressionStream) {
|
|
214
416
|
// For nested archives (like gzip containing tar)
|
|
215
|
-
|
|
417
|
+
analyzedResultChunk.resultStream
|
|
216
418
|
.pipe(analyzedResultChunk.decompressionStream)
|
|
217
419
|
.pipe(createAnalyzedStream())
|
|
218
420
|
.pipe(createUnpackStream());
|
|
219
|
-
|
|
220
|
-
// Don't signal end here - let the nested unpacker handle it
|
|
221
421
|
} else {
|
|
222
422
|
const streamFile = plugins.smartfile.StreamFile.fromStream(
|
|
223
423
|
analyzedResultChunk.resultStream,
|
|
224
|
-
analyzedResultChunk.fileType?.ext
|
|
424
|
+
analyzedResultChunk.fileType?.ext
|
|
225
425
|
);
|
|
226
426
|
streamFileIntake.push(streamFile);
|
|
227
|
-
|
|
427
|
+
safeSignalEnd();
|
|
228
428
|
}
|
|
229
429
|
},
|
|
230
|
-
{
|
|
231
|
-
objectMode: true,
|
|
232
|
-
},
|
|
430
|
+
{ objectMode: true }
|
|
233
431
|
);
|
|
234
432
|
|
|
235
433
|
archiveStream.pipe(createAnalyzedStream()).pipe(createUnpackStream());
|
|
236
434
|
return streamFileIntake;
|
|
237
435
|
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Extract archive to an array of SmartFile objects (in-memory)
|
|
439
|
+
*/
|
|
440
|
+
public async toSmartFiles(): Promise<plugins.smartfile.SmartFile[]> {
|
|
441
|
+
this.ensureExtractionSource();
|
|
442
|
+
const streamFiles = await this.toStreamFiles();
|
|
443
|
+
const smartFiles: plugins.smartfile.SmartFile[] = [];
|
|
444
|
+
const filterFn = this.buildFilterFunction();
|
|
445
|
+
const pendingConversions: Promise<void>[] = [];
|
|
446
|
+
|
|
447
|
+
return new Promise((resolve, reject) => {
|
|
448
|
+
streamFiles.on('data', (streamFile: plugins.smartfile.StreamFile) => {
|
|
449
|
+
// Track all async conversions to ensure they complete before resolving
|
|
450
|
+
const conversion = (async () => {
|
|
451
|
+
try {
|
|
452
|
+
const smartFile = await streamFile.toSmartFile();
|
|
453
|
+
|
|
454
|
+
// Apply filter if configured
|
|
455
|
+
if (filterFn) {
|
|
456
|
+
const passes = filterFn({
|
|
457
|
+
path: smartFile.relative,
|
|
458
|
+
size: smartFile.contents.length,
|
|
459
|
+
isDirectory: false,
|
|
460
|
+
isFile: true,
|
|
461
|
+
});
|
|
462
|
+
if (!passes) return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
smartFiles.push(smartFile);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
reject(err);
|
|
468
|
+
}
|
|
469
|
+
})();
|
|
470
|
+
pendingConversions.push(conversion);
|
|
471
|
+
});
|
|
472
|
+
streamFiles.on('end', async () => {
|
|
473
|
+
// Wait for all conversions to complete before resolving
|
|
474
|
+
await Promise.all(pendingConversions);
|
|
475
|
+
resolve(smartFiles);
|
|
476
|
+
});
|
|
477
|
+
streamFiles.on('error', reject);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Extract a single file from the archive by path
|
|
483
|
+
*/
|
|
484
|
+
public async extractFile(filePath: string): Promise<plugins.smartfile.SmartFile | null> {
|
|
485
|
+
this.ensureExtractionSource();
|
|
486
|
+
const streamFiles = await this.toStreamFiles();
|
|
487
|
+
|
|
488
|
+
return new Promise((resolve, reject) => {
|
|
489
|
+
let found = false;
|
|
490
|
+
|
|
491
|
+
streamFiles.on('data', async (streamFile: plugins.smartfile.StreamFile) => {
|
|
492
|
+
if (streamFile.relativeFilePath === filePath || streamFile.relativeFilePath?.endsWith(filePath)) {
|
|
493
|
+
found = true;
|
|
494
|
+
try {
|
|
495
|
+
const smartFile = await streamFile.toSmartFile();
|
|
496
|
+
resolve(smartFile);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
reject(err);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
streamFiles.on('end', () => {
|
|
504
|
+
if (!found) {
|
|
505
|
+
resolve(null);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
streamFiles.on('error', reject);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ============================================
|
|
514
|
+
// TERMINAL METHODS - OUTPUT
|
|
515
|
+
// ============================================
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Build and finalize the archive, returning this instance
|
|
519
|
+
*/
|
|
520
|
+
public async build(): Promise<SmartArchive> {
|
|
521
|
+
await this.doBuild();
|
|
522
|
+
return this;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Internal build implementation (avoids thenable recursion)
|
|
527
|
+
*/
|
|
528
|
+
private async doBuild(): Promise<void> {
|
|
529
|
+
if (this._mode === 'extract') {
|
|
530
|
+
// For extraction mode, nothing to build
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (this.archiveBuffer) {
|
|
535
|
+
// Already built
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// For creation mode, build the archive buffer
|
|
540
|
+
this.ensureCreationFormat();
|
|
541
|
+
await this.resolveDirectories();
|
|
542
|
+
|
|
543
|
+
const entries = this.getFilteredEntries();
|
|
544
|
+
|
|
545
|
+
if (this.creationFormat === 'tar' || this.creationFormat === 'tar.gz' || this.creationFormat === 'tgz') {
|
|
546
|
+
if (this.creationFormat === 'tar') {
|
|
547
|
+
this.archiveBuffer = await this.tarTools.packFiles(entries);
|
|
548
|
+
} else {
|
|
549
|
+
this.archiveBuffer = await this.tarTools.packFilesToTarGz(entries, this._compressionLevel);
|
|
550
|
+
}
|
|
551
|
+
} else if (this.creationFormat === 'zip') {
|
|
552
|
+
this.archiveBuffer = await this.zipTools.createZip(entries, this._compressionLevel);
|
|
553
|
+
} else if (this.creationFormat === 'gz') {
|
|
554
|
+
if (entries.length !== 1) {
|
|
555
|
+
throw new Error('GZIP format only supports a single file');
|
|
556
|
+
}
|
|
557
|
+
let content: Buffer;
|
|
558
|
+
if (typeof entries[0].content === 'string') {
|
|
559
|
+
content = Buffer.from(entries[0].content);
|
|
560
|
+
} else if (Buffer.isBuffer(entries[0].content)) {
|
|
561
|
+
content = entries[0].content;
|
|
562
|
+
} else {
|
|
563
|
+
throw new Error('GZIP format requires string or Buffer content');
|
|
564
|
+
}
|
|
565
|
+
this.archiveBuffer = await this.gzipTools.compress(content, this._compressionLevel);
|
|
566
|
+
} else {
|
|
567
|
+
throw new Error(`Unsupported format: ${this.creationFormat}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Build archive and return as Buffer
|
|
573
|
+
*/
|
|
574
|
+
public async toBuffer(): Promise<Buffer> {
|
|
575
|
+
if (this._mode === 'create' && !this.archiveBuffer) {
|
|
576
|
+
await this.doBuild();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (this.archiveBuffer) {
|
|
580
|
+
return this.archiveBuffer;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// For extraction mode, get the source as buffer
|
|
584
|
+
const stream = await this.getSourceStream();
|
|
585
|
+
return this.streamToBuffer(stream);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Build archive and write to file
|
|
590
|
+
*/
|
|
591
|
+
public async toFile(filePath: string): Promise<void> {
|
|
592
|
+
const buffer = await this.toBuffer();
|
|
593
|
+
await plugins.fsPromises.mkdir(plugins.path.dirname(filePath), { recursive: true });
|
|
594
|
+
await plugins.fsPromises.writeFile(filePath, buffer);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Get archive as a readable stream
|
|
599
|
+
*/
|
|
600
|
+
public async toStream(): Promise<plugins.stream.Readable> {
|
|
601
|
+
if (this._mode === 'create' && !this.archiveBuffer) {
|
|
602
|
+
await this.doBuild();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (this.archiveBuffer) {
|
|
606
|
+
return plugins.stream.Readable.from(this.archiveBuffer);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return this.getSourceStream();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ============================================
|
|
613
|
+
// TERMINAL METHODS - ANALYSIS
|
|
614
|
+
// ============================================
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Analyze the archive and return metadata
|
|
618
|
+
*/
|
|
619
|
+
public async analyze(): Promise<IArchiveInfo> {
|
|
620
|
+
this.ensureExtractionSource();
|
|
621
|
+
const stream = await this.getSourceStream();
|
|
622
|
+
const firstChunk = await this.readFirstChunk(stream);
|
|
623
|
+
const fileType = await plugins.fileType.fileTypeFromBuffer(firstChunk);
|
|
624
|
+
|
|
625
|
+
let format: TArchiveFormat | null = null;
|
|
626
|
+
let isCompressed = false;
|
|
627
|
+
let isArchive = false;
|
|
628
|
+
|
|
629
|
+
if (fileType) {
|
|
630
|
+
switch (fileType.mime) {
|
|
631
|
+
case 'application/gzip':
|
|
632
|
+
format = 'gz';
|
|
633
|
+
isCompressed = true;
|
|
634
|
+
isArchive = true;
|
|
635
|
+
break;
|
|
636
|
+
case 'application/zip':
|
|
637
|
+
format = 'zip';
|
|
638
|
+
isCompressed = true;
|
|
639
|
+
isArchive = true;
|
|
640
|
+
break;
|
|
641
|
+
case 'application/x-tar':
|
|
642
|
+
format = 'tar';
|
|
643
|
+
isArchive = true;
|
|
644
|
+
break;
|
|
645
|
+
case 'application/x-bzip2':
|
|
646
|
+
format = 'bz2';
|
|
647
|
+
isCompressed = true;
|
|
648
|
+
isArchive = true;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
format,
|
|
655
|
+
isCompressed,
|
|
656
|
+
isArchive,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* List all entries in the archive
|
|
662
|
+
*/
|
|
663
|
+
public async list(): Promise<IArchiveEntryInfo[]> {
|
|
664
|
+
this.ensureExtractionSource();
|
|
665
|
+
const entries: IArchiveEntryInfo[] = [];
|
|
666
|
+
const streamFiles = await this.toStreamFiles();
|
|
667
|
+
|
|
668
|
+
return new Promise((resolve, reject) => {
|
|
669
|
+
streamFiles.on('data', (streamFile: plugins.smartfile.StreamFile) => {
|
|
670
|
+
entries.push({
|
|
671
|
+
path: streamFile.relativeFilePath || 'unknown',
|
|
672
|
+
size: 0, // Size not available without reading
|
|
673
|
+
isDirectory: false,
|
|
674
|
+
isFile: true,
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
streamFiles.on('end', () => resolve(entries));
|
|
678
|
+
streamFiles.on('error', reject);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Check if a specific file exists in the archive
|
|
684
|
+
*/
|
|
685
|
+
public async hasFile(filePath: string): Promise<boolean> {
|
|
686
|
+
this.ensureExtractionSource();
|
|
687
|
+
const entries = await this.list();
|
|
688
|
+
return entries.some((e) => e.path === filePath || e.path.endsWith(filePath));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
// ============================================
|
|
693
|
+
// PRIVATE HELPERS
|
|
694
|
+
// ============================================
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Ensure we're not in create mode when calling extraction methods
|
|
698
|
+
*/
|
|
699
|
+
private ensureNotInCreateMode(methodName: string): void {
|
|
700
|
+
if (this._mode === 'create') {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`Cannot call .${methodName}() in creation mode. ` +
|
|
703
|
+
`Use extraction methods (.url(), .file(), .stream(), .buffer()) for extraction mode.`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Ensure we're not in extract mode when calling creation methods
|
|
710
|
+
*/
|
|
711
|
+
private ensureNotInExtractMode(methodName: string): void {
|
|
712
|
+
if (this._mode === 'extract') {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`Cannot call .${methodName}() in extraction mode. ` +
|
|
715
|
+
`Use .format() for creation mode.`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Ensure an extraction source is configured
|
|
722
|
+
*/
|
|
723
|
+
private ensureExtractionSource(): void {
|
|
724
|
+
if (!this.sourceUrl && !this.sourceFilePath && !this.sourceStream && !this.archiveBuffer) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
'No source configured. Call .url(), .file(), .stream(), or .buffer() first.'
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Ensure a format is configured for creation
|
|
733
|
+
*/
|
|
734
|
+
private ensureCreationFormat(): void {
|
|
735
|
+
if (!this.creationFormat) {
|
|
736
|
+
throw new Error('No format specified. Call .format() before creating archive.');
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Get the source stream
|
|
742
|
+
*/
|
|
743
|
+
private async getSourceStream(): Promise<plugins.stream.Readable> {
|
|
744
|
+
if (this.archiveBuffer) {
|
|
745
|
+
return plugins.stream.Readable.from(this.archiveBuffer);
|
|
746
|
+
}
|
|
747
|
+
if (this.sourceStream) {
|
|
748
|
+
return this.sourceStream;
|
|
749
|
+
}
|
|
750
|
+
if (this.sourceUrl) {
|
|
751
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
752
|
+
.url(this.sourceUrl)
|
|
753
|
+
.get();
|
|
754
|
+
const webStream = response.stream();
|
|
755
|
+
return plugins.stream.Readable.fromWeb(webStream as any);
|
|
756
|
+
}
|
|
757
|
+
if (this.sourceFilePath) {
|
|
758
|
+
return plugins.fs.createReadStream(this.sourceFilePath);
|
|
759
|
+
}
|
|
760
|
+
throw new Error('No archive source configured');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Build a combined filter function from all configured filters
|
|
765
|
+
*/
|
|
766
|
+
private buildFilterFunction(): TEntryFilter | undefined {
|
|
767
|
+
const hasFilters =
|
|
768
|
+
this._filters.length > 0 ||
|
|
769
|
+
this._includePatterns.length > 0 ||
|
|
770
|
+
this._excludePatterns.length > 0;
|
|
771
|
+
|
|
772
|
+
if (!hasFilters) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return (entry: IArchiveEntryInfo) => {
|
|
777
|
+
// Check include patterns (if any specified, at least one must match)
|
|
778
|
+
if (this._includePatterns.length > 0) {
|
|
779
|
+
const included = this._includePatterns.some((p) => p.test(entry.path));
|
|
780
|
+
if (!included) return false;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Check exclude patterns (none must match)
|
|
784
|
+
for (const pattern of this._excludePatterns) {
|
|
785
|
+
if (pattern.test(entry.path)) return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Check custom filters (all must pass)
|
|
789
|
+
for (const filter of this._filters) {
|
|
790
|
+
if (!filter(entry)) return false;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return true;
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Resolve pending directories to entries
|
|
799
|
+
*/
|
|
800
|
+
private async resolveDirectories(): Promise<void> {
|
|
801
|
+
for (const dir of this.pendingDirectories) {
|
|
802
|
+
const files = await plugins.listFileTree(dir.sourcePath, '**/*');
|
|
803
|
+
for (const filePath of files) {
|
|
804
|
+
const archivePath = dir.archiveBase
|
|
805
|
+
? plugins.path.join(dir.archiveBase, filePath)
|
|
806
|
+
: filePath;
|
|
807
|
+
const absolutePath = plugins.path.join(dir.sourcePath, filePath);
|
|
808
|
+
const content = await plugins.fsPromises.readFile(absolutePath);
|
|
809
|
+
this.pendingEntries.push({
|
|
810
|
+
archivePath,
|
|
811
|
+
content,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
this.pendingDirectories = [];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Get entries filtered by include/exclude patterns
|
|
820
|
+
*/
|
|
821
|
+
private getFilteredEntries(): IArchiveEntry[] {
|
|
822
|
+
const filterFn = this.buildFilterFunction();
|
|
823
|
+
if (!filterFn) {
|
|
824
|
+
return this.pendingEntries;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return this.pendingEntries.filter((entry) =>
|
|
828
|
+
filterFn({
|
|
829
|
+
path: entry.archivePath,
|
|
830
|
+
size: 0,
|
|
831
|
+
isDirectory: false,
|
|
832
|
+
isFile: true,
|
|
833
|
+
})
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Convert a stream to buffer
|
|
839
|
+
*/
|
|
840
|
+
private async streamToBuffer(stream: plugins.stream.Readable): Promise<Buffer> {
|
|
841
|
+
const chunks: Buffer[] = [];
|
|
842
|
+
return new Promise((resolve, reject) => {
|
|
843
|
+
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
844
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
845
|
+
stream.on('error', reject);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Read first chunk from stream
|
|
851
|
+
*/
|
|
852
|
+
private async readFirstChunk(stream: plugins.stream.Readable): Promise<Buffer> {
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
const onData = (chunk: Buffer) => {
|
|
855
|
+
stream.removeListener('data', onData);
|
|
856
|
+
stream.removeListener('error', reject);
|
|
857
|
+
resolve(chunk);
|
|
858
|
+
};
|
|
859
|
+
stream.on('data', onData);
|
|
860
|
+
stream.on('error', reject);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
238
863
|
}
|