@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.
- 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 +398 -61
- package/dist_ts/classes.tartools.d.ts +31 -4
- package/dist_ts/classes.tartools.js +97 -36
- 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/dist_ts/plugins.d.ts +9 -3
- package/dist_ts/plugins.js +27 -4
- package/package.json +3 -4
- package/readme.hints.md +38 -1
- package/readme.md +248 -137
- 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 +472 -122
- package/ts/classes.tartools.ts +128 -59
- 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
- package/ts/plugins.ts +29 -3
|
@@ -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,162 +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
|
-
await plugins.
|
|
120
|
-
const writePath = plugins.path.join(
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
350
|
+
innerDone.resolve();
|
|
130
351
|
});
|
|
131
|
-
await
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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,
|
|
384
|
+
plugins.smartstream.createTransformFunction<IAnalyzedResult, void>(
|
|
152
385
|
async (analyzedResultChunk) => {
|
|
153
386
|
if (analyzedResultChunk.fileType?.mime === 'application/x-tar') {
|
|
154
|
-
const tarStream =
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
streamFileIntake.signalEnd();
|
|
405
|
+
|
|
406
|
+
tarStream.on('finish', () => {
|
|
407
|
+
safeSignalEnd();
|
|
190
408
|
});
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|