stegdoc 1.0.1 → 4.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/package.json +6 -7
- package/src/commands/decode.js +233 -91
- package/src/commands/encode.js +302 -199
- package/src/commands/info.js +10 -9
- package/src/commands/verify.js +65 -30
- package/src/index.js +2 -2
- package/src/lib/compression.js +18 -0
- package/src/lib/crypto.js +54 -0
- package/src/lib/docx-handler.js +1 -1
- package/src/lib/metadata.js +13 -2
- package/src/lib/streams.js +197 -0
- package/src/lib/utils.js +2 -2
- package/src/lib/xlsx-handler.js +133 -76
- package/bootstrap.js +0 -33
package/src/commands/verify.js
CHANGED
|
@@ -3,9 +3,9 @@ const chalk = require('chalk');
|
|
|
3
3
|
const ora = require('ora');
|
|
4
4
|
const { readDocxBase64 } = require('../lib/docx-handler');
|
|
5
5
|
const { readXlsxBase64 } = require('../lib/xlsx-handler');
|
|
6
|
-
const { validateMetadata, isMultiPart } = require('../lib/metadata');
|
|
6
|
+
const { validateMetadata, isMultiPart, isStreamingFormat } = require('../lib/metadata');
|
|
7
7
|
const { detectFormat, formatBytes } = require('../lib/utils');
|
|
8
|
-
const { decrypt, unpackEncryptionMeta } = require('../lib/crypto');
|
|
8
|
+
const { decrypt, unpackEncryptionMeta, createDecryptStream } = require('../lib/crypto');
|
|
9
9
|
const { extractContent, findMultiPartFiles } = require('../lib/file-utils');
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -47,6 +47,7 @@ async function verifyCommand(inputFile, options) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const isEncrypted = metadata.encrypted || (encryptionMeta && encryptionMeta.length > 0);
|
|
50
|
+
const isV4 = isStreamingFormat(metadata);
|
|
50
51
|
|
|
51
52
|
// Check multi-part
|
|
52
53
|
if (isMultiPart(metadata)) {
|
|
@@ -81,35 +82,13 @@ async function verifyCommand(inputFile, options) {
|
|
|
81
82
|
spinner.text = 'Verifying decryption...';
|
|
82
83
|
|
|
83
84
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (isMultiPart(metadata)) {
|
|
91
|
-
const inputDir = path.dirname(inputFile);
|
|
92
|
-
const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
|
|
93
|
-
|
|
94
|
-
if (allParts.length === metadata.totalParts) {
|
|
95
|
-
// Read and merge all parts
|
|
96
|
-
const contentParts = [];
|
|
97
|
-
for (const part of allParts) {
|
|
98
|
-
let partResult;
|
|
99
|
-
if (format === 'xlsx') {
|
|
100
|
-
partResult = await readXlsxBase64(part.path);
|
|
101
|
-
} else {
|
|
102
|
-
partResult = await readDocxBase64(part.path);
|
|
103
|
-
}
|
|
104
|
-
const { encryptedContent: partContent } = extractContent(partResult, format);
|
|
105
|
-
contentParts.push(partContent);
|
|
106
|
-
}
|
|
107
|
-
fullContent = contentParts.join('');
|
|
108
|
-
}
|
|
85
|
+
if (isV4) {
|
|
86
|
+
// v4: per-part encryption — verify each part independently
|
|
87
|
+
await verifyV4Encryption(inputFile, format, metadata, encryptionMeta, options.password, spinner);
|
|
88
|
+
} else {
|
|
89
|
+
// v3: shared encryption — merge all parts then decrypt
|
|
90
|
+
await verifyV3Encryption(inputFile, format, metadata, encryptedContent, encryptionMeta, options.password, spinner);
|
|
109
91
|
}
|
|
110
|
-
|
|
111
|
-
// Decrypt the full content to verify password
|
|
112
|
-
decrypt(fullContent, options.password, iv, salt, authTag);
|
|
113
92
|
spinner.succeed('Decryption password valid');
|
|
114
93
|
} catch (e) {
|
|
115
94
|
issues.push('Decryption failed - wrong password or corrupted data');
|
|
@@ -127,6 +106,7 @@ async function verifyCommand(inputFile, options) {
|
|
|
127
106
|
console.log(chalk.cyan(` Size: ${formatBytes(metadata.originalSize)}`));
|
|
128
107
|
console.log(chalk.cyan(` Encrypted: ${isEncrypted ? 'Yes' : 'No'}`));
|
|
129
108
|
console.log(chalk.cyan(` Compressed: ${metadata.compressed ? 'Yes' : 'No'}`));
|
|
109
|
+
console.log(chalk.cyan(` Format version: ${isV4 ? 'v4 (streaming)' : 'v3 (legacy)'}`));
|
|
130
110
|
|
|
131
111
|
if (isMultiPart(metadata)) {
|
|
132
112
|
console.log(chalk.cyan(` Parts: ${metadata.totalParts}`));
|
|
@@ -166,4 +146,59 @@ async function verifyCommand(inputFile, options) {
|
|
|
166
146
|
}
|
|
167
147
|
}
|
|
168
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Verify v4 per-part encryption by decrypting the first part
|
|
151
|
+
*/
|
|
152
|
+
async function verifyV4Encryption(inputFile, format, metadata, encryptionMeta, password, spinner) {
|
|
153
|
+
// For v4, each part has its own encryption metadata
|
|
154
|
+
// Verify by decrypting the first part's content
|
|
155
|
+
const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
|
|
156
|
+
|
|
157
|
+
// Read the first part's content
|
|
158
|
+
let readResult;
|
|
159
|
+
if (format === 'xlsx') {
|
|
160
|
+
readResult = await readXlsxBase64(inputFile);
|
|
161
|
+
} else {
|
|
162
|
+
readResult = await readDocxBase64(inputFile);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { encryptedContent } = extractContent(readResult, format);
|
|
166
|
+
const binaryData = Buffer.from(encryptedContent, 'base64');
|
|
167
|
+
|
|
168
|
+
const decipher = createDecryptStream(password, iv, salt, authTag);
|
|
169
|
+
decipher.update(binaryData);
|
|
170
|
+
decipher.final(); // Throws on wrong password
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Verify v3 shared encryption by merging all parts and decrypting
|
|
175
|
+
*/
|
|
176
|
+
async function verifyV3Encryption(inputFile, format, metadata, encryptedContent, encryptionMeta, password, spinner) {
|
|
177
|
+
const { iv, salt, authTag } = unpackEncryptionMeta(encryptionMeta);
|
|
178
|
+
|
|
179
|
+
let fullContent = encryptedContent;
|
|
180
|
+
|
|
181
|
+
if (isMultiPart(metadata)) {
|
|
182
|
+
const inputDir = path.dirname(inputFile);
|
|
183
|
+
const allParts = findMultiPartFiles(inputDir, metadata.hash, format);
|
|
184
|
+
|
|
185
|
+
if (allParts.length === metadata.totalParts) {
|
|
186
|
+
const contentParts = [];
|
|
187
|
+
for (const part of allParts) {
|
|
188
|
+
let partResult;
|
|
189
|
+
if (format === 'xlsx') {
|
|
190
|
+
partResult = await readXlsxBase64(part.path);
|
|
191
|
+
} else {
|
|
192
|
+
partResult = await readDocxBase64(part.path);
|
|
193
|
+
}
|
|
194
|
+
const { encryptedContent: partContent } = extractContent(partResult, format);
|
|
195
|
+
contentParts.push(partContent);
|
|
196
|
+
}
|
|
197
|
+
fullContent = contentParts.join('');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
decrypt(fullContent, password, iv, salt, authTag);
|
|
202
|
+
}
|
|
203
|
+
|
|
169
204
|
module.exports = verifyCommand;
|
package/src/index.js
CHANGED
|
@@ -10,8 +10,8 @@ const verifyCommand = require('./commands/verify');
|
|
|
10
10
|
// CLI Configuration
|
|
11
11
|
program
|
|
12
12
|
.name('stegdoc')
|
|
13
|
-
.description('
|
|
14
|
-
.version('
|
|
13
|
+
.description('CLI tool to encode files into Office documents with AES-256 encryption')
|
|
14
|
+
.version('3.0.2');
|
|
15
15
|
|
|
16
16
|
// Encode command
|
|
17
17
|
program
|
package/src/lib/compression.js
CHANGED
|
@@ -89,9 +89,27 @@ function decompress(buffer) {
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Create a streaming gzip compression transform
|
|
94
|
+
* @returns {zlib.Gzip} Gzip transform stream
|
|
95
|
+
*/
|
|
96
|
+
function createCompressStream() {
|
|
97
|
+
return zlib.createGzip({ level: 9 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a streaming gunzip decompression transform
|
|
102
|
+
* @returns {zlib.Gunzip} Gunzip transform stream
|
|
103
|
+
*/
|
|
104
|
+
function createDecompressStream() {
|
|
105
|
+
return zlib.createGunzip();
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
module.exports = {
|
|
93
109
|
isCompressedMime,
|
|
94
110
|
compress,
|
|
95
111
|
decompress,
|
|
112
|
+
createCompressStream,
|
|
113
|
+
createDecompressStream,
|
|
96
114
|
COMPRESSED_MIMES,
|
|
97
115
|
};
|
package/src/lib/crypto.js
CHANGED
|
@@ -109,10 +109,64 @@ function unpackEncryptionMeta(packed) {
|
|
|
109
109
|
return { iv, salt, authTag };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Generate a random salt for key derivation (one per encode session)
|
|
114
|
+
* @returns {Buffer} Random salt
|
|
115
|
+
*/
|
|
116
|
+
function generateSalt() {
|
|
117
|
+
return crypto.randomBytes(SALT_LENGTH);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a streaming encrypt cipher for per-part encryption.
|
|
122
|
+
* Each call generates a unique IV. The salt should be shared across parts.
|
|
123
|
+
* Call getAuthTag() AFTER the cipher stream has ended (after .final()).
|
|
124
|
+
* @param {string} password - User password
|
|
125
|
+
* @param {Buffer} salt - Salt buffer (shared across parts in a session)
|
|
126
|
+
* @returns {object} { stream, iv, salt, getAuthTag }
|
|
127
|
+
*/
|
|
128
|
+
function createEncryptStream(password, salt) {
|
|
129
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
130
|
+
const key = deriveKey(password, salt);
|
|
131
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
132
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
stream: cipher,
|
|
136
|
+
iv: iv.toString('base64'),
|
|
137
|
+
salt: salt.toString('base64'),
|
|
138
|
+
getAuthTag: () => cipher.getAuthTag().toString('base64'),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a streaming decrypt decipher for per-part decryption.
|
|
144
|
+
* Auth tag is set immediately and verified on .final().
|
|
145
|
+
* @param {string} password - User password
|
|
146
|
+
* @param {string} ivBase64 - IV (base64)
|
|
147
|
+
* @param {string} saltBase64 - Salt (base64)
|
|
148
|
+
* @param {string} authTagBase64 - Auth tag (base64)
|
|
149
|
+
* @returns {crypto.Decipher} Decipher transform stream
|
|
150
|
+
*/
|
|
151
|
+
function createDecryptStream(password, ivBase64, saltBase64, authTagBase64) {
|
|
152
|
+
const iv = Buffer.from(ivBase64, 'base64');
|
|
153
|
+
const salt = Buffer.from(saltBase64, 'base64');
|
|
154
|
+
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
155
|
+
const key = deriveKey(password, salt);
|
|
156
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
157
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
158
|
+
});
|
|
159
|
+
decipher.setAuthTag(authTag);
|
|
160
|
+
return decipher;
|
|
161
|
+
}
|
|
162
|
+
|
|
112
163
|
module.exports = {
|
|
113
164
|
encrypt,
|
|
114
165
|
decrypt,
|
|
115
166
|
deriveKey,
|
|
116
167
|
packEncryptionMeta,
|
|
117
168
|
unpackEncryptionMeta,
|
|
169
|
+
generateSalt,
|
|
170
|
+
createEncryptStream,
|
|
171
|
+
createDecryptStream,
|
|
118
172
|
};
|
package/src/lib/docx-handler.js
CHANGED
|
@@ -100,7 +100,7 @@ async function readDocxBase64(docxPath) {
|
|
|
100
100
|
const metadataStart = fullText.indexOf(metadataMarker);
|
|
101
101
|
|
|
102
102
|
if (metadataStart === -1) {
|
|
103
|
-
throw new Error('No metadata found in DOCX file. This may not be a
|
|
103
|
+
throw new Error('No metadata found in DOCX file. This may not be a stegdoc-encoded file.');
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Find the separator "---" which comes after the metadata
|
package/src/lib/metadata.js
CHANGED
|
@@ -36,8 +36,9 @@ function createMetadata({
|
|
|
36
36
|
encrypted,
|
|
37
37
|
compressed,
|
|
38
38
|
contentHash,
|
|
39
|
+
pipelineOrder: 'compress-encrypt-base64',
|
|
39
40
|
encodingDate: new Date().toISOString(),
|
|
40
|
-
version: '
|
|
41
|
+
version: '4.0.0',
|
|
41
42
|
tool: 'stegdoc',
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -79,7 +80,7 @@ function validateMetadata(metadata) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
if (metadata.tool !== 'stegdoc' && metadata.tool !== '
|
|
83
|
+
if (metadata.tool !== 'stegdoc' && metadata.tool !== 'whitener') {
|
|
83
84
|
throw new Error('Invalid tool identifier in metadata');
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -102,10 +103,20 @@ function isMultiPart(metadata) {
|
|
|
102
103
|
return metadata.totalParts !== null && metadata.totalParts > 1;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Check if metadata indicates the v4 streaming format
|
|
108
|
+
* @param {object} metadata - Metadata object
|
|
109
|
+
* @returns {boolean} True if streaming format (v4+)
|
|
110
|
+
*/
|
|
111
|
+
function isStreamingFormat(metadata) {
|
|
112
|
+
return metadata.pipelineOrder === 'compress-encrypt-base64';
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
module.exports = {
|
|
106
116
|
createMetadata,
|
|
107
117
|
serializeMetadata,
|
|
108
118
|
parseMetadata,
|
|
109
119
|
validateMetadata,
|
|
110
120
|
isMultiPart,
|
|
121
|
+
isStreamingFormat,
|
|
111
122
|
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const { Transform, Writable } = require('stream');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transform stream that converts binary input to base64 text output.
|
|
6
|
+
* Buffers incomplete 3-byte groups across chunk boundaries.
|
|
7
|
+
*/
|
|
8
|
+
class Base64EncodeTransform extends Transform {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this._remainder = Buffer.alloc(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_transform(chunk, encoding, callback) {
|
|
15
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
16
|
+
const combined = this._remainder.length > 0 ? Buffer.concat([this._remainder, buf]) : buf;
|
|
17
|
+
const usable = combined.length - (combined.length % 3);
|
|
18
|
+
if (usable > 0) {
|
|
19
|
+
this.push(combined.slice(0, usable).toString('base64'));
|
|
20
|
+
}
|
|
21
|
+
this._remainder = usable < combined.length ? combined.slice(usable) : Buffer.alloc(0);
|
|
22
|
+
callback();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_flush(callback) {
|
|
26
|
+
if (this._remainder.length > 0) {
|
|
27
|
+
this.push(this._remainder.toString('base64'));
|
|
28
|
+
this._remainder = Buffer.alloc(0);
|
|
29
|
+
}
|
|
30
|
+
callback();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform stream that converts base64 text input to binary output.
|
|
36
|
+
* Buffers incomplete 4-char groups across chunk boundaries.
|
|
37
|
+
*/
|
|
38
|
+
class Base64DecodeTransform extends Transform {
|
|
39
|
+
constructor() {
|
|
40
|
+
super();
|
|
41
|
+
this._remainder = '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_transform(chunk, encoding, callback) {
|
|
45
|
+
const str = this._remainder + (Buffer.isBuffer(chunk) ? chunk.toString() : chunk);
|
|
46
|
+
const usable = str.length - (str.length % 4);
|
|
47
|
+
if (usable > 0) {
|
|
48
|
+
this.push(Buffer.from(str.slice(0, usable), 'base64'));
|
|
49
|
+
}
|
|
50
|
+
this._remainder = usable < str.length ? str.slice(usable) : '';
|
|
51
|
+
callback();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_flush(callback) {
|
|
55
|
+
if (this._remainder.length > 0) {
|
|
56
|
+
this.push(Buffer.from(this._remainder, 'base64'));
|
|
57
|
+
this._remainder = '';
|
|
58
|
+
}
|
|
59
|
+
callback();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Transform stream that passes data through unchanged while computing SHA-256 hash.
|
|
65
|
+
* Access the hex hash via .digest after the stream has ended.
|
|
66
|
+
*/
|
|
67
|
+
class HashPassthrough extends Transform {
|
|
68
|
+
constructor() {
|
|
69
|
+
super();
|
|
70
|
+
this._hash = crypto.createHash('sha256');
|
|
71
|
+
this._finalized = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_transform(chunk, encoding, callback) {
|
|
75
|
+
this._hash.update(chunk);
|
|
76
|
+
this.push(chunk);
|
|
77
|
+
callback();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_flush(callback) {
|
|
81
|
+
this._finalized = true;
|
|
82
|
+
callback();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get digest() {
|
|
86
|
+
if (!this._finalized) {
|
|
87
|
+
throw new Error('Cannot read digest before stream has ended');
|
|
88
|
+
}
|
|
89
|
+
return this._hash.digest('hex');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Writable stream that collects string output up to maxBytes.
|
|
95
|
+
* Calls an async onChunkReady callback when a chunk is full, applying
|
|
96
|
+
* backpressure to pause upstream until the callback resolves.
|
|
97
|
+
*/
|
|
98
|
+
class ChunkCollector extends Writable {
|
|
99
|
+
constructor(maxBytes, onChunkReady) {
|
|
100
|
+
super({ decodeStrings: false });
|
|
101
|
+
this._maxBytes = maxBytes;
|
|
102
|
+
this._buffer = '';
|
|
103
|
+
this._chunkIndex = 0;
|
|
104
|
+
this._onChunkReady = onChunkReady;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _write(chunk, encoding, callback) {
|
|
108
|
+
try {
|
|
109
|
+
this._buffer += typeof chunk === 'string' ? chunk : chunk.toString();
|
|
110
|
+
while (this._buffer.length >= this._maxBytes) {
|
|
111
|
+
const piece = this._buffer.slice(0, this._maxBytes);
|
|
112
|
+
this._buffer = this._buffer.slice(this._maxBytes);
|
|
113
|
+
await this._onChunkReady(piece, this._chunkIndex++);
|
|
114
|
+
}
|
|
115
|
+
callback();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
callback(err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async _final(callback) {
|
|
122
|
+
try {
|
|
123
|
+
if (this._buffer.length > 0) {
|
|
124
|
+
await this._onChunkReady(this._buffer, this._chunkIndex++);
|
|
125
|
+
this._buffer = '';
|
|
126
|
+
}
|
|
127
|
+
callback();
|
|
128
|
+
} catch (err) {
|
|
129
|
+
callback(err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get totalChunks() {
|
|
134
|
+
return this._chunkIndex;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Writable stream that collects binary Buffer output up to maxBytes.
|
|
140
|
+
* Calls an async onChunkReady callback with a Buffer when full.
|
|
141
|
+
*/
|
|
142
|
+
class BinaryChunkCollector extends Writable {
|
|
143
|
+
constructor(maxBytes, onChunkReady) {
|
|
144
|
+
super();
|
|
145
|
+
this._maxBytes = maxBytes;
|
|
146
|
+
this._buffers = [];
|
|
147
|
+
this._currentSize = 0;
|
|
148
|
+
this._chunkIndex = 0;
|
|
149
|
+
this._onChunkReady = onChunkReady;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async _write(chunk, encoding, callback) {
|
|
153
|
+
try {
|
|
154
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
155
|
+
this._buffers.push(buf);
|
|
156
|
+
this._currentSize += buf.length;
|
|
157
|
+
|
|
158
|
+
while (this._currentSize >= this._maxBytes) {
|
|
159
|
+
const combined = Buffer.concat(this._buffers);
|
|
160
|
+
const piece = combined.slice(0, this._maxBytes);
|
|
161
|
+
const leftover = combined.slice(this._maxBytes);
|
|
162
|
+
this._buffers = leftover.length > 0 ? [leftover] : [];
|
|
163
|
+
this._currentSize = leftover.length;
|
|
164
|
+
await this._onChunkReady(piece, this._chunkIndex++);
|
|
165
|
+
}
|
|
166
|
+
callback();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
callback(err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async _final(callback) {
|
|
173
|
+
try {
|
|
174
|
+
if (this._currentSize > 0) {
|
|
175
|
+
const combined = Buffer.concat(this._buffers);
|
|
176
|
+
await this._onChunkReady(combined, this._chunkIndex++);
|
|
177
|
+
this._buffers = [];
|
|
178
|
+
this._currentSize = 0;
|
|
179
|
+
}
|
|
180
|
+
callback();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
callback(err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get totalChunks() {
|
|
187
|
+
return this._chunkIndex;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
Base64EncodeTransform,
|
|
193
|
+
Base64DecodeTransform,
|
|
194
|
+
HashPassthrough,
|
|
195
|
+
ChunkCollector,
|
|
196
|
+
BinaryChunkCollector,
|
|
197
|
+
};
|
package/src/lib/utils.js
CHANGED
|
@@ -100,13 +100,13 @@ function generateFilename(hash, partNumber = null, totalParts = null, format = '
|
|
|
100
100
|
|
|
101
101
|
if (format === 'xlsx') {
|
|
102
102
|
// Server metrics theme - looks like periodic 4-hour monitoring exports
|
|
103
|
-
if (partNumber !== null
|
|
103
|
+
if (partNumber !== null) {
|
|
104
104
|
return `server_metrics_${dateStr}_${timeStr}_${reportId}_part${partNumber}.${ext}`;
|
|
105
105
|
}
|
|
106
106
|
return `server_metrics_${dateStr}_${timeStr}_${reportId}.${ext}`;
|
|
107
107
|
} else {
|
|
108
108
|
// DOCX - use a different theme
|
|
109
|
-
if (partNumber !== null
|
|
109
|
+
if (partNumber !== null) {
|
|
110
110
|
return `system_report_${dateStr}_${timeStr}_${reportId}_part${partNumber}.${ext}`;
|
|
111
111
|
}
|
|
112
112
|
return `system_report_${dateStr}_${timeStr}_${reportId}.${ext}`;
|