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