stegdoc 5.1.0 → 5.3.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/package.json +1 -1
- package/src/commands/encode.js +57 -30
- package/src/index.js +1 -1
- package/src/lib/compression.js +14 -5
package/package.json
CHANGED
package/src/commands/encode.js
CHANGED
|
@@ -178,16 +178,52 @@ async function encodeCommand(inputFile, options) {
|
|
|
178
178
|
// Generate session salt for encryption
|
|
179
179
|
const sessionSalt = useEncryption ? generateSalt() : null;
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
// === Phase 1: Compress and collect all chunks ===
|
|
182
|
+
spinner.text = useCompression ? 'Compressing...' : 'Reading...';
|
|
182
183
|
|
|
183
184
|
const partFiles = [];
|
|
184
|
-
|
|
185
|
-
// v5 pipeline: compress (brotli) → collect binary chunks → encrypt per-part → embed in log lines
|
|
186
185
|
const binaryChunkSize = chunkSizeBytes === Infinity ? Infinity : Math.floor(chunkSizeBytes * 3 / 4);
|
|
187
186
|
|
|
188
|
-
|
|
187
|
+
// Collect all chunks first (enables parallel part creation + known total)
|
|
188
|
+
const collectedChunks = [];
|
|
189
|
+
const collectChunk = async (binaryBuffer, index) => {
|
|
190
|
+
collectedChunks.push({ buffer: Buffer.from(binaryBuffer), index });
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const collector = new BinaryChunkCollector(binaryChunkSize, collectChunk);
|
|
194
|
+
|
|
195
|
+
const streams = [fs.createReadStream(streamSource)];
|
|
196
|
+
|
|
197
|
+
// Add progress tracking (tracks input bytes read)
|
|
198
|
+
if (!quiet) {
|
|
199
|
+
const progressStream = new ProgressTransform(fileSize, (processed, total) => {
|
|
200
|
+
const pct = Math.min(100, Math.round((processed / total) * 100));
|
|
201
|
+
const processedFmt = formatBytes(processed);
|
|
202
|
+
const totalFmt = formatBytes(total);
|
|
203
|
+
const phase = useCompression ? 'Compressing' : 'Reading';
|
|
204
|
+
spinner.text = `${phase}... ${processedFmt} / ${totalFmt} (${pct}%)`;
|
|
205
|
+
});
|
|
206
|
+
streams.push(progressStream);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (useCompression) {
|
|
210
|
+
streams.push(createBrotliCompressStream());
|
|
211
|
+
}
|
|
212
|
+
streams.push(collector);
|
|
213
|
+
|
|
214
|
+
await pipeline(...streams);
|
|
215
|
+
|
|
216
|
+
const totalParts = collectedChunks.length;
|
|
217
|
+
spinner.succeed && spinner.succeed(`${useCompression ? 'Compressed' : 'Read'}: ${totalParts} part${totalParts !== 1 ? 's' : ''} to create`);
|
|
218
|
+
|
|
219
|
+
// === Phase 2: Create parts in parallel ===
|
|
220
|
+
const os = require('os');
|
|
221
|
+
const concurrency = Math.min(totalParts, Math.max(1, os.cpus().length));
|
|
222
|
+
let completedParts = 0;
|
|
223
|
+
const partSpinner = quiet ? spinner : ora(`Creating parts... 0/${totalParts}`).start();
|
|
224
|
+
|
|
225
|
+
const createPart = async ({ buffer: binaryBuffer, index }) => {
|
|
189
226
|
const partNumber = index + 1;
|
|
190
|
-
const partSpinner = quiet ? spinner : ora(`Creating part ${partNumber}...`).start();
|
|
191
227
|
|
|
192
228
|
let payloadBuffer;
|
|
193
229
|
let encryptionMeta = '';
|
|
@@ -208,7 +244,7 @@ async function encodeCommand(inputFile, options) {
|
|
|
208
244
|
originalExtension: extension,
|
|
209
245
|
hash,
|
|
210
246
|
partNumber,
|
|
211
|
-
totalParts
|
|
247
|
+
totalParts,
|
|
212
248
|
originalSize: fileSize,
|
|
213
249
|
format,
|
|
214
250
|
encrypted: useEncryption,
|
|
@@ -262,35 +298,26 @@ async function encodeCommand(inputFile, options) {
|
|
|
262
298
|
|
|
263
299
|
partFiles.push(outputPath);
|
|
264
300
|
createdFiles.push(outputPath);
|
|
265
|
-
partSpinner.succeed && partSpinner.succeed(`Created: ${outputFilename} (${formatBytes(payloadBuffer.length)} payload, ${dataLineCount} data lines)`);
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const collector = new BinaryChunkCollector(binaryChunkSize, onBinaryChunkReady);
|
|
269
301
|
|
|
270
|
-
|
|
302
|
+
completedParts++;
|
|
303
|
+
const pct = Math.round((completedParts / totalParts) * 100);
|
|
304
|
+
partSpinner.text = `Creating parts... ${completedParts}/${totalParts} (${pct}%)`;
|
|
305
|
+
};
|
|
271
306
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const pct = Math.min(100, Math.round((processed / total) * 100));
|
|
276
|
-
const processedFmt = formatBytes(processed);
|
|
277
|
-
const totalFmt = formatBytes(total);
|
|
278
|
-
const phase = useCompression ? 'Compressing' : 'Processing';
|
|
279
|
-
spinner.text = `${phase}... ${processedFmt} / ${totalFmt} (${pct}%)`;
|
|
280
|
-
});
|
|
281
|
-
streams.push(progressStream);
|
|
282
|
-
}
|
|
307
|
+
// Run part creation in parallel with limited concurrency
|
|
308
|
+
const chunks = [...collectedChunks];
|
|
309
|
+
const executing = new Set();
|
|
283
310
|
|
|
284
|
-
|
|
285
|
-
|
|
311
|
+
for (const chunk of chunks) {
|
|
312
|
+
const promise = createPart(chunk).then(() => executing.delete(promise));
|
|
313
|
+
executing.add(promise);
|
|
314
|
+
if (executing.size >= concurrency) {
|
|
315
|
+
await Promise.race(executing);
|
|
316
|
+
}
|
|
286
317
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
await pipeline(...streams);
|
|
290
|
-
|
|
291
|
-
const totalParts = partFiles.length;
|
|
318
|
+
await Promise.all(executing);
|
|
292
319
|
|
|
293
|
-
|
|
320
|
+
partSpinner.succeed && partSpinner.succeed(`Created ${totalParts} part${totalParts !== 1 ? 's' : ''}`);
|
|
294
321
|
|
|
295
322
|
if (!quiet) {
|
|
296
323
|
console.log();
|
package/src/index.js
CHANGED
package/src/lib/compression.js
CHANGED
|
@@ -3,15 +3,24 @@ const zlib = require('zlib');
|
|
|
3
3
|
// ─── Brotli Compression (v5+) ──────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Default Brotli quality level.
|
|
7
|
+
* Quality 6 offers ~10x faster compression than max (11) with only
|
|
8
|
+
* marginally larger output — a good trade-off for large files.
|
|
9
|
+
*/
|
|
10
|
+
const BROTLI_DEFAULT_QUALITY = 6;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Compress data using Brotli (used in v5+)
|
|
7
14
|
* @param {Buffer} buffer - Data to compress
|
|
15
|
+
* @param {number} [quality] - Compression quality (0-11, default 6)
|
|
8
16
|
* @returns {Promise<Buffer>} Compressed data
|
|
9
17
|
*/
|
|
10
|
-
function compressBrotli(buffer) {
|
|
18
|
+
function compressBrotli(buffer, quality) {
|
|
19
|
+
const q = quality !== undefined ? quality : BROTLI_DEFAULT_QUALITY;
|
|
11
20
|
return new Promise((resolve, reject) => {
|
|
12
21
|
zlib.brotliCompress(buffer, {
|
|
13
22
|
params: {
|
|
14
|
-
[zlib.constants.BROTLI_PARAM_QUALITY]:
|
|
23
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: q,
|
|
15
24
|
},
|
|
16
25
|
}, (err, result) => {
|
|
17
26
|
if (err) reject(err);
|
|
@@ -36,11 +45,11 @@ function decompressBrotli(buffer) {
|
|
|
36
45
|
|
|
37
46
|
/**
|
|
38
47
|
* Create a streaming Brotli compression transform
|
|
39
|
-
* @param {number} [quality] - Compression quality (0-11, default
|
|
48
|
+
* @param {number} [quality] - Compression quality (0-11, default 6)
|
|
40
49
|
* @returns {zlib.BrotliCompress} Brotli transform stream
|
|
41
50
|
*/
|
|
42
51
|
function createBrotliCompressStream(quality) {
|
|
43
|
-
const q = quality !== undefined ? quality :
|
|
52
|
+
const q = quality !== undefined ? quality : BROTLI_DEFAULT_QUALITY;
|
|
44
53
|
return zlib.createBrotliCompress({
|
|
45
54
|
params: {
|
|
46
55
|
[zlib.constants.BROTLI_PARAM_QUALITY]: q,
|