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