@push.rocks/smartarchive 4.2.4 → 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.
- 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 +39 -6
- package/dist_ts/classes.gziptools.js +85 -24
- package/dist_ts/classes.smartarchive.d.ts +101 -16
- package/dist_ts/classes.smartarchive.js +395 -58
- 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 +142 -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 +115 -0
- package/dist_ts/interfaces.js +2 -0
- package/package.json +1 -1
- 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 +96 -33
- package/ts/classes.smartarchive.ts +469 -118
- package/ts/classes.tartools.ts +124 -52
- package/ts/classes.ziptools.ts +160 -34
- package/ts/errors.ts +70 -0
- package/ts/index.ts +11 -0
- package/ts/interfaces.ts +131 -0
|
@@ -1,75 +1,267 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
//
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
51
|
-
public sourceFilePath
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
*
|
|
259
|
+
* Get the original archive stream
|
|
71
260
|
*/
|
|
72
|
-
public async
|
|
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,161 +270,320 @@ export class SmartArchive {
|
|
|
78
270
|
.url(this.sourceUrl)
|
|
79
271
|
.get();
|
|
80
272
|
const webStream = response.stream();
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Extract archive to filesystem
|
|
303
|
+
*/
|
|
304
|
+
public async extractToDirectory(
|
|
99
305
|
targetDir: string,
|
|
100
|
-
|
|
306
|
+
options?: Partial<IArchiveExtractionOptions>
|
|
101
307
|
): Promise<void> {
|
|
102
308
|
const done = plugins.smartpromise.defer<void>();
|
|
103
|
-
const streamFileStream = await this.
|
|
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
|
-
|
|
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
344
|
await plugins.fsPromises.mkdir(targetDir, { recursive: true });
|
|
120
|
-
const writePath = plugins.path.join(
|
|
121
|
-
targetDir,
|
|
122
|
-
streamFile.relativeFilePath || fileNameArg,
|
|
123
|
-
);
|
|
345
|
+
const writePath = plugins.path.join(targetDir, relativePath);
|
|
124
346
|
await plugins.fsPromises.mkdir(plugins.path.dirname(writePath), { recursive: true });
|
|
125
347
|
const writeStream = plugins.fs.createWriteStream(writePath);
|
|
126
348
|
readStream.pipe(writeStream);
|
|
127
349
|
writeStream.on('finish', () => {
|
|
128
|
-
|
|
350
|
+
innerDone.resolve();
|
|
129
351
|
});
|
|
130
|
-
await
|
|
352
|
+
await innerDone.promise;
|
|
131
353
|
},
|
|
132
354
|
finalFunction: async () => {
|
|
133
355
|
done.resolve();
|
|
134
356
|
},
|
|
135
|
-
})
|
|
357
|
+
})
|
|
136
358
|
);
|
|
359
|
+
|
|
137
360
|
return done.promise;
|
|
138
361
|
}
|
|
139
362
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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();
|
|
146
381
|
const createAnalyzedStream = () => this.archiveAnalyzer.getAnalyzedStream();
|
|
147
382
|
|
|
148
|
-
// lets create a function that can be called multiple times to unpack layers of archives
|
|
149
383
|
const createUnpackStream = () =>
|
|
150
|
-
plugins.smartstream.createTransformFunction<IAnalyzedResult,
|
|
384
|
+
plugins.smartstream.createTransformFunction<IAnalyzedResult, void>(
|
|
151
385
|
async (analyzedResultChunk) => {
|
|
152
386
|
if (analyzedResultChunk.fileType?.mime === 'application/x-tar') {
|
|
153
|
-
const tarStream =
|
|
154
|
-
|
|
387
|
+
const tarStream = analyzedResultChunk.decompressionStream as plugins.tarStream.Extract;
|
|
388
|
+
|
|
155
389
|
tarStream.on('entry', async (header, stream, next) => {
|
|
156
390
|
if (header.type === 'directory') {
|
|
157
|
-
|
|
158
|
-
`tar stream directory: ${header.name} ... skipping!`,
|
|
159
|
-
);
|
|
160
|
-
stream.resume(); // Consume directory stream
|
|
391
|
+
stream.resume();
|
|
161
392
|
stream.on('end', () => next());
|
|
162
393
|
return;
|
|
163
394
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Create a PassThrough stream to buffer the data
|
|
395
|
+
|
|
167
396
|
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
|
|
397
|
+
const streamfile = plugins.smartfile.StreamFile.fromStream(passThrough, header.name);
|
|
174
398
|
streamFileIntake.push(streamfile);
|
|
175
|
-
|
|
176
|
-
// Pipe the tar entry stream to the passthrough
|
|
177
399
|
stream.pipe(passThrough);
|
|
178
|
-
|
|
179
|
-
// Move to next entry when this one ends
|
|
180
400
|
stream.on('end', () => {
|
|
181
401
|
passThrough.end();
|
|
182
402
|
next();
|
|
183
403
|
});
|
|
184
404
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
streamFileIntake.signalEnd();
|
|
405
|
+
|
|
406
|
+
tarStream.on('finish', () => {
|
|
407
|
+
safeSignalEnd();
|
|
189
408
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
409
|
+
|
|
410
|
+
analyzedResultChunk.resultStream.pipe(analyzedResultChunk.decompressionStream);
|
|
193
411
|
} else if (analyzedResultChunk.fileType?.mime === 'application/zip') {
|
|
194
412
|
analyzedResultChunk.resultStream
|
|
195
413
|
.pipe(analyzedResultChunk.decompressionStream)
|
|
196
414
|
.pipe(
|
|
197
415
|
new plugins.smartstream.SmartDuplex({
|
|
198
416
|
objectMode: true,
|
|
199
|
-
writeFunction: async (
|
|
200
|
-
streamFileArg: plugins.smartfile.StreamFile,
|
|
201
|
-
streamtools,
|
|
202
|
-
) => {
|
|
417
|
+
writeFunction: async (streamFileArg: plugins.smartfile.StreamFile) => {
|
|
203
418
|
streamFileIntake.push(streamFileArg);
|
|
204
419
|
},
|
|
205
420
|
finalFunction: async () => {
|
|
206
|
-
|
|
421
|
+
safeSignalEnd();
|
|
207
422
|
},
|
|
208
|
-
})
|
|
423
|
+
})
|
|
209
424
|
);
|
|
210
|
-
} else if (
|
|
211
|
-
analyzedResultChunk.isArchive &&
|
|
212
|
-
analyzedResultChunk.decompressionStream
|
|
213
|
-
) {
|
|
425
|
+
} else if (analyzedResultChunk.isArchive && analyzedResultChunk.decompressionStream) {
|
|
214
426
|
// For nested archives (like gzip containing tar)
|
|
215
|
-
|
|
427
|
+
analyzedResultChunk.resultStream
|
|
216
428
|
.pipe(analyzedResultChunk.decompressionStream)
|
|
217
429
|
.pipe(createAnalyzedStream())
|
|
218
430
|
.pipe(createUnpackStream());
|
|
219
|
-
|
|
220
|
-
// Don't signal end here - let the nested unpacker handle it
|
|
221
431
|
} else {
|
|
222
432
|
const streamFile = plugins.smartfile.StreamFile.fromStream(
|
|
223
433
|
analyzedResultChunk.resultStream,
|
|
224
|
-
analyzedResultChunk.fileType?.ext
|
|
434
|
+
analyzedResultChunk.fileType?.ext
|
|
225
435
|
);
|
|
226
436
|
streamFileIntake.push(streamFile);
|
|
227
|
-
|
|
437
|
+
safeSignalEnd();
|
|
228
438
|
}
|
|
229
439
|
},
|
|
230
|
-
{
|
|
231
|
-
objectMode: true,
|
|
232
|
-
},
|
|
440
|
+
{ objectMode: true }
|
|
233
441
|
);
|
|
234
442
|
|
|
235
443
|
archiveStream.pipe(createAnalyzedStream()).pipe(createUnpackStream());
|
|
236
444
|
return streamFileIntake;
|
|
237
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
|
+
}
|
|
238
589
|
}
|