neozip-cli 0.70.0-alpha
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/CHANGELOG.md +72 -0
- package/DOCUMENTATION.md +194 -0
- package/LICENSE +22 -0
- package/README.md +504 -0
- package/WHY_NEOZIP.md +212 -0
- package/bin/neolist +16 -0
- package/bin/neounzip +16 -0
- package/bin/neozip +15 -0
- package/dist/neozipkit-bundles/blockchain.js +13091 -0
- package/dist/neozipkit-bundles/browser.js +5733 -0
- package/dist/neozipkit-bundles/core.js +3766 -0
- package/dist/neozipkit-bundles/server.js +14996 -0
- package/dist/neozipkit-wrappers/blockchain/core/contracts.js +16 -0
- package/dist/neozipkit-wrappers/blockchain/index.js +2 -0
- package/dist/neozipkit-wrappers/core/ZipDecompress.js +2 -0
- package/dist/neozipkit-wrappers/core/components/HashCalculator.js +2 -0
- package/dist/neozipkit-wrappers/core/components/Logger.js +2 -0
- package/dist/neozipkit-wrappers/core/constants/Errors.js +2 -0
- package/dist/neozipkit-wrappers/core/constants/Headers.js +2 -0
- package/dist/neozipkit-wrappers/core/encryption/ZipCrypto.js +7 -0
- package/dist/neozipkit-wrappers/core/index.js +3 -0
- package/dist/neozipkit-wrappers/index.js +13 -0
- package/dist/neozipkit-wrappers/server/index.js +2 -0
- package/dist/src/config/ConfigSetup.js +455 -0
- package/dist/src/config/ConfigStore.js +373 -0
- package/dist/src/config/ConfigWizard.js +453 -0
- package/dist/src/config/WalletConfig.js +372 -0
- package/dist/src/exit-codes.js +210 -0
- package/dist/src/index.js +141 -0
- package/dist/src/neolist.js +1194 -0
- package/dist/src/neounzip.js +2177 -0
- package/dist/src/neozip/CommentManager.js +240 -0
- package/dist/src/neozip/blockchain.js +383 -0
- package/dist/src/neozip/createZip.js +2273 -0
- package/dist/src/neozip/file-operations.js +920 -0
- package/dist/src/neozip/types.js +6 -0
- package/dist/src/neozip/user-interaction.js +256 -0
- package/dist/src/neozip/utils.js +96 -0
- package/dist/src/neozip.js +785 -0
- package/dist/src/server/CommentManager.js +240 -0
- package/dist/src/version.js +59 -0
- package/env.example +101 -0
- package/package.json +175 -0
|
@@ -0,0 +1,2273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ZIP creation functions for NeoZip CLI
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.createZip = createZip;
|
|
43
|
+
exports.testArchiveIntegrity = testArchiveIntegrity;
|
|
44
|
+
exports.upgradeZipForTokenization = upgradeZipForTokenization;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const src_1 = require('../../neozipkit-wrappers');
|
|
48
|
+
const ZipCrypto_1 = require('../../neozipkit-wrappers/core/encryption/ZipCrypto');
|
|
49
|
+
const server_1 = require('../../neozipkit-wrappers/server');
|
|
50
|
+
const blockchain_1 = require('../../neozipkit-wrappers/blockchain');
|
|
51
|
+
const contracts_1 = require('../../neozipkit-wrappers/blockchain/core/contracts');
|
|
52
|
+
const HashCalculator_1 = __importDefault(require('../../neozipkit-wrappers/core/components/HashCalculator'));
|
|
53
|
+
const CommentManager_1 = require("./CommentManager");
|
|
54
|
+
const utils_1 = require("./utils");
|
|
55
|
+
const file_operations_1 = require("./file-operations");
|
|
56
|
+
const blockchain_2 = require("./blockchain");
|
|
57
|
+
const user_interaction_1 = require("./user-interaction");
|
|
58
|
+
const exit_codes_1 = require("../exit-codes");
|
|
59
|
+
const version_1 = require("../version");
|
|
60
|
+
/**
|
|
61
|
+
* BufferOutputWriter: Collects ZIP data in memory (for in-memory mode)
|
|
62
|
+
*/
|
|
63
|
+
class BufferOutputWriter {
|
|
64
|
+
constructor() {
|
|
65
|
+
this.chunks = [];
|
|
66
|
+
this.position = 0;
|
|
67
|
+
this.headerPositions = new Map(); // Track header positions for updates
|
|
68
|
+
}
|
|
69
|
+
async writeHeader(header) {
|
|
70
|
+
const headerPosition = this.position;
|
|
71
|
+
this.headerPositions.set(this.chunks.length, headerPosition);
|
|
72
|
+
this.chunks.push(header);
|
|
73
|
+
this.position += header.length;
|
|
74
|
+
}
|
|
75
|
+
async writeData(data) {
|
|
76
|
+
this.chunks.push(data);
|
|
77
|
+
this.position += data.length;
|
|
78
|
+
}
|
|
79
|
+
async updateHeader(headerPosition, compressedSize, crc) {
|
|
80
|
+
// Find the chunk containing this header position
|
|
81
|
+
let currentPos = 0;
|
|
82
|
+
for (let i = 0; i < this.chunks.length; i++) {
|
|
83
|
+
const chunk = this.chunks[i];
|
|
84
|
+
const chunkStart = currentPos;
|
|
85
|
+
const chunkEnd = currentPos + chunk.length;
|
|
86
|
+
if (headerPosition >= chunkStart && headerPosition < chunkEnd) {
|
|
87
|
+
// This chunk contains the header
|
|
88
|
+
const offsetInChunk = headerPosition - chunkStart;
|
|
89
|
+
if (chunk.length >= offsetInChunk + 22 && chunk.readUInt32LE(offsetInChunk) === 0x04034b50) {
|
|
90
|
+
// Valid local file header signature
|
|
91
|
+
// Update CRC (offset 14) if provided
|
|
92
|
+
if (crc !== undefined) {
|
|
93
|
+
chunk.writeUInt32LE(crc, offsetInChunk + 14);
|
|
94
|
+
}
|
|
95
|
+
// Update compressed size (offset 18)
|
|
96
|
+
chunk.writeUInt32LE(compressedSize, offsetInChunk + 18);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
currentPos = chunkEnd;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
getPosition() {
|
|
104
|
+
return this.position;
|
|
105
|
+
}
|
|
106
|
+
async close() {
|
|
107
|
+
// Nothing to close for buffer mode
|
|
108
|
+
}
|
|
109
|
+
getResult() {
|
|
110
|
+
return Buffer.concat(this.chunks);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* FileOutputWriter: Writes ZIP data directly to file (for file-based mode)
|
|
115
|
+
*/
|
|
116
|
+
class FileOutputWriter {
|
|
117
|
+
constructor(archiveName, outputFd, outputStream, positionRef) {
|
|
118
|
+
this.stream = outputStream;
|
|
119
|
+
this.fd = outputFd;
|
|
120
|
+
this.positionRef = positionRef;
|
|
121
|
+
}
|
|
122
|
+
async writeHeader(header) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
this.stream.write(header, (error) => {
|
|
125
|
+
if (error) {
|
|
126
|
+
reject(error);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.positionRef.current += header.length;
|
|
130
|
+
resolve();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async writeData(data) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
this.stream.write(data, (error) => {
|
|
138
|
+
if (error) {
|
|
139
|
+
reject(error);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.positionRef.current += data.length;
|
|
143
|
+
resolve();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async updateHeader(headerPosition, compressedSize, crc) {
|
|
149
|
+
// Update CRC (offset 14) if provided
|
|
150
|
+
if (crc !== undefined) {
|
|
151
|
+
const crcOffset = headerPosition + 14;
|
|
152
|
+
const crcBuffer = Buffer.alloc(4);
|
|
153
|
+
crcBuffer.writeUInt32LE(crc, 0);
|
|
154
|
+
fs.writeSync(this.fd, crcBuffer, 0, 4, crcOffset);
|
|
155
|
+
}
|
|
156
|
+
// Update compressed size (offset 18)
|
|
157
|
+
const compressedSizeOffset = headerPosition + 18;
|
|
158
|
+
const sizeBuffer = Buffer.alloc(4);
|
|
159
|
+
sizeBuffer.writeUInt32LE(compressedSize, 0);
|
|
160
|
+
fs.writeSync(this.fd, sizeBuffer, 0, 4, compressedSizeOffset);
|
|
161
|
+
}
|
|
162
|
+
getPosition() {
|
|
163
|
+
return this.positionRef.current;
|
|
164
|
+
}
|
|
165
|
+
async close() {
|
|
166
|
+
// Close is handled by ZipCreator.closeOutput()
|
|
167
|
+
// This method exists for interface compliance
|
|
168
|
+
}
|
|
169
|
+
getResult() {
|
|
170
|
+
return null; // File mode doesn't return buffer
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Create ZIP archive using the EXACT working code from zip.ts
|
|
175
|
+
*/
|
|
176
|
+
async function createZip(archiveName, files, options = { verbose: false, quiet: false }) {
|
|
177
|
+
const zipCreator = new ZipCreator(archiveName, files, options);
|
|
178
|
+
await zipCreator.create();
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* ZipCreator class that handles ZIP file creation with modular, reusable methods
|
|
182
|
+
*
|
|
183
|
+
* ZIP FILE CREATION PROCEDURE:
|
|
184
|
+
*
|
|
185
|
+
* This class follows a sequential, efficient ZIP file creation process:
|
|
186
|
+
*
|
|
187
|
+
* 1. FILE ANALYSIS (processFiles):
|
|
188
|
+
* - Collects files to be added to the ZIP archive
|
|
189
|
+
* - Creates ZipEntry objects using zipkit.createZipEntry() for each file
|
|
190
|
+
* - Each entry is automatically added to zip.zipEntries[] (single source of truth)
|
|
191
|
+
* - Entries are stored in processing order (files first)
|
|
192
|
+
* - Sets file metadata: timestamps, sizes, paths, etc.
|
|
193
|
+
* - Marks entries as updated (entry.isUpdated = true) for compression
|
|
194
|
+
*
|
|
195
|
+
* 2. ZIP HEADER & COMPRESSION (compressEntries):
|
|
196
|
+
* - Loops through zip.zipEntries[] directly (maintains exact processing order)
|
|
197
|
+
* - For each entry marked for update (entry.isUpdated):
|
|
198
|
+
* a. Reads file data from disk (for in-memory mode) or streams (for file-based mode)
|
|
199
|
+
* b. Applies line ending conversion if requested
|
|
200
|
+
* c. SEQUENTIAL WRITE PROCESS:
|
|
201
|
+
* - Step 1: Get current file position
|
|
202
|
+
* - Step 2: Create local file header with placeholder compressed size (0)
|
|
203
|
+
* - Step 3: Write local header to output (file or memory buffer)
|
|
204
|
+
* - Step 4: Compress file data using ZipCompress.compressFileBuffer()
|
|
205
|
+
* - Step 5: Write compressed data to output
|
|
206
|
+
* - Step 6: Seek back and update compressed size in local header
|
|
207
|
+
* d. Updates entry with compression results: compressedSize, crc, sha256
|
|
208
|
+
* e. Accumulates SHA-256 hashes for blockchain metadata (if enabled)
|
|
209
|
+
*
|
|
210
|
+
* 3. METADATA ADDITION (handleTokenization):
|
|
211
|
+
* - After all files are compressed, calculates blockchain metadata (if enabled)
|
|
212
|
+
* - Creates metadata entry using zipkit.createZipEntry() for:
|
|
213
|
+
* - META-INF/NZIP.TOKEN (blockchain token metadata)
|
|
214
|
+
* - META-INF/TIMESTAMP.METADATA (timestamp metadata)
|
|
215
|
+
* - META-INF/TS-SUBMIT.OTS (OpenTimestamp proof)
|
|
216
|
+
* - Each metadata entry is automatically added to zip.zipEntries[] (at the end)
|
|
217
|
+
* - Writes metadata to ZIP file using same sequential process:
|
|
218
|
+
* - Local header (with placeholder) → metadata data → update header
|
|
219
|
+
* - Metadata entries use STORED compression (cmpMethod = 0) since they're small
|
|
220
|
+
*
|
|
221
|
+
* 4. CENTRAL DIRECTORY GENERATION (finalize):
|
|
222
|
+
* - Uses zip.zipEntries[] directly (already in correct order: files first, metadata last)
|
|
223
|
+
* - For each entry in zip.zipEntries[]:
|
|
224
|
+
* - Updates localHdrOffset with actual file position
|
|
225
|
+
* - Generates central directory entry using entry.centralDirEntry()
|
|
226
|
+
* - Writes central directory entry to output
|
|
227
|
+
* - Calculates central directory size and offset
|
|
228
|
+
* - Creates and writes End of Central Directory record
|
|
229
|
+
* - Closes output stream/file
|
|
230
|
+
*
|
|
231
|
+
* KEY PRINCIPLES:
|
|
232
|
+
* - zip.zipEntries[] is the SINGLE SOURCE OF TRUTH for entry order
|
|
233
|
+
* - Entries are added to zip.zipEntries[] in processing order (files first, metadata last)
|
|
234
|
+
* - No reordering or separation logic needed - natural order is correct
|
|
235
|
+
* - Sequential write ensures efficient processing: header → data → update header
|
|
236
|
+
* - Local header management handled in neozipkit (ZipCompress)
|
|
237
|
+
* - Zipkit is used for loading existing ZIPs (buffer-based mode)
|
|
238
|
+
*/
|
|
239
|
+
class ZipCreator {
|
|
240
|
+
constructor(archiveName, files, options) {
|
|
241
|
+
this.archiveName = archiveName;
|
|
242
|
+
this.files = files;
|
|
243
|
+
this.options = options;
|
|
244
|
+
this.tempArchiveName = '';
|
|
245
|
+
this.inputFiles = [];
|
|
246
|
+
this.entryToPath = new Map();
|
|
247
|
+
this.entryToDisplay = new Map();
|
|
248
|
+
this.entryToStats = new Map();
|
|
249
|
+
this.hashAccumulator = null; // Accumulate SHA-256 hashes as entries are added
|
|
250
|
+
this.zipWriter = null; // ZIP file writer (replaces outputStream/outputFd)
|
|
251
|
+
this.currentPosition = 0;
|
|
252
|
+
this.totalIn = 0;
|
|
253
|
+
this.totalOut = 0;
|
|
254
|
+
this.filesCount = 0;
|
|
255
|
+
this.fdCache = new Map();
|
|
256
|
+
this.compressionOffset = 0;
|
|
257
|
+
this.centralDirOffset = 0;
|
|
258
|
+
this.zip = new server_1.ZipkitServer(); // Use ZipkitServer for ZIP operations
|
|
259
|
+
// Initialize HashAccumulator if blockchain features are enabled
|
|
260
|
+
if (this.options.blockchain || this.options.blockchainOts) {
|
|
261
|
+
this.hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Initialize the output file stream for direct writing
|
|
266
|
+
* Uses ZipkitServer's initializeZipFile() method
|
|
267
|
+
* Always writes to temporary file first for atomic operations
|
|
268
|
+
*/
|
|
269
|
+
async initializeOutput() {
|
|
270
|
+
// Ensure temp file name is set (should be set in initialize())
|
|
271
|
+
if (!this.tempArchiveName) {
|
|
272
|
+
throw new Error('Temp archive name not initialized. Call initialize() first.');
|
|
273
|
+
}
|
|
274
|
+
// For all modes (file-based or in-memory blockchain), create temp file first
|
|
275
|
+
// For in-memory blockchain mode, compression will write directly to temp file
|
|
276
|
+
// For file-based mode, compression will also write to temp file
|
|
277
|
+
// Final file will be moved atomically after successful completion
|
|
278
|
+
this.zipWriter = await this.zip.initializeZipFile(this.tempArchiveName);
|
|
279
|
+
this.currentPosition = 0;
|
|
280
|
+
if (this.options.verbose) {
|
|
281
|
+
(0, utils_1.log)(`📊 Created temporary output file: ${this.tempArchiveName}`, this.options);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Write a chunk of data directly to the output file stream or buffer
|
|
286
|
+
* Used for in-memory mode and metadata entries
|
|
287
|
+
*/
|
|
288
|
+
async writeChunk(data) {
|
|
289
|
+
// For in-memory non-blockchain mode, write to buffer
|
|
290
|
+
if (this.options.inMemory && !this.options.blockchain && !this.options.blockchainOts) {
|
|
291
|
+
const bufferWriter = this.zip.bufferWriter;
|
|
292
|
+
if (bufferWriter) {
|
|
293
|
+
// Determine if this is a header or data by checking signature
|
|
294
|
+
if (data.length >= 4 && data.readUInt32LE(0) === 0x04034b50) {
|
|
295
|
+
// This is a local file header
|
|
296
|
+
await bufferWriter.writeHeader(data);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// This is data
|
|
300
|
+
await bufferWriter.writeData(data);
|
|
301
|
+
}
|
|
302
|
+
this.currentPosition = bufferWriter.getPosition();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// For file-based mode or in-memory blockchain mode, write to file stream
|
|
307
|
+
if (!this.zipWriter) {
|
|
308
|
+
throw new Error('ZIP writer not initialized');
|
|
309
|
+
}
|
|
310
|
+
return new Promise((resolve, reject) => {
|
|
311
|
+
this.zipWriter.outputStream.write(data, (error) => {
|
|
312
|
+
if (error) {
|
|
313
|
+
reject(error);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
this.zipWriter.currentPosition += data.length;
|
|
317
|
+
this.currentPosition = this.zipWriter.currentPosition;
|
|
318
|
+
resolve();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Close the output stream and file descriptor
|
|
325
|
+
* Uses ZipkitServer's finalizeZipFile() method
|
|
326
|
+
*/
|
|
327
|
+
async closeOutput() {
|
|
328
|
+
if (this.zipWriter) {
|
|
329
|
+
await this.zip.finalizeZipFile(this.zipWriter);
|
|
330
|
+
this.zipWriter = null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async create() {
|
|
334
|
+
try {
|
|
335
|
+
await this.initialize();
|
|
336
|
+
// Initialize output stream for file-based mode OR in-memory blockchain mode
|
|
337
|
+
// For in-memory blockchain mode, we need to write directly to file during compression
|
|
338
|
+
// For in-memory non-blockchain mode, output is written directly during compression
|
|
339
|
+
if (!this.options.inMemory || (this.options.blockchain || this.options.blockchainOts)) {
|
|
340
|
+
await this.initializeOutput();
|
|
341
|
+
}
|
|
342
|
+
await this.processFiles();
|
|
343
|
+
await this.setupCompression();
|
|
344
|
+
await this.handleComments();
|
|
345
|
+
await this.handleCompression();
|
|
346
|
+
await this.handleTokenization();
|
|
347
|
+
// Finalize ZIP file (write central directory and end record)
|
|
348
|
+
// For all modes (including in-memory non-blockchain), finalize after metadata is added
|
|
349
|
+
// This ensures metadata is included in the ZIP before central directory is written
|
|
350
|
+
await this.finalize();
|
|
351
|
+
// Test integrity AFTER finalization (so it includes metadata)
|
|
352
|
+
if (this.options.testIntegrity) {
|
|
353
|
+
const integrityPassed = await testArchiveIntegrity(this.archiveName, this.options);
|
|
354
|
+
if (!integrityPassed) {
|
|
355
|
+
(0, utils_1.logError)(`❌ Archive integrity test failed for: ${this.archiveName}`);
|
|
356
|
+
throw new Error('Archive integrity test failed');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Display final statistics
|
|
360
|
+
this.displayStatistics();
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
// On error, ensure temp file is cleaned up
|
|
364
|
+
if (this.tempArchiveName && fs.existsSync(this.tempArchiveName)) {
|
|
365
|
+
try {
|
|
366
|
+
fs.unlinkSync(this.tempArchiveName);
|
|
367
|
+
if (this.options.verbose) {
|
|
368
|
+
(0, utils_1.log)(`🧹 Cleaned up temporary file after error`, this.options);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (cleanupError) {
|
|
372
|
+
// Ignore cleanup errors
|
|
373
|
+
if (this.options.verbose) {
|
|
374
|
+
(0, utils_1.log)(`⚠️ Warning: Could not clean up temp file: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, this.options);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Re-throw error after cleanup
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
displayStatistics() {
|
|
383
|
+
(0, utils_1.log)(`\n✅ ZIP archive created: ${this.archiveName}`, this.options);
|
|
384
|
+
(0, utils_1.log)(` Files: ${this.filesCount}`, this.options);
|
|
385
|
+
(0, utils_1.log)(` Total size: ${(0, utils_1.formatBytes)(this.totalIn)}`, this.options);
|
|
386
|
+
(0, utils_1.log)(` Compressed size: ${(0, utils_1.formatBytes)(this.totalOut)}`, this.options);
|
|
387
|
+
(0, utils_1.log)(` Compression ratio: ${this.totalIn > 0 ? Math.round((1 - this.totalOut / this.totalIn) * 100) : 0}%`, this.options);
|
|
388
|
+
if (this.options.debug) {
|
|
389
|
+
(0, utils_1.logDebug)(` Input files processed: ${this.filesCount}`, this.options);
|
|
390
|
+
(0, utils_1.logDebug)(` Total uncompressed size: ${this.totalIn} bytes`, this.options);
|
|
391
|
+
(0, utils_1.logDebug)(` Total compressed size: ${this.totalOut} bytes`, this.options);
|
|
392
|
+
(0, utils_1.logDebug)(` Compression ratio: ${this.totalIn > 0 ? Math.round((1 - this.totalOut / this.totalIn) * 100) : 0}%`, this.options);
|
|
393
|
+
(0, utils_1.logDebug)(` Archive file: ${this.archiveName}`, this.options);
|
|
394
|
+
if (fs.existsSync(this.archiveName)) {
|
|
395
|
+
(0, utils_1.logDebug)(` Archive file size: ${fs.statSync(this.archiveName).size} bytes`, this.options);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async initialize() {
|
|
400
|
+
// Display application information
|
|
401
|
+
(0, utils_1.log)(`🚀 NEOZIP v${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})`, this.options);
|
|
402
|
+
(0, utils_1.log)(`📦 Creating: ${this.archiveName}`, this.options);
|
|
403
|
+
(0, utils_1.log)(`📁 Files: ${this.files.length} file(s)`, this.options);
|
|
404
|
+
const compressionDesc = this.options.level === 0 ? 'store (no compression)' : `${this.options.compression} (level ${this.options.level})`;
|
|
405
|
+
(0, utils_1.log)(`🔧 Compression: ${compressionDesc}`, this.options);
|
|
406
|
+
(0, utils_1.log)(`📊 Block size: ${(0, utils_1.formatBytes)(this.options.blockSize || 64 * 1024)}`, this.options);
|
|
407
|
+
(0, utils_1.log)(`🔗 Tokenization: ${this.options.blockchain ? 'enabled' : 'disabled'}`, this.options);
|
|
408
|
+
const encryptionMethod = this.options.encryptionMethod || 'pkzip';
|
|
409
|
+
(0, utils_1.log)(`🔐 Encryption: ${this.options.encrypt ? `${encryptionMethod.toUpperCase()} enabled` : 'disabled'}`, this.options);
|
|
410
|
+
(0, utils_1.log)('', this.options);
|
|
411
|
+
if (this.options.debug) {
|
|
412
|
+
(0, utils_1.logDebug)('Debug mode enabled', this.options);
|
|
413
|
+
(0, utils_1.logDebug)(`Command line arguments: ${process.argv.join(' ')}`, this.options);
|
|
414
|
+
(0, utils_1.logDebug)(`Working directory: ${process.cwd()}`, this.options);
|
|
415
|
+
(0, utils_1.logDebug)(`Node.js version: ${process.version}`, this.options);
|
|
416
|
+
(0, utils_1.logDebug)(`Platform: ${process.platform} ${process.arch}`, this.options);
|
|
417
|
+
(0, utils_1.log)('', this.options);
|
|
418
|
+
}
|
|
419
|
+
if (this.options.verbose) {
|
|
420
|
+
(0, utils_1.log)(`Creating ZIP archive: ${this.archiveName}`, this.options);
|
|
421
|
+
(0, utils_1.log)(`Files to compress: ${this.files.join(', ')}`, this.options);
|
|
422
|
+
if (this.options.blockchain) {
|
|
423
|
+
if (this.options.blockchainDefault) {
|
|
424
|
+
(0, utils_1.log)(`Blockchain: enabled (auto-select default)`, this.options);
|
|
425
|
+
}
|
|
426
|
+
else if (this.options.blockchainMint) {
|
|
427
|
+
(0, utils_1.log)(`Blockchain: enabled (force mint new)`, this.options);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
(0, utils_1.log)(`Blockchain: enabled (default)`, this.options);
|
|
431
|
+
}
|
|
432
|
+
(0, utils_1.log)(`Tokenization: enabled`, this.options);
|
|
433
|
+
(0, utils_1.log)(`Network: ${this.options.network || 'base-sepolia'}`, this.options);
|
|
434
|
+
}
|
|
435
|
+
else if (this.options.blockchainOts) {
|
|
436
|
+
(0, utils_1.log)(`Blockchain: enabled (OpenTimestamp)`, this.options);
|
|
437
|
+
(0, utils_1.log)(`Bitcoin blockchain: enabled`, this.options);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
(0, utils_1.log)(`Blockchain: disabled`, this.options);
|
|
441
|
+
(0, utils_1.log)(`Tokenization: disabled`, this.options);
|
|
442
|
+
}
|
|
443
|
+
if (this.options.toCrlf) {
|
|
444
|
+
(0, utils_1.log)(`Line ending conversion: LF to CRLF (Windows style)`, this.options);
|
|
445
|
+
}
|
|
446
|
+
if (this.options.fromCrlf) {
|
|
447
|
+
(0, utils_1.log)(`Line ending conversion: CRLF to LF (Unix style)`, this.options);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Ensure parent directory exists
|
|
451
|
+
const parentDir = path.dirname(this.archiveName);
|
|
452
|
+
if (parentDir && parentDir !== '.' && !fs.existsSync(parentDir)) {
|
|
453
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
454
|
+
if (this.options.verbose)
|
|
455
|
+
(0, utils_1.log)(`Created directory: ${parentDir}`, this.options);
|
|
456
|
+
}
|
|
457
|
+
// Create temporary ZIP file name
|
|
458
|
+
const tempDir = (0, utils_1.getTempDir)(this.options);
|
|
459
|
+
// Ensure temp directory exists
|
|
460
|
+
if (!fs.existsSync(tempDir)) {
|
|
461
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
462
|
+
}
|
|
463
|
+
this.tempArchiveName = path.join(tempDir, `${path.basename(this.archiveName)}.tmp.${Date.now()}`);
|
|
464
|
+
if (this.options.tempPath && this.options.verbose) {
|
|
465
|
+
(0, utils_1.log)(`📁 Using custom temp directory: ${tempDir}`, this.options);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Collect and filter files for compression (unified for both modes)
|
|
470
|
+
*/
|
|
471
|
+
async collectAndFilterFiles() {
|
|
472
|
+
const collectedFiles = await (0, file_operations_1.collectFilesRecursively)(this.files, this.options);
|
|
473
|
+
const filePaths = collectedFiles.map(f => f.displayPath);
|
|
474
|
+
const filteredFilePaths = (0, file_operations_1.filterFiles)(filePaths, this.options);
|
|
475
|
+
return collectedFiles.filter(f => filteredFilePaths.includes(f.displayPath)).map(f => ({
|
|
476
|
+
entryName: f.entryName,
|
|
477
|
+
absPath: f.absPath,
|
|
478
|
+
displayPath: f.displayPath,
|
|
479
|
+
stat: f.stat,
|
|
480
|
+
isSymlink: f.isSymlink,
|
|
481
|
+
linkTarget: f.linkTarget,
|
|
482
|
+
isHardLink: f.isHardLink,
|
|
483
|
+
originalEntry: f.originalEntry,
|
|
484
|
+
inode: f.inode
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
async processFiles() {
|
|
488
|
+
// Load existing archive if update or freshen mode is enabled
|
|
489
|
+
let existingArchive = null;
|
|
490
|
+
if (this.options.update || this.options.freshen) {
|
|
491
|
+
try {
|
|
492
|
+
existingArchive = (0, file_operations_1.loadExistingArchive)(this.archiveName, this.options);
|
|
493
|
+
if (existingArchive) {
|
|
494
|
+
const mode = this.options.freshen ? 'Freshen' : 'Update';
|
|
495
|
+
const existingDir = existingArchive.getDirectory();
|
|
496
|
+
(0, utils_1.log)(`📦 ${mode} mode: Found existing archive with ${existingDir?.length || 0} files`, this.options);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const mode = this.options.freshen ? 'Freshen' : 'Update';
|
|
500
|
+
(0, utils_1.log)(`📦 ${mode} mode: No existing archive found, creating new one`, this.options);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
(0, utils_1.logError)(`❌ Failed to load existing archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
505
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_OPEN_ARCHIVE);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Collect files recursively using unified method
|
|
509
|
+
const filteredFiles = await this.collectAndFilterFiles();
|
|
510
|
+
if (this.options.verbose && (this.options.include || this.options.exclude || this.options.suffixes)) {
|
|
511
|
+
const collectedFiles = await (0, file_operations_1.collectFilesRecursively)(this.files, this.options);
|
|
512
|
+
(0, utils_1.log)(`File filtering applied:`, this.options);
|
|
513
|
+
if (this.options.include) {
|
|
514
|
+
(0, utils_1.log)(` Include patterns: ${this.options.include.join(', ')}`, this.options);
|
|
515
|
+
}
|
|
516
|
+
if (this.options.exclude) {
|
|
517
|
+
(0, utils_1.log)(` Exclude patterns: ${this.options.exclude.join(', ')}`, this.options);
|
|
518
|
+
}
|
|
519
|
+
if (this.options.suffixes) {
|
|
520
|
+
(0, utils_1.log)(` Excluded suffixes: ${this.options.suffixes.join(', ')}`, this.options);
|
|
521
|
+
}
|
|
522
|
+
(0, utils_1.log)(` Original files: ${collectedFiles.length}, Filtered files: ${filteredFiles.length}`, this.options);
|
|
523
|
+
}
|
|
524
|
+
// Store filtered files for use in compression
|
|
525
|
+
this.inputFiles = filteredFiles;
|
|
526
|
+
// Filter files for update or freshen mode
|
|
527
|
+
if ((this.options.update || this.options.freshen) && existingArchive) {
|
|
528
|
+
const originalCount = this.inputFiles.length;
|
|
529
|
+
this.inputFiles = this.inputFiles.filter(file => {
|
|
530
|
+
const shouldUpdate = (0, file_operations_1.shouldUpdateFile)(file.entryName, file.stat, existingArchive, this.options.freshen);
|
|
531
|
+
if (!shouldUpdate && this.options.verbose) {
|
|
532
|
+
const reason = this.options.freshen ? 'not in archive' : 'up to date';
|
|
533
|
+
(0, utils_1.log)(`skipping: ${file.displayPath} (${reason})`, this.options);
|
|
534
|
+
}
|
|
535
|
+
return shouldUpdate;
|
|
536
|
+
});
|
|
537
|
+
const mode = this.options.freshen ? 'Freshen' : 'Update';
|
|
538
|
+
if (this.options.verbose) {
|
|
539
|
+
(0, utils_1.log)(`${mode} filtering: ${originalCount} files checked, ${this.inputFiles.length} files need updating`, this.options);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Display planned files (optional)
|
|
543
|
+
if (this.options.verbose) {
|
|
544
|
+
(0, utils_1.log)('Files to compress:', this.options);
|
|
545
|
+
for (const it of this.inputFiles) {
|
|
546
|
+
(0, utils_1.log)(` - ${it.displayPath} (${(0, utils_1.formatBytes)(it.stat.size)})`, this.options);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Handle existing archive entries for update mode
|
|
550
|
+
if (this.options.update && existingArchive) {
|
|
551
|
+
const existingEntries = await existingArchive.getDirectory() || [];
|
|
552
|
+
const updatedEntryNames = new Set(this.inputFiles.map(f => f.entryName));
|
|
553
|
+
// Copy existing entries that aren't being updated
|
|
554
|
+
const entriesToPreserve = existingEntries.filter(entry => !updatedEntryNames.has(entry.filename));
|
|
555
|
+
for (const existingEntry of entriesToPreserve) {
|
|
556
|
+
// Copy the existing entry to the new archive
|
|
557
|
+
const newEntry = this.zip.createZipEntry(existingEntry.filename);
|
|
558
|
+
newEntry.timeDateDOS = existingEntry.timeDateDOS;
|
|
559
|
+
newEntry.uncompressedSize = existingEntry.uncompressedSize;
|
|
560
|
+
newEntry.compressedSize = existingEntry.compressedSize;
|
|
561
|
+
newEntry.cmpMethod = existingEntry.cmpMethod;
|
|
562
|
+
newEntry.crc = existingEntry.crc;
|
|
563
|
+
// Copy the file data from the existing archive
|
|
564
|
+
try {
|
|
565
|
+
const existingData = await (async () => {
|
|
566
|
+
try {
|
|
567
|
+
const buf = fs.readFileSync(this.archiveName);
|
|
568
|
+
const kit = new (require('../../neozipkit-wrappers').default)();
|
|
569
|
+
kit.loadZip(buf);
|
|
570
|
+
return await kit.extract(existingEntry);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
})();
|
|
576
|
+
if (existingData) {
|
|
577
|
+
newEntry.fileBuffer = existingData;
|
|
578
|
+
newEntry.isUpdated = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
if (this.options.verbose) {
|
|
583
|
+
(0, utils_1.log)(`warning: Could not copy existing entry ${existingEntry.filename}`, this.options);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (this.options.verbose)
|
|
587
|
+
(0, utils_1.log)(`preserved: ${existingEntry.filename} (existing)`, this.options);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Create entries for input files
|
|
591
|
+
let processedFiles = 0;
|
|
592
|
+
const totalFiles = this.inputFiles.length;
|
|
593
|
+
for (const { entryName, absPath, stat, displayPath, isSymlink, linkTarget, isHardLink, originalEntry, inode } of this.inputFiles) {
|
|
594
|
+
if (this.options.debug) {
|
|
595
|
+
(0, utils_1.logDebug)(`Processing file: ${displayPath}`, this.options);
|
|
596
|
+
(0, utils_1.logDebug)(` Entry name: ${entryName}`, this.options);
|
|
597
|
+
(0, utils_1.logDebug)(` Absolute path: ${absPath}`, this.options);
|
|
598
|
+
(0, utils_1.logDebug)(` File size: ${stat.size} bytes`, this.options);
|
|
599
|
+
(0, utils_1.logDebug)(` File stats: ${JSON.stringify({
|
|
600
|
+
isFile: stat.isFile(),
|
|
601
|
+
isDirectory: stat.isDirectory(),
|
|
602
|
+
mode: stat.mode,
|
|
603
|
+
mtime: stat.mtime.toISOString(),
|
|
604
|
+
atime: stat.atime.toISOString(),
|
|
605
|
+
ctime: stat.ctime.toISOString()
|
|
606
|
+
})}`, this.options);
|
|
607
|
+
}
|
|
608
|
+
if (this.options.progress && !this.options.quiet) {
|
|
609
|
+
(0, utils_1.log)(`📦 Processing file ${processedFiles + 1}/${totalFiles}: ${displayPath}`, this.options);
|
|
610
|
+
}
|
|
611
|
+
if (this.options.showFiles && !this.options.quiet) {
|
|
612
|
+
(0, utils_1.log)(`adding: ${entryName}`, this.options);
|
|
613
|
+
}
|
|
614
|
+
const entry = this.zip.createZipEntry(entryName);
|
|
615
|
+
if (this.options.debug) {
|
|
616
|
+
(0, utils_1.logDebug)(` Creating ZIP entry for: ${entryName}`, this.options);
|
|
617
|
+
}
|
|
618
|
+
// Set the timestamp directly from file modification time
|
|
619
|
+
// @ts-ignore neozipkit ZipEntry has method setDateTime at runtime
|
|
620
|
+
entry.timeDateDOS = entry.setDateTime(new Date(stat.mtimeMs));
|
|
621
|
+
if (this.options.debug) {
|
|
622
|
+
(0, utils_1.logDebug)(` Set timestamp: ${new Date(stat.mtimeMs).toISOString()}`, this.options);
|
|
623
|
+
(0, utils_1.logDebug)(` Set DOS timestamp: ${entry.timeDateDOS}`, this.options);
|
|
624
|
+
}
|
|
625
|
+
// Also set lastModTimeDate for better compatibility (same as timeDateDOS)
|
|
626
|
+
entry.lastModTimeDate = entry.timeDateDOS;
|
|
627
|
+
// Set extended timestamps for better accuracy on Unix/Mac systems
|
|
628
|
+
// Try to set extended timestamps if the properties exist
|
|
629
|
+
try {
|
|
630
|
+
// @ts-ignore neozipkit ZipEntry may have extended timestamp properties
|
|
631
|
+
entry.ntfsTime = {
|
|
632
|
+
mtime: stat.mtimeMs,
|
|
633
|
+
atime: stat.atimeMs,
|
|
634
|
+
ctime: stat.ctimeMs
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
catch (e) {
|
|
638
|
+
// Property may not exist, that's okay
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
// @ts-ignore neozipkit ZipEntry may have extended timestamp properties
|
|
642
|
+
entry.extendedTime = {
|
|
643
|
+
mtime: stat.mtimeMs,
|
|
644
|
+
atime: stat.atimeMs,
|
|
645
|
+
ctime: stat.ctimeMs
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
// Property may not exist, that's okay
|
|
650
|
+
}
|
|
651
|
+
// Preserve UID/GID if requested (Unix-like systems only)
|
|
652
|
+
if (this.options.preservePerms && process.platform !== 'win32') {
|
|
653
|
+
try {
|
|
654
|
+
// @ts-ignore fs.Stats has uid and gid on Unix systems
|
|
655
|
+
entry.uid = stat.uid;
|
|
656
|
+
// @ts-ignore fs.Stats has uid and gid on Unix systems
|
|
657
|
+
entry.gid = stat.gid;
|
|
658
|
+
if (this.options.debug) {
|
|
659
|
+
(0, utils_1.logDebug)(` Preserved permissions: uid=${entry.uid}, gid=${entry.gid}`, this.options);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch (e) {
|
|
663
|
+
if (this.options.debug) {
|
|
664
|
+
(0, utils_1.logDebug)(` Could not preserve permissions: ${e}`, this.options);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Handle symbolic links
|
|
669
|
+
if (isSymlink && linkTarget) {
|
|
670
|
+
// Mark entry as symbolic link and store target
|
|
671
|
+
entry.isSymlink = true;
|
|
672
|
+
entry.linkTarget = linkTarget;
|
|
673
|
+
// Set Unix file attributes for symbolic link (S_IFLNK | permissions)
|
|
674
|
+
const S_IFLNK = 0o120000; // Symbolic link file type
|
|
675
|
+
const permissions = stat.mode & 0o777; // Extract permission bits
|
|
676
|
+
entry.extFileAttr = (S_IFLNK | permissions) << 16;
|
|
677
|
+
// Set the uncompressed size to the length of the target path
|
|
678
|
+
entry.uncompressedSize = Buffer.byteLength(linkTarget, 'utf8');
|
|
679
|
+
if (this.options.debug) {
|
|
680
|
+
(0, utils_1.logDebug)(` Symbolic link: ${linkTarget}`, this.options);
|
|
681
|
+
(0, utils_1.logDebug)(` Link target size: ${entry.uncompressedSize} bytes`, this.options);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// Handle hard links
|
|
685
|
+
if (isHardLink && originalEntry && inode) {
|
|
686
|
+
// Mark entry as hard link and store reference to original
|
|
687
|
+
entry.isHardLink = true;
|
|
688
|
+
entry.originalEntry = originalEntry;
|
|
689
|
+
entry.inode = inode;
|
|
690
|
+
// For hard links, we don't store the file content again
|
|
691
|
+
// Set uncompressed size to 0 to indicate this is a link entry
|
|
692
|
+
entry.uncompressedSize = 0;
|
|
693
|
+
if (this.options.debug) {
|
|
694
|
+
(0, utils_1.logDebug)(` Hard link: ${entryName} -> ${originalEntry} (inode: ${inode})`, this.options);
|
|
695
|
+
(0, utils_1.logDebug)(` Link entry (no content stored)`, this.options);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Debug: Check what setDateTime actually returns
|
|
699
|
+
if (process.env.DEBUG_UPDATE) {
|
|
700
|
+
console.log(`Setting timestamp for ${entryName}:`);
|
|
701
|
+
console.log(` File mtime: ${stat.mtimeMs} (${new Date(stat.mtimeMs).toISOString()})`);
|
|
702
|
+
console.log(` timeDateDOS set to: ${Math.floor(stat.mtimeMs / 1000)}`);
|
|
703
|
+
console.log(` lastModTimeDate set to: ${Math.floor(stat.mtimeMs / 1000)}`);
|
|
704
|
+
console.log(` Extended timestamps set: ntfsTime=${!!entry.ntfsTime}, extendedTime=${!!entry.extendedTime}`);
|
|
705
|
+
}
|
|
706
|
+
entry.uncompressedSize = stat.size;
|
|
707
|
+
// Mark entry as updated so it gets processed
|
|
708
|
+
entry.isUpdated = true;
|
|
709
|
+
// Entry is automatically added to zip.zipEntries[] by createZipEntry()
|
|
710
|
+
this.entryToPath.set(entryName, absPath);
|
|
711
|
+
this.entryToDisplay.set(entryName, displayPath);
|
|
712
|
+
this.entryToStats.set(entryName, stat);
|
|
713
|
+
if (this.options.verbose)
|
|
714
|
+
(0, utils_1.log)(`Queued: ${entryName} (${stat.size} bytes)`, this.options);
|
|
715
|
+
processedFiles++;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async setupCompression() {
|
|
719
|
+
// Handle password prompting for encryption
|
|
720
|
+
if (this.options.encrypt && this.options.password === 'PROMPT') {
|
|
721
|
+
(0, utils_1.log)('🔐 Encryption enabled - password required', this.options);
|
|
722
|
+
this.options.password = await (0, user_interaction_1.promptPassword)('Enter password: ');
|
|
723
|
+
if (!this.options.password) {
|
|
724
|
+
(0, utils_1.logError)('❌ Password is required for encryption');
|
|
725
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
|
|
726
|
+
}
|
|
727
|
+
(0, utils_1.log)('✅ Password set for encryption', this.options);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async handleComments() {
|
|
731
|
+
let commentOptions = {};
|
|
732
|
+
if (this.options.commentFile) {
|
|
733
|
+
(0, utils_1.log)('📝 Reading comments from file...', this.options);
|
|
734
|
+
const commentResult = await CommentManager_1.CommentManager.readCommentsFromFile(this.options.commentFile);
|
|
735
|
+
if (commentResult.success) {
|
|
736
|
+
commentOptions = {
|
|
737
|
+
archiveComment: commentResult.archiveComment,
|
|
738
|
+
fileComments: commentResult.fileComments
|
|
739
|
+
};
|
|
740
|
+
if (commentResult.archiveComment) {
|
|
741
|
+
(0, utils_1.log)(` Archive comment: ${commentResult.archiveComment.length} characters`, this.options);
|
|
742
|
+
}
|
|
743
|
+
if (commentResult.fileComments) {
|
|
744
|
+
(0, utils_1.log)(` File comments: ${Object.keys(commentResult.fileComments).length} files`, this.options);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
(0, utils_1.logError)(`❌ Error reading comment file: ${commentResult.error}`);
|
|
749
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else if (this.options.archiveComment) {
|
|
753
|
+
commentOptions.archiveComment = this.options.archiveComment;
|
|
754
|
+
(0, utils_1.log)(`📝 Archive comment: ${this.options.archiveComment.length} characters`, this.options);
|
|
755
|
+
}
|
|
756
|
+
if (this.options.interactiveComments) {
|
|
757
|
+
(0, utils_1.log)('📝 Interactive comment mode enabled', this.options);
|
|
758
|
+
if (!commentOptions.archiveComment) {
|
|
759
|
+
commentOptions.archiveComment = await CommentManager_1.CommentManager.promptForArchiveComment();
|
|
760
|
+
}
|
|
761
|
+
const filenames = this.inputFiles.map(f => f.entryName);
|
|
762
|
+
const fileComments = await CommentManager_1.CommentManager.promptForFileComments(filenames);
|
|
763
|
+
if (Object.keys(fileComments).length > 0) {
|
|
764
|
+
commentOptions.fileComments = fileComments;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (this.options.fileCommentsOnly) {
|
|
768
|
+
(0, utils_1.log)('📝 File comment mode enabled', this.options);
|
|
769
|
+
const filenames = this.inputFiles.map(f => f.entryName);
|
|
770
|
+
const fileComments = await CommentManager_1.CommentManager.promptForFileCommentsOnly(filenames);
|
|
771
|
+
if (Object.keys(fileComments).length > 0) {
|
|
772
|
+
commentOptions.fileComments = fileComments;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// Set archive comment option (will be applied during finalization)
|
|
776
|
+
if (commentOptions.archiveComment) {
|
|
777
|
+
this.options.archiveComment = commentOptions.archiveComment;
|
|
778
|
+
(0, utils_1.log)('✅ Archive comment set (will be applied during finalization)', this.options);
|
|
779
|
+
}
|
|
780
|
+
// Apply file comments to entries
|
|
781
|
+
if (commentOptions.fileComments) {
|
|
782
|
+
const entries = await this.zip.getDirectory() || [];
|
|
783
|
+
for (const [filename, comment] of Object.entries(commentOptions.fileComments)) {
|
|
784
|
+
const entry = entries.find((e) => e.filename === filename);
|
|
785
|
+
if (entry) {
|
|
786
|
+
entry.comment = comment;
|
|
787
|
+
if (this.options.verbose)
|
|
788
|
+
(0, utils_1.log)(` File comment set: ${filename}`, this.options);
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
(0, utils_1.logError)(`❌ File not found for comment: ${filename}`);
|
|
792
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_FIND_ARCHIVE);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
(0, utils_1.log)('✅ File comments applied successfully', this.options);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async handleCompression() {
|
|
799
|
+
this.compressionOffset = await this.compressEntries();
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Unified compression method for both in-memory and file-based modes
|
|
803
|
+
* Loops through zip.zipEntries[] directly (single source of truth for entry order)
|
|
804
|
+
*/
|
|
805
|
+
async compressEntries() {
|
|
806
|
+
// Create compression options
|
|
807
|
+
const level = this.options.level ?? 6;
|
|
808
|
+
// Validate password exists when encryption is enabled
|
|
809
|
+
if (this.options.encrypt && !this.options.password) {
|
|
810
|
+
(0, utils_1.logError)('❌ Password is required for encryption');
|
|
811
|
+
throw new Error('Password is required for encryption');
|
|
812
|
+
}
|
|
813
|
+
const compressionOptions = {
|
|
814
|
+
level: level,
|
|
815
|
+
useZstd: level === 0 ? false : (this.options.compression || 'zstd') === 'zstd',
|
|
816
|
+
useSHA256: this.options.blockchain || this.options.blockchainOts || false,
|
|
817
|
+
password: this.options.encrypt ? this.options.password : null,
|
|
818
|
+
encryptionMethod: this.options.encryptionMethod || 'pkzip'
|
|
819
|
+
};
|
|
820
|
+
// Get entries from zip.getDirectory() (populated by processFiles())
|
|
821
|
+
// This is the single source of truth for entry order
|
|
822
|
+
const zipEntries = this.zip.getDirectory();
|
|
823
|
+
if (zipEntries.length === 0) {
|
|
824
|
+
(0, utils_1.log)('No files to compress', this.options);
|
|
825
|
+
return 0;
|
|
826
|
+
}
|
|
827
|
+
if (this.options.inMemory) {
|
|
828
|
+
// In-memory mode: use compressFileBuffer with BufferOutputWriter
|
|
829
|
+
(0, utils_1.log)(`🔧 Using in-memory compression (browser compatible)`, this.options);
|
|
830
|
+
(0, utils_1.log)(`🔄 Compressing ${zipEntries.length} files in memory...`, this.options);
|
|
831
|
+
// Create ZipkitServer instance for buffer-based compression
|
|
832
|
+
const zipkit = new server_1.ZipkitServer();
|
|
833
|
+
// Determine if we need to write to file (for blockchain mode)
|
|
834
|
+
const writeToFile = this.options.blockchain || this.options.blockchainOts;
|
|
835
|
+
// Create appropriate output writer
|
|
836
|
+
let writer;
|
|
837
|
+
const positionRef = { current: this.currentPosition };
|
|
838
|
+
if (writeToFile) {
|
|
839
|
+
// For blockchain mode, write directly to file
|
|
840
|
+
if (!this.zipWriter) {
|
|
841
|
+
throw new Error('ZIP writer not initialized for blockchain mode');
|
|
842
|
+
}
|
|
843
|
+
// Start from currentPosition (should be 0 for new file, or existing position for append)
|
|
844
|
+
positionRef.current = this.currentPosition;
|
|
845
|
+
writer = new FileOutputWriter(this.archiveName, this.zipWriter.outputFd, this.zipWriter.outputStream, positionRef);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
// For non-blockchain mode, collect in memory
|
|
849
|
+
writer = new BufferOutputWriter();
|
|
850
|
+
}
|
|
851
|
+
// Compress each file using SEQUENTIAL write: header -> compress -> data -> update header
|
|
852
|
+
// Loop through zip.zipEntries[] directly (entries in processing order)
|
|
853
|
+
for (const entry of zipEntries) {
|
|
854
|
+
// Skip entries that aren't marked for update (e.g., preserved entries in update mode)
|
|
855
|
+
if (!entry.isUpdated) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
// Read file data for this entry
|
|
859
|
+
const absPath = this.entryToPath.get(entry.filename);
|
|
860
|
+
if (!absPath) {
|
|
861
|
+
(0, utils_1.logError)(`❌ Cannot find source path for entry: ${entry.filename}`);
|
|
862
|
+
throw new Error(`Cannot find source path for entry: ${entry.filename}`);
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
// Read file data into memory
|
|
866
|
+
let fileData = fs.readFileSync(absPath);
|
|
867
|
+
// Apply line ending conversion if requested and file is a text file
|
|
868
|
+
if ((this.options.toCrlf || this.options.fromCrlf) && (0, utils_1.isTextFile)(absPath)) {
|
|
869
|
+
const originalSize = fileData.length;
|
|
870
|
+
const convertedBuf = (0, utils_1.convertLineEndings)(fileData, this.options);
|
|
871
|
+
fileData = Buffer.from(convertedBuf);
|
|
872
|
+
if (this.options.verbose) {
|
|
873
|
+
(0, utils_1.log)(`📝 Converted line endings: ${entry.filename} (${originalSize} -> ${fileData.length} bytes)`, this.options);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Step 1: Get header position BEFORE creating header (current file position)
|
|
877
|
+
const headerPosition = writer.getPosition();
|
|
878
|
+
entry.localHdrOffset = headerPosition;
|
|
879
|
+
// Step 2: Set compression method BEFORE creating header
|
|
880
|
+
// Determine compression method based on options
|
|
881
|
+
if (compressionOptions.level === 0) {
|
|
882
|
+
entry.cmpMethod = 0; // STORED
|
|
883
|
+
}
|
|
884
|
+
else if (compressionOptions.useZstd) {
|
|
885
|
+
entry.cmpMethod = 93; // ZSTD
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
entry.cmpMethod = 8; // DEFLATED
|
|
889
|
+
}
|
|
890
|
+
// Step 3: Create local header with placeholder compressed size (0)
|
|
891
|
+
entry.compressedSize = 0; // Placeholder - will be updated after compression
|
|
892
|
+
const localHeader = entry.createLocalHdr();
|
|
893
|
+
// Step 4: Write local header FIRST (with placeholder compressed size)
|
|
894
|
+
await writer.writeHeader(localHeader);
|
|
895
|
+
// Track entry position for central directory (position where header was written)
|
|
896
|
+
// Note: For in-memory mode, positions are tracked in bufferWriter, not zipWriter
|
|
897
|
+
if (this.zipWriter) {
|
|
898
|
+
this.zipWriter.entryPositions.set(entry.filename || '', headerPosition);
|
|
899
|
+
}
|
|
900
|
+
// Step 5: Compress data (returns ONLY compressed data, no header)
|
|
901
|
+
const compressedData = await zipkit.compressFileBuffer(entry, fileData, compressionOptions);
|
|
902
|
+
// Update entry compressed size for statistics and central directory
|
|
903
|
+
entry.compressedSize = compressedData.length;
|
|
904
|
+
// Step 6: Write compressed data
|
|
905
|
+
await writer.writeData(compressedData);
|
|
906
|
+
// Step 7: Update CRC and compressed size in local header
|
|
907
|
+
await writer.updateHeader(headerPosition, compressedData.length, entry.crc);
|
|
908
|
+
// Update statistics
|
|
909
|
+
this.totalIn += entry.uncompressedSize || 0;
|
|
910
|
+
this.totalOut += entry.compressedSize || 0;
|
|
911
|
+
this.filesCount += 1;
|
|
912
|
+
// Accumulate SHA-256 hash as entries are added (for merkle root calculation)
|
|
913
|
+
if (this.hashAccumulator && entry.sha256) {
|
|
914
|
+
const filename = entry.filename || '';
|
|
915
|
+
// Skip blockchain metadata files
|
|
916
|
+
if (filename !== src_1.TOKENIZED_METADATA &&
|
|
917
|
+
filename !== src_1.TIMESTAMP_METADATA &&
|
|
918
|
+
filename !== src_1.TIMESTAMP_SUBMITTED) {
|
|
919
|
+
const hashBuffer = Buffer.from(entry.sha256, 'hex');
|
|
920
|
+
this.hashAccumulator.addHash(hashBuffer);
|
|
921
|
+
if (this.options.verbose) {
|
|
922
|
+
(0, utils_1.logDebug)(`Added SHA-256 hash to accumulator for ${filename}`, this.options);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// Entry is already in zip.zipEntries[] from processFiles(), updated with compression properties
|
|
927
|
+
if (this.options.verbose) {
|
|
928
|
+
const ratio = entry.uncompressedSize > 0 ? Math.round((1 - entry.compressedSize / entry.uncompressedSize) * 100) : 0;
|
|
929
|
+
(0, utils_1.log)(`📦 Compressed: ${entry.filename} (${(0, utils_1.formatBytes)(entry.uncompressedSize)} -> ${(0, utils_1.formatBytes)(entry.compressedSize)} bytes, ${ratio}%)`, this.options);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
(0, utils_1.logError)(`❌ Failed to compress file ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// For non-blockchain mode, keep buffer open for metadata addition
|
|
938
|
+
// Metadata will be added before central directory in handleTokenization() and finalize()
|
|
939
|
+
if (!writeToFile) {
|
|
940
|
+
// Store buffer writer for later use (metadata addition and finalization)
|
|
941
|
+
// For in-memory non-blockchain mode, we'll write the complete ZIP file in finalize()
|
|
942
|
+
// after metadata is added
|
|
943
|
+
this.zip.bufferWriter = writer;
|
|
944
|
+
this.currentPosition = writer.getPosition();
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
// For blockchain mode, update currentPosition from FileOutputWriter
|
|
948
|
+
// The FileOutputWriter updates positionRef.current as it writes
|
|
949
|
+
// Sync this.currentPosition with the actual file position
|
|
950
|
+
this.currentPosition = positionRef.current;
|
|
951
|
+
// Note: Do NOT close the stream here - it needs to remain open
|
|
952
|
+
// so that metadata can be added and finalize() can write central directory
|
|
953
|
+
if (this.options.verbose) {
|
|
954
|
+
(0, utils_1.log)(`📊 Compression completed at file position: ${this.currentPosition}`, this.options);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return writer.getPosition();
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
// File-based mode: use ZipkitServer's writeZipEntry() method
|
|
961
|
+
// Get entries to compress from zip.getDirectory() (populated by processFiles())
|
|
962
|
+
const zipEntries = this.zip.getDirectory().length > 0 ? this.zip.getDirectory() : await this.zip.getDirectory() || [];
|
|
963
|
+
if (zipEntries.length === 0) {
|
|
964
|
+
(0, utils_1.logError)('❌ No files to compress');
|
|
965
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.NO_FILES_MATCHED);
|
|
966
|
+
}
|
|
967
|
+
if (!this.zipWriter) {
|
|
968
|
+
throw new Error('ZIP writer not initialized for file-based mode');
|
|
969
|
+
}
|
|
970
|
+
const cmpOptions = {
|
|
971
|
+
...compressionOptions,
|
|
972
|
+
bufferSize: this.options.blockSize || 512 * 1024 // Default 512KB buffer
|
|
973
|
+
};
|
|
974
|
+
if (this.options.verbose) {
|
|
975
|
+
(0, utils_1.log)(`🔧 Using file-based compression with ${(0, utils_1.formatBytes)(cmpOptions.bufferSize)} buffer`, this.options);
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
// Loop through entries and process each one
|
|
979
|
+
let entryCnt = zipEntries.length;
|
|
980
|
+
for (let index = 0; index < entryCnt; index++) {
|
|
981
|
+
const entry = zipEntries[index];
|
|
982
|
+
// Filter out metadata entries (handled separately in handleTokenization)
|
|
983
|
+
if (entry.filename === src_1.TOKENIZED_METADATA ||
|
|
984
|
+
entry.filename === src_1.TIMESTAMP_METADATA ||
|
|
985
|
+
entry.filename === src_1.TIMESTAMP_SUBMITTED) {
|
|
986
|
+
zipEntries.splice(index, 1);
|
|
987
|
+
entryCnt--;
|
|
988
|
+
index--;
|
|
989
|
+
if (this.options.verbose) {
|
|
990
|
+
(0, utils_1.log)(`Deleting Metadata entry: ${entry.filename}`, this.options);
|
|
991
|
+
}
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
// Handle non-updated entries: copy from existing ZIP
|
|
995
|
+
if (!entry.isUpdated) {
|
|
996
|
+
if (this.options.verbose) {
|
|
997
|
+
(0, utils_1.log)(`Copying Entry: ${entry.filename}`, this.options);
|
|
998
|
+
}
|
|
999
|
+
entry.localHdrOffset = this.zipWriter.currentPosition;
|
|
1000
|
+
const inData = await this.zip.copyEntry(entry);
|
|
1001
|
+
await this.writeChunk(inData);
|
|
1002
|
+
// Update entry position tracking
|
|
1003
|
+
this.zipWriter.entryPositions.set(entry.filename || '', entry.localHdrOffset);
|
|
1004
|
+
// Update statistics
|
|
1005
|
+
this.totalIn += entry.uncompressedSize || 0;
|
|
1006
|
+
this.totalOut += entry.compressedSize || entry.uncompressedSize || 0;
|
|
1007
|
+
this.filesCount += 1;
|
|
1008
|
+
if (this.options.verbose) {
|
|
1009
|
+
const actionPath = this.entryToDisplay.get(entry.filename) || entry.filename;
|
|
1010
|
+
(0, utils_1.log)(`adding: ${actionPath} (copied, no recompression)`, this.options);
|
|
1011
|
+
}
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
// Handle updated entries: use writeZipEntry() for compression
|
|
1015
|
+
const filePath = this.entryToPath.get(entry.filename);
|
|
1016
|
+
if (!filePath) {
|
|
1017
|
+
throw new Error(`Cannot find source path for entry: ${entry.filename}`);
|
|
1018
|
+
}
|
|
1019
|
+
// Check for pre-loaded buffer (symlinks, hardlinks, line-ending conversion)
|
|
1020
|
+
// If we have a pre-loaded buffer, we need to compress it directly
|
|
1021
|
+
let fileBuffer = null;
|
|
1022
|
+
if (entry.fileBuffer) {
|
|
1023
|
+
fileBuffer = entry.fileBuffer;
|
|
1024
|
+
}
|
|
1025
|
+
// Use writeZipEntry() for regular file compression
|
|
1026
|
+
// For pre-loaded buffers, we'll need to handle them specially
|
|
1027
|
+
if (fileBuffer) {
|
|
1028
|
+
// For pre-loaded buffers, compress in memory and write manually
|
|
1029
|
+
// This handles symlinks, hardlinks, and line-ending conversion
|
|
1030
|
+
const zipkit = new server_1.ZipkitServer();
|
|
1031
|
+
const cmpData = await zipkit.compressData(entry, fileBuffer, cmpOptions);
|
|
1032
|
+
// Write header manually
|
|
1033
|
+
entry.compressedSize = 0; // Placeholder
|
|
1034
|
+
entry.localHdrOffset = this.zipWriter.currentPosition;
|
|
1035
|
+
const localHdr = entry.createLocalHdr();
|
|
1036
|
+
await this.writeChunk(localHdr);
|
|
1037
|
+
this.zipWriter.entryPositions.set(entry.filename || '', entry.localHdrOffset);
|
|
1038
|
+
// Write compressed data
|
|
1039
|
+
await this.writeChunk(cmpData);
|
|
1040
|
+
// Update header with actual compressed size
|
|
1041
|
+
entry.compressedSize = cmpData.length;
|
|
1042
|
+
const compressedSizeOffset = entry.localHdrOffset + 18;
|
|
1043
|
+
const sizeBuffer = Buffer.alloc(4);
|
|
1044
|
+
sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
|
|
1045
|
+
fs.writeSync(this.zipWriter.outputFd, sizeBuffer, 0, 4, compressedSizeOffset);
|
|
1046
|
+
if (entry.crc !== undefined) {
|
|
1047
|
+
const crcOffset = entry.localHdrOffset + 14;
|
|
1048
|
+
const crcBuffer = Buffer.alloc(4);
|
|
1049
|
+
crcBuffer.writeUInt32LE(entry.crc, 0);
|
|
1050
|
+
fs.writeSync(this.zipWriter.outputFd, crcBuffer, 0, 4, crcOffset);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
// Use writeZipEntry() for regular file compression
|
|
1055
|
+
await this.zip.writeZipEntry(this.zipWriter, entry, filePath, cmpOptions, {
|
|
1056
|
+
onProgress: (entry, bytes) => {
|
|
1057
|
+
// Progress tracking if needed
|
|
1058
|
+
},
|
|
1059
|
+
onHashCalculated: (entry, hash) => {
|
|
1060
|
+
// Accumulate SHA-256 hash for blockchain
|
|
1061
|
+
if (this.hashAccumulator) {
|
|
1062
|
+
const filename = entry.filename || '';
|
|
1063
|
+
// Skip blockchain metadata files
|
|
1064
|
+
if (filename !== src_1.TOKENIZED_METADATA &&
|
|
1065
|
+
filename !== src_1.TIMESTAMP_METADATA &&
|
|
1066
|
+
filename !== src_1.TIMESTAMP_SUBMITTED) {
|
|
1067
|
+
this.hashAccumulator.addHash(hash);
|
|
1068
|
+
if (this.options.verbose) {
|
|
1069
|
+
(0, utils_1.logDebug)(`Added SHA-256 hash to accumulator for ${filename}`, this.options);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
// Update currentPosition from zipWriter
|
|
1077
|
+
this.currentPosition = this.zipWriter.currentPosition;
|
|
1078
|
+
// Update statistics
|
|
1079
|
+
this.totalIn += entry.uncompressedSize || 0;
|
|
1080
|
+
this.totalOut += entry.compressedSize || 0;
|
|
1081
|
+
this.filesCount += 1;
|
|
1082
|
+
// Log progress
|
|
1083
|
+
const compressed = entry.uncompressedSize && entry.uncompressedSize > 0
|
|
1084
|
+
? Math.max(0, Math.round((1 - (entry.compressedSize / entry.uncompressedSize)) * 100))
|
|
1085
|
+
: 0;
|
|
1086
|
+
const method = entry.cmpMethod === 0 ? 'stored' : (entry.cmpMethod === 93 ? 'zstd' : 'deflated');
|
|
1087
|
+
const actionPath = this.entryToDisplay.get(entry.filename) || entry.filename;
|
|
1088
|
+
(0, utils_1.log)(`adding: ${actionPath} (${method} ${compressed}%)`, this.options);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
finally {
|
|
1092
|
+
// Always cleanup file descriptors
|
|
1093
|
+
await this.cleanupFileDescriptors();
|
|
1094
|
+
}
|
|
1095
|
+
if (this.options.verbose) {
|
|
1096
|
+
(0, utils_1.log)(`📊 Compression completed at file position: ${this.currentPosition}`, this.options);
|
|
1097
|
+
}
|
|
1098
|
+
// Return the current file position
|
|
1099
|
+
return this.currentPosition;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async addTokenMetadataDirectly(tokenMeta) {
|
|
1103
|
+
// Create token metadata JSON
|
|
1104
|
+
const tokenContent = JSON.stringify(tokenMeta, null, 2);
|
|
1105
|
+
const tokenBuffer = Buffer.from(tokenContent, 'utf8');
|
|
1106
|
+
const tokenPath = 'META-INF/NZIP.TOKEN';
|
|
1107
|
+
console.log('✅ Token metadata prepared for ZIP');
|
|
1108
|
+
console.log(`📄 Token content:\n${tokenContent}`);
|
|
1109
|
+
// Create local file header for token metadata
|
|
1110
|
+
const tokenEntry = this.zip.createZipEntry(tokenPath);
|
|
1111
|
+
tokenEntry.timeDateDOS = tokenEntry.setDateTime(new Date());
|
|
1112
|
+
tokenEntry.compressedSize = tokenBuffer.length;
|
|
1113
|
+
tokenEntry.uncompressedSize = tokenBuffer.length;
|
|
1114
|
+
tokenEntry.cmpMethod = 0; // STORED (no compression for small metadata file)
|
|
1115
|
+
tokenEntry.crc = (0, src_1.crc32)(tokenBuffer);
|
|
1116
|
+
// Use current file position for local header offset (not compressionOffset)
|
|
1117
|
+
tokenEntry.localHdrOffset = this.currentPosition;
|
|
1118
|
+
// Track entry position for central directory
|
|
1119
|
+
if (this.zipWriter) {
|
|
1120
|
+
this.zipWriter.entryPositions.set(tokenPath, this.currentPosition);
|
|
1121
|
+
}
|
|
1122
|
+
// Create local header
|
|
1123
|
+
const localHdr = tokenEntry.createLocalHdr();
|
|
1124
|
+
// Write token metadata directly to file (at the end, just before central directory)
|
|
1125
|
+
await this.writeChunk(localHdr);
|
|
1126
|
+
await this.writeChunk(tokenBuffer);
|
|
1127
|
+
// Token entry is automatically added to zip.zipEntries[] by createZipEntry()
|
|
1128
|
+
// Since it's added last, it will appear last in the central directory
|
|
1129
|
+
console.log(`✅ Token metadata added to ZIP (${tokenBuffer.length} bytes)`);
|
|
1130
|
+
return tokenEntry;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Calculate merkle root from ZIP entries with SHA-256 hashes
|
|
1134
|
+
* Excludes blockchain metadata files (META-INF/*) to ensure consistent calculation
|
|
1135
|
+
*/
|
|
1136
|
+
calculateMerkleRootFromEntries(entries) {
|
|
1137
|
+
if (!entries || entries.length === 0) {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
const hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
|
|
1141
|
+
// Filter out blockchain metadata files to ensure consistent Merkle Root calculation
|
|
1142
|
+
const contentEntries = entries.filter(entry => {
|
|
1143
|
+
const filename = entry.filename || '';
|
|
1144
|
+
return filename !== src_1.TOKENIZED_METADATA &&
|
|
1145
|
+
filename !== src_1.TIMESTAMP_METADATA &&
|
|
1146
|
+
filename !== src_1.TIMESTAMP_SUBMITTED;
|
|
1147
|
+
});
|
|
1148
|
+
// Add SHA-256 hashes to accumulator
|
|
1149
|
+
for (const entry of contentEntries) {
|
|
1150
|
+
if (entry.sha256) {
|
|
1151
|
+
// Convert hex string to Buffer
|
|
1152
|
+
const hashBuffer = Buffer.from(entry.sha256, 'hex');
|
|
1153
|
+
hashAccumulator.addHash(hashBuffer);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const merkleRoot = hashAccumulator.merkleRoot();
|
|
1157
|
+
return merkleRoot;
|
|
1158
|
+
}
|
|
1159
|
+
async handleTokenization() {
|
|
1160
|
+
let tokenMeta = null;
|
|
1161
|
+
let tokenInfoEntry = null;
|
|
1162
|
+
let otsMetaEntry = null;
|
|
1163
|
+
let merkleRoot = null;
|
|
1164
|
+
if (this.options.blockchain) {
|
|
1165
|
+
// Check for wallet passkey AFTER compression is complete
|
|
1166
|
+
// Priority: -w option > wallet.json > environment variable
|
|
1167
|
+
// Don't show error yet - we'll prompt user if key is missing
|
|
1168
|
+
const walletPasskey = (0, blockchain_2.getWalletPasskey)(this.options.walletKey, false);
|
|
1169
|
+
if (!walletPasskey) {
|
|
1170
|
+
// Show error message and prompt user
|
|
1171
|
+
console.error('\n❌ Error: Wallet private key is required for blockchain operations');
|
|
1172
|
+
console.error('');
|
|
1173
|
+
console.error(' Option 1 (Recommended): Run interactive setup');
|
|
1174
|
+
console.error(' $ neozip init');
|
|
1175
|
+
console.error('');
|
|
1176
|
+
console.error(' Option 2: Use command-line flag');
|
|
1177
|
+
console.error(' $ neozip -b -w 0x... <archive> <files>');
|
|
1178
|
+
console.error('');
|
|
1179
|
+
console.error(' Option 3: Set environment variable');
|
|
1180
|
+
console.error(' $ export NEOZIP_WALLET_PASSKEY="0x..."');
|
|
1181
|
+
console.error('');
|
|
1182
|
+
console.error(' Get testnet ETH from: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet');
|
|
1183
|
+
console.error('');
|
|
1184
|
+
// Prompt user to continue without blockchain tokenization
|
|
1185
|
+
if (!this.options.nonInteractive) {
|
|
1186
|
+
const answer = await (0, user_interaction_1.promptUser)('Continue without blockchain tokenization? (y/n): ');
|
|
1187
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
1188
|
+
(0, utils_1.log)('⚠️ Continuing without blockchain tokenization (ZIP file is already created)...', this.options);
|
|
1189
|
+
this.options.blockchain = false;
|
|
1190
|
+
this.options.blockchainOts = false;
|
|
1191
|
+
return; // Skip tokenization, ZIP is already complete
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
(0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
|
|
1195
|
+
(0, utils_1.logError)('💡 Tip: Omit blockchain flags to create ZIP without blockchain operations');
|
|
1196
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
// Non-interactive mode - exit with error
|
|
1201
|
+
(0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
|
|
1202
|
+
(0, utils_1.logError)('💡 Tip: Omit blockchain flags to create ZIP without blockchain operations');
|
|
1203
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
// Wallet key is available, store it for use
|
|
1208
|
+
this.options.walletPasskey = walletPasskey;
|
|
1209
|
+
}
|
|
1210
|
+
if (this.options.debug) {
|
|
1211
|
+
(0, utils_1.logDebug)('Tokenization process starting...', this.options);
|
|
1212
|
+
}
|
|
1213
|
+
// Calculate merkle root from accumulated hashes (if available) or from entries
|
|
1214
|
+
if (this.hashAccumulator && this.hashAccumulator.leafCount() > 0) {
|
|
1215
|
+
// Use accumulated hashes (calculated incrementally during compression)
|
|
1216
|
+
merkleRoot = this.hashAccumulator.merkleRoot();
|
|
1217
|
+
if (this.options.verbose) {
|
|
1218
|
+
console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
// Fallback: Calculate from entries (for updates or in-memory mode)
|
|
1223
|
+
let entriesForMerkle = [];
|
|
1224
|
+
const zipEntries = this.zip.getDirectory();
|
|
1225
|
+
if (zipEntries.length > 0) {
|
|
1226
|
+
// New ZIP creation (file-based) - use entries we created (they have SHA-256 hashes from compression)
|
|
1227
|
+
entriesForMerkle = zipEntries;
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
// Update mode OR in-memory mode - load ZIP file and get entries
|
|
1231
|
+
// For in-memory mode, the ZIP file was already written to disk by createZipInMemory()
|
|
1232
|
+
try {
|
|
1233
|
+
if (fs.existsSync(this.archiveName)) {
|
|
1234
|
+
// Load the ZIP file to get entries with SHA-256 hashes
|
|
1235
|
+
const zipkit = new server_1.ZipkitServer();
|
|
1236
|
+
if (this.options.inMemory) {
|
|
1237
|
+
// For in-memory mode, load as buffer
|
|
1238
|
+
const zipBuffer = fs.readFileSync(this.archiveName);
|
|
1239
|
+
zipkit.loadZip(zipBuffer);
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
// For file-based mode
|
|
1243
|
+
await zipkit.loadZipFile(this.archiveName);
|
|
1244
|
+
}
|
|
1245
|
+
entriesForMerkle = zipkit.getDirectory() || [];
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
// Update mode - get entries from loaded ZIP
|
|
1249
|
+
entriesForMerkle = await this.zip.getDirectory() || [];
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
console.error('❌ Error loading ZIP file for merkle root calculation:', error instanceof Error ? error.message : String(error));
|
|
1254
|
+
// Fallback to getting entries from this.zip
|
|
1255
|
+
entriesForMerkle = await this.zip.getDirectory() || [];
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
// Calculate merkle root from entries
|
|
1259
|
+
merkleRoot = this.calculateMerkleRootFromEntries(entriesForMerkle);
|
|
1260
|
+
}
|
|
1261
|
+
if (!merkleRoot) {
|
|
1262
|
+
console.error('❌ Error: Could not calculate merkle root for tokenization');
|
|
1263
|
+
console.error(' This is required for blockchain operations.');
|
|
1264
|
+
console.error(' Make sure files have been compressed with SHA-256 hashes.');
|
|
1265
|
+
if (this.options.verbose) {
|
|
1266
|
+
const hashCount = this.hashAccumulator ? this.hashAccumulator.leafCount() : 0;
|
|
1267
|
+
console.error(` Found ${hashCount} accumulated SHA-256 hashes`);
|
|
1268
|
+
}
|
|
1269
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT);
|
|
1270
|
+
}
|
|
1271
|
+
if (this.options.verbose && this.hashAccumulator) {
|
|
1272
|
+
console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
|
|
1273
|
+
}
|
|
1274
|
+
if (merkleRoot) {
|
|
1275
|
+
console.log(`\n📋 Merkle Root: ${merkleRoot}`);
|
|
1276
|
+
// Validate network
|
|
1277
|
+
const network = this.options.network || 'base-sepolia';
|
|
1278
|
+
// Always display network when tokenization is enabled
|
|
1279
|
+
console.log(`🌐 Network: ${network}`);
|
|
1280
|
+
if (!(0, blockchain_2.isSupportedNetwork)(network)) {
|
|
1281
|
+
const supportedNetworks = (0, contracts_1.getSupportedNetworkNames)();
|
|
1282
|
+
console.error(`❌ Error: Unsupported network: ${network}`);
|
|
1283
|
+
console.error(` Currently supported networks: ${supportedNetworks.join(', ')}`);
|
|
1284
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
|
|
1285
|
+
}
|
|
1286
|
+
let minter = null;
|
|
1287
|
+
try {
|
|
1288
|
+
// Initialize blockchain minter
|
|
1289
|
+
minter = new src_1.ZipkitMinter(merkleRoot, {
|
|
1290
|
+
network: network,
|
|
1291
|
+
walletPrivateKey: this.options.walletPasskey,
|
|
1292
|
+
verbose: this.options.verbose,
|
|
1293
|
+
debug: this.options.verbose || this.options.debug || process.env.NEOZIP_DEBUG === 'true' // Enable debug output
|
|
1294
|
+
});
|
|
1295
|
+
// Start the minting process (checks duplicates, gets wallet info, estimates gas)
|
|
1296
|
+
console.log('🔗 Connecting to blockchain network...');
|
|
1297
|
+
const processResult = await Promise.race([
|
|
1298
|
+
minter.processMinting(),
|
|
1299
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Network connection timeout after 30 seconds. The RPC endpoint may be slow or unreachable.')), 30000))
|
|
1300
|
+
]);
|
|
1301
|
+
console.log(`💳 Wallet: ${processResult.walletInfo.address}`);
|
|
1302
|
+
console.log(`💰 Balance: ${processResult.walletInfo.balance} ETH`);
|
|
1303
|
+
// Check if tokens already exist for this merkle root
|
|
1304
|
+
if (processResult.duplicateCheck.hasExistingTokens) {
|
|
1305
|
+
let userChoice;
|
|
1306
|
+
if (this.options.blockchainMint) {
|
|
1307
|
+
// Force mint new token
|
|
1308
|
+
console.log('\n🔨 Force minting new token (--blockchain-mint)');
|
|
1309
|
+
userChoice = { action: 'mint-new' };
|
|
1310
|
+
}
|
|
1311
|
+
else if (this.options.blockchainDefault) {
|
|
1312
|
+
// Auto-select default option
|
|
1313
|
+
if (processResult.duplicateCheck.userOwnedTokens.length > 0) {
|
|
1314
|
+
// Use the earliest token owned by user
|
|
1315
|
+
const earliestToken = processResult.duplicateCheck.userOwnedTokens[0];
|
|
1316
|
+
console.log(`\n✅ Auto-selecting existing token ID ${earliestToken.tokenId} (--blockchain-default)`);
|
|
1317
|
+
userChoice = { action: 'use-existing', selectedToken: earliestToken };
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
// No user-owned tokens, mint new
|
|
1321
|
+
console.log('\n🔨 No user-owned tokens found, minting new token (--blockchain-default)');
|
|
1322
|
+
userChoice = { action: 'mint-new' };
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
// Present user with choices (interactive mode)
|
|
1327
|
+
userChoice = await (0, user_interaction_1.getUserTokenChoice)(processResult.duplicateCheck);
|
|
1328
|
+
}
|
|
1329
|
+
if (userChoice.action === 'use-existing' && userChoice.selectedToken) {
|
|
1330
|
+
console.log(`\n✅ Using existing token ID ${userChoice.selectedToken.tokenId}`);
|
|
1331
|
+
// Create TokenMetadata from selected existing token
|
|
1332
|
+
// Use a fixed timestamp for existing tokens to ensure consistency
|
|
1333
|
+
// Get network config using nameAliases from CONTRACT_CONFIGS
|
|
1334
|
+
const networkConfig = (0, contracts_1.getNetworkByName)(network);
|
|
1335
|
+
let contractAddress;
|
|
1336
|
+
let networkChainId;
|
|
1337
|
+
if (networkConfig) {
|
|
1338
|
+
contractAddress = networkConfig.address;
|
|
1339
|
+
networkChainId = networkConfig.chainId;
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
// Fallback to default (Base Sepolia)
|
|
1343
|
+
const defaultConfig = (0, contracts_1.getContractConfig)(84532);
|
|
1344
|
+
contractAddress = defaultConfig.address;
|
|
1345
|
+
networkChainId = defaultConfig.chainId;
|
|
1346
|
+
}
|
|
1347
|
+
tokenMeta = {
|
|
1348
|
+
version: '1.0',
|
|
1349
|
+
tokenId: userChoice.selectedToken.tokenId,
|
|
1350
|
+
contractAddress: contractAddress,
|
|
1351
|
+
network: processResult.walletInfo.networkName,
|
|
1352
|
+
networkChainId: networkChainId,
|
|
1353
|
+
transactionHash: userChoice.selectedToken.tokenData?.transactionHash || '',
|
|
1354
|
+
merkleRoot: merkleRoot,
|
|
1355
|
+
mintedAt: '2025-01-01T00:00:00.000Z' // Fixed timestamp for existing tokens
|
|
1356
|
+
};
|
|
1357
|
+
// Add token metadata to ZIP (after compression, at the end)
|
|
1358
|
+
console.log('\n📄 Adding token metadata to ZIP...');
|
|
1359
|
+
if (tokenMeta) {
|
|
1360
|
+
tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
|
|
1361
|
+
// Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
|
|
1362
|
+
}
|
|
1363
|
+
// Cleanup provider to allow process to exit
|
|
1364
|
+
minter.destroy();
|
|
1365
|
+
}
|
|
1366
|
+
else if (userChoice.action === 'mint-new') {
|
|
1367
|
+
// Mint new token
|
|
1368
|
+
const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, this.zip, this.options.nonInteractive || false, 0);
|
|
1369
|
+
if (mintResult.success && mintResult.tokenInfo) {
|
|
1370
|
+
tokenMeta = mintResult.tokenInfo;
|
|
1371
|
+
// Add token metadata directly to ZIP (after compression, at the end)
|
|
1372
|
+
console.log('\n📄 Adding token metadata to ZIP...');
|
|
1373
|
+
tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
|
|
1374
|
+
// Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
console.error('❌ Failed to mint new token');
|
|
1378
|
+
console.error('⚠️ Continuing with ZIP creation without token metadata...');
|
|
1379
|
+
tokenMeta = null;
|
|
1380
|
+
tokenInfoEntry = null;
|
|
1381
|
+
}
|
|
1382
|
+
// Cleanup provider to allow process to exit
|
|
1383
|
+
minter.destroy();
|
|
1384
|
+
}
|
|
1385
|
+
else if (userChoice.action === 'skip-tokenization') {
|
|
1386
|
+
// Skip tokenization - create ZIP without token metadata
|
|
1387
|
+
console.log('\n⏭️ Skipping tokenization - creating ZIP without token metadata');
|
|
1388
|
+
tokenMeta = null;
|
|
1389
|
+
tokenInfoEntry = null;
|
|
1390
|
+
// Cleanup provider to allow process to exit
|
|
1391
|
+
if (minter) {
|
|
1392
|
+
minter.destroy();
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
console.log('❌ Operation cancelled by user');
|
|
1397
|
+
if (minter) {
|
|
1398
|
+
minter.destroy();
|
|
1399
|
+
}
|
|
1400
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.TOKEN_MINT_USER_CANCELLED);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
// No existing tokens, mint new one
|
|
1405
|
+
const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, this.zip, this.options.nonInteractive || false, 0);
|
|
1406
|
+
if (mintResult.success && mintResult.tokenInfo) {
|
|
1407
|
+
tokenMeta = mintResult.tokenInfo;
|
|
1408
|
+
// Add token metadata directly to ZIP (after compression, at the end)
|
|
1409
|
+
console.log('\n📄 Adding token metadata to ZIP...');
|
|
1410
|
+
tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
|
|
1411
|
+
// Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
console.error('❌ Failed to mint new token');
|
|
1415
|
+
console.error('⚠️ Continuing with ZIP creation without token metadata...');
|
|
1416
|
+
tokenMeta = null;
|
|
1417
|
+
tokenInfoEntry = null;
|
|
1418
|
+
}
|
|
1419
|
+
// Cleanup provider to allow process to exit
|
|
1420
|
+
minter.destroy();
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
catch (error) {
|
|
1424
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1425
|
+
console.error(`❌ Blockchain error: ${errorMessage}`);
|
|
1426
|
+
// Provide helpful guidance for timeout errors
|
|
1427
|
+
if (errorMessage.includes('timeout')) {
|
|
1428
|
+
console.error('');
|
|
1429
|
+
console.error('💡 Troubleshooting tips:');
|
|
1430
|
+
console.error(' 1. Check your internet connection');
|
|
1431
|
+
console.error(' 2. The RPC endpoint may be slow or unreachable');
|
|
1432
|
+
console.error(' 3. Try using a different network or RPC endpoint');
|
|
1433
|
+
console.error(' 4. Use --verbose or --debug for more details');
|
|
1434
|
+
console.error('');
|
|
1435
|
+
}
|
|
1436
|
+
console.error('⚠️ Continuing with ZIP creation without token metadata...');
|
|
1437
|
+
tokenMeta = null;
|
|
1438
|
+
tokenInfoEntry = null;
|
|
1439
|
+
}
|
|
1440
|
+
finally {
|
|
1441
|
+
// Always cleanup provider to allow process to exit, even on error
|
|
1442
|
+
// Note: Some paths already call destroy() above, but this ensures cleanup on all paths
|
|
1443
|
+
if (minter) {
|
|
1444
|
+
try {
|
|
1445
|
+
minter.destroy();
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
// Ignore errors during cleanup
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
else if (this.options.blockchainOts) {
|
|
1455
|
+
// Handle OpenTimestamp for Bitcoin blockchain
|
|
1456
|
+
if (this.options.debug) {
|
|
1457
|
+
(0, utils_1.logDebug)('OpenTimestamp process starting...', this.options);
|
|
1458
|
+
}
|
|
1459
|
+
console.log('🔗 OpenTimestamp: Creating timestamp proof on Bitcoin blockchain...');
|
|
1460
|
+
// Calculate merkle root for OpenTimestamp from accumulated hashes (if available) or from entries
|
|
1461
|
+
let merkleRoot = null;
|
|
1462
|
+
if (this.hashAccumulator && this.hashAccumulator.leafCount() > 0) {
|
|
1463
|
+
// Use accumulated hashes (calculated incrementally during compression)
|
|
1464
|
+
merkleRoot = this.hashAccumulator.merkleRoot();
|
|
1465
|
+
if (this.options.verbose) {
|
|
1466
|
+
console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
// Fallback: Calculate from entries (for updates or in-memory mode)
|
|
1471
|
+
let entriesForMerkle = [];
|
|
1472
|
+
const zipEntries = this.zip.getDirectory();
|
|
1473
|
+
if (zipEntries.length > 0) {
|
|
1474
|
+
// New ZIP creation (file-based) - use entries we created (they have SHA-256 hashes from compression)
|
|
1475
|
+
entriesForMerkle = zipEntries;
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
// Update mode OR in-memory mode - load ZIP file and get entries
|
|
1479
|
+
// For in-memory mode, the ZIP file was already written to disk by createZipInMemory()
|
|
1480
|
+
try {
|
|
1481
|
+
if (fs.existsSync(this.archiveName)) {
|
|
1482
|
+
// Load the ZIP file to get entries with SHA-256 hashes
|
|
1483
|
+
const zipkit = new server_1.ZipkitServer();
|
|
1484
|
+
if (this.options.inMemory) {
|
|
1485
|
+
// For in-memory mode, load as buffer
|
|
1486
|
+
const zipBuffer = fs.readFileSync(this.archiveName);
|
|
1487
|
+
zipkit.loadZip(zipBuffer);
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
// For file-based mode
|
|
1491
|
+
await zipkit.loadZipFile(this.archiveName);
|
|
1492
|
+
}
|
|
1493
|
+
entriesForMerkle = zipkit.getDirectory() || [];
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
// Update mode - get entries from loaded ZIP
|
|
1497
|
+
entriesForMerkle = await this.zip.getDirectory() || [];
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
catch (error) {
|
|
1501
|
+
console.error('❌ Error loading ZIP file for merkle root calculation:', error instanceof Error ? error.message : String(error));
|
|
1502
|
+
// Fallback to getting entries from this.zip
|
|
1503
|
+
entriesForMerkle = await this.zip.getDirectory() || [];
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
// Calculate merkle root from entries
|
|
1507
|
+
merkleRoot = this.calculateMerkleRootFromEntries(entriesForMerkle);
|
|
1508
|
+
}
|
|
1509
|
+
if (!merkleRoot) {
|
|
1510
|
+
console.error('❌ Error calculating merkle root for OpenTimestamp');
|
|
1511
|
+
console.error(' Make sure files have been compressed with SHA-256 hashes.');
|
|
1512
|
+
if (this.options.verbose) {
|
|
1513
|
+
const hashCount = this.hashAccumulator ? this.hashAccumulator.leafCount() : 0;
|
|
1514
|
+
console.error(` Found ${hashCount} accumulated SHA-256 hashes`);
|
|
1515
|
+
}
|
|
1516
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT);
|
|
1517
|
+
}
|
|
1518
|
+
if (this.options.verbose && this.hashAccumulator) {
|
|
1519
|
+
console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
|
|
1520
|
+
}
|
|
1521
|
+
if (merkleRoot) {
|
|
1522
|
+
console.log(`📋 Merkle Root: ${merkleRoot}`);
|
|
1523
|
+
// Create actual OpenTimestamp proof
|
|
1524
|
+
try {
|
|
1525
|
+
const otsProof = await (0, src_1.createTimestamp)(merkleRoot, { debug: this.options.verbose });
|
|
1526
|
+
if (!otsProof) {
|
|
1527
|
+
throw new Error('Failed to create OpenTimestamp proof');
|
|
1528
|
+
}
|
|
1529
|
+
console.log('📄 OpenTimestamp metadata prepared');
|
|
1530
|
+
// Use the createOtsMetadataEntry function like in the example
|
|
1531
|
+
const metaEntry = (0, blockchain_1.createOtsMetadataEntry)(this.zip, otsProof);
|
|
1532
|
+
if (metaEntry) {
|
|
1533
|
+
// Ensure the filename is set correctly
|
|
1534
|
+
metaEntry.filename = src_1.TIMESTAMP_SUBMITTED;
|
|
1535
|
+
// Ensure OTS entry uses STORED compression (no compression for small metadata file)
|
|
1536
|
+
metaEntry.cmpMethod = 0; // STORED
|
|
1537
|
+
metaEntry.compressedSize = metaEntry.fileBuffer?.length || metaEntry.uncompressedSize || 0;
|
|
1538
|
+
if (this.options.verbose) {
|
|
1539
|
+
console.log(`📂 Final timestamp entry: ${metaEntry.filename}`);
|
|
1540
|
+
console.log(`📏 Final timestamp entry size: ${metaEntry.fileBuffer?.length || metaEntry.uncompressedSize || 0} bytes`);
|
|
1541
|
+
}
|
|
1542
|
+
console.log('✅ OpenTimestamp proof created and queued for inclusion');
|
|
1543
|
+
// Use current file position for local header offset
|
|
1544
|
+
metaEntry.localHdrOffset = this.currentPosition;
|
|
1545
|
+
// Record the position for this entry
|
|
1546
|
+
if (this.zipWriter) {
|
|
1547
|
+
this.zipWriter.entryPositions.set(metaEntry.filename, this.currentPosition);
|
|
1548
|
+
}
|
|
1549
|
+
// Create local header (must be done after setting all entry properties)
|
|
1550
|
+
const localHdr = metaEntry.createLocalHdr();
|
|
1551
|
+
if (this.options.verbose) {
|
|
1552
|
+
(0, utils_1.log)(`📝 Writing OTS entry: ${metaEntry.filename} at offset ${this.currentPosition}`, this.options);
|
|
1553
|
+
(0, utils_1.log)(`📊 OTS entry size: ${metaEntry.fileBuffer?.length || 0} bytes`, this.options);
|
|
1554
|
+
}
|
|
1555
|
+
// Write local header and file data
|
|
1556
|
+
await this.writeChunk(localHdr);
|
|
1557
|
+
if (metaEntry.fileBuffer) {
|
|
1558
|
+
await this.writeChunk(metaEntry.fileBuffer);
|
|
1559
|
+
}
|
|
1560
|
+
// OTS entry is automatically added to zip.zipEntries[] by createZipEntry()
|
|
1561
|
+
// Store the metadata entry
|
|
1562
|
+
otsMetaEntry = metaEntry;
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
console.warn('⚠️ Warning: Failed to create timestamp metadata entry');
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
console.error('❌ Failed to create OpenTimestamp proof:', error instanceof Error ? error.message : String(error));
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
async finalize() {
|
|
1576
|
+
// Check if this is in-memory non-blockchain mode (needs special handling)
|
|
1577
|
+
if (this.options.inMemory && !this.options.blockchain && !this.options.blockchainOts) {
|
|
1578
|
+
// For in-memory non-blockchain mode, write complete ZIP file from buffer
|
|
1579
|
+
// Metadata should already be in the buffer (added via writeChunk() in handleTokenization())
|
|
1580
|
+
const bufferWriter = this.zip.bufferWriter;
|
|
1581
|
+
if (bufferWriter) {
|
|
1582
|
+
const zipData = bufferWriter.getResult();
|
|
1583
|
+
if (zipData) {
|
|
1584
|
+
// Create central directory from zip.getDirectory() in the SAME order as data section
|
|
1585
|
+
// Files are written first during compression, then token metadata is added
|
|
1586
|
+
// So getDirectory() is already in the correct order (data section order)
|
|
1587
|
+
const zipEntries = this.zip.getDirectory();
|
|
1588
|
+
const centralDirChunks = [];
|
|
1589
|
+
for (const entry of zipEntries) {
|
|
1590
|
+
const centralDirEntry = entry.centralDirEntry();
|
|
1591
|
+
centralDirChunks.push(centralDirEntry);
|
|
1592
|
+
}
|
|
1593
|
+
const centralDir = Buffer.concat(centralDirChunks);
|
|
1594
|
+
// Create end of central directory record
|
|
1595
|
+
const endOfCentralDir = createEndOfCentralDirRecord(zipEntries.length, centralDir.length, zipData.length, this.options.archiveComment || '');
|
|
1596
|
+
// Combine everything: ZIP data (includes metadata) + Central Directory + End of Central Directory
|
|
1597
|
+
const completeZip = Buffer.concat([zipData, centralDir, endOfCentralDir]);
|
|
1598
|
+
// Write ZIP file to temporary file first (for atomic operation)
|
|
1599
|
+
if (!this.tempArchiveName) {
|
|
1600
|
+
throw new Error('Temp archive name not initialized');
|
|
1601
|
+
}
|
|
1602
|
+
fs.writeFileSync(this.tempArchiveName, completeZip);
|
|
1603
|
+
// Atomically move temp file to final location
|
|
1604
|
+
fs.renameSync(this.tempArchiveName, this.archiveName);
|
|
1605
|
+
// Apply old timestamp if requested
|
|
1606
|
+
if (this.options.oldTimestamp) {
|
|
1607
|
+
try {
|
|
1608
|
+
const oldestTime = await (0, file_operations_1.findOldestEntryTime)(this.zip);
|
|
1609
|
+
if (oldestTime) {
|
|
1610
|
+
fs.utimesSync(this.archiveName, oldestTime, oldestTime);
|
|
1611
|
+
if (this.options.verbose) {
|
|
1612
|
+
(0, utils_1.log)(`📅 Set archive timestamp to oldest entry: ${oldestTime.toISOString()}`, this.options);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
catch (error) {
|
|
1617
|
+
if (this.options.verbose) {
|
|
1618
|
+
(0, utils_1.log)(`⚠️ Warning: Could not set old timestamp: ${error instanceof Error ? error.message : String(error)}`, this.options);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
// Clean up temp file (should already be moved, but ensure it's gone)
|
|
1623
|
+
if (fs.existsSync(this.tempArchiveName)) {
|
|
1624
|
+
try {
|
|
1625
|
+
fs.unlinkSync(this.tempArchiveName);
|
|
1626
|
+
}
|
|
1627
|
+
catch {
|
|
1628
|
+
// Ignore cleanup errors
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
// Done - no need to close stream for in-memory mode
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
// For file-based mode or in-memory blockchain mode, write central directory to file
|
|
1637
|
+
// Use ZipkitServer's writeCentralDirectory() and writeEndOfCentralDirectory() methods
|
|
1638
|
+
if (!this.zipWriter) {
|
|
1639
|
+
throw new Error('ZIP writer not initialized for finalization');
|
|
1640
|
+
}
|
|
1641
|
+
// Get entries - for new ZIPs, use zip.getDirectory(); for updates, use getDirectory()
|
|
1642
|
+
// IMPORTANT: getDirectory() contains entries in the EXACT order they were written to the ZIP file
|
|
1643
|
+
// Files are written first during compression, then token metadata is added at the end
|
|
1644
|
+
// So getDirectory() already has the correct order - use it as-is without reordering
|
|
1645
|
+
let zipEntries = [];
|
|
1646
|
+
const currentEntries = this.zip.getDirectory();
|
|
1647
|
+
if (currentEntries.length > 0) {
|
|
1648
|
+
// New ZIP creation - use getDirectory() in the exact order they were written
|
|
1649
|
+
// This matches the data section order: files first, then metadata last
|
|
1650
|
+
zipEntries = [...currentEntries];
|
|
1651
|
+
}
|
|
1652
|
+
else {
|
|
1653
|
+
// Update mode - get entries from loaded ZIP
|
|
1654
|
+
zipEntries = await this.zip.getDirectory() || [];
|
|
1655
|
+
}
|
|
1656
|
+
// Write central directory using ZipkitServer method
|
|
1657
|
+
const centralDirOffset = this.zipWriter.currentPosition;
|
|
1658
|
+
if (this.options.verbose) {
|
|
1659
|
+
(0, utils_1.log)(`📊 Central directory starts at offset: ${centralDirOffset}`, this.options);
|
|
1660
|
+
(0, utils_1.log)(`📁 Writing ${zipEntries.length} central directory entries`, this.options);
|
|
1661
|
+
}
|
|
1662
|
+
const centralDirSize = await this.zip.writeCentralDirectory(this.zipWriter, zipEntries, {
|
|
1663
|
+
archiveComment: this.options.archiveComment,
|
|
1664
|
+
onProgress: (entry) => {
|
|
1665
|
+
if (this.options.verbose) {
|
|
1666
|
+
(0, utils_1.log)(`📝 Writing central dir entry: ${entry.filename}`, this.options);
|
|
1667
|
+
(0, utils_1.log)(`📊 Entry localHdrOffset: ${entry.localHdrOffset}`, this.options);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
// Update currentPosition from zipWriter
|
|
1672
|
+
this.currentPosition = this.zipWriter.currentPosition;
|
|
1673
|
+
// Write end of central directory using ZipkitServer method
|
|
1674
|
+
if (this.options.verbose) {
|
|
1675
|
+
(0, utils_1.log)(`📝 Writing end of central directory`, this.options);
|
|
1676
|
+
(0, utils_1.log)(`📊 Central directory size: ${centralDirSize} bytes`, this.options);
|
|
1677
|
+
(0, utils_1.log)(`📊 Total file size: ${this.currentPosition} bytes (before EOCD)`, this.options);
|
|
1678
|
+
}
|
|
1679
|
+
await this.zip.writeEndOfCentralDirectory(this.zipWriter, zipEntries.length, centralDirSize, centralDirOffset, this.options.archiveComment);
|
|
1680
|
+
// Update currentPosition from zipWriter
|
|
1681
|
+
this.currentPosition = this.zipWriter.currentPosition;
|
|
1682
|
+
// Close the output stream
|
|
1683
|
+
await this.closeOutput();
|
|
1684
|
+
// Atomically move temp file to final location
|
|
1685
|
+
if (!this.tempArchiveName) {
|
|
1686
|
+
throw new Error('Temp archive name not initialized');
|
|
1687
|
+
}
|
|
1688
|
+
if (!fs.existsSync(this.tempArchiveName)) {
|
|
1689
|
+
throw new Error(`Temporary file not found: ${this.tempArchiveName}`);
|
|
1690
|
+
}
|
|
1691
|
+
// Use fs.rename() for atomic move (same filesystem)
|
|
1692
|
+
// This will overwrite existing file atomically if it exists
|
|
1693
|
+
fs.renameSync(this.tempArchiveName, this.archiveName);
|
|
1694
|
+
// Clean up temp file immediately after successful move (should already be moved, but ensure)
|
|
1695
|
+
if (fs.existsSync(this.tempArchiveName)) {
|
|
1696
|
+
try {
|
|
1697
|
+
fs.unlinkSync(this.tempArchiveName);
|
|
1698
|
+
}
|
|
1699
|
+
catch {
|
|
1700
|
+
// Ignore cleanup errors - file may have already been moved
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
// Apply old timestamp if requested (on final file)
|
|
1704
|
+
if (this.options.oldTimestamp) {
|
|
1705
|
+
try {
|
|
1706
|
+
const oldestTime = await (0, file_operations_1.findOldestEntryTime)(this.zip);
|
|
1707
|
+
if (oldestTime) {
|
|
1708
|
+
fs.utimesSync(this.archiveName, oldestTime, oldestTime);
|
|
1709
|
+
if (this.options.verbose) {
|
|
1710
|
+
(0, utils_1.log)(`📅 Set archive timestamp to oldest entry: ${oldestTime.toISOString()}`, this.options);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
catch (error) {
|
|
1715
|
+
if (this.options.verbose) {
|
|
1716
|
+
(0, utils_1.log)(`⚠️ Warning: Could not set old timestamp: ${error instanceof Error ? error.message : String(error)}`, this.options);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
cleanupFileDescriptors() {
|
|
1722
|
+
for (const [filePath, fd] of this.fdCache.entries()) {
|
|
1723
|
+
try {
|
|
1724
|
+
fs.closeSync(fd);
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
// Ignore errors during cleanup
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
this.fdCache.clear();
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Test the integrity of a ZIP archive using neounzip
|
|
1735
|
+
*/
|
|
1736
|
+
async function testArchiveIntegrity(archiveName, options) {
|
|
1737
|
+
try {
|
|
1738
|
+
// Always show when integrity testing starts
|
|
1739
|
+
(0, utils_1.log)(`\n🧪 Testing archive integrity: ${archiveName}`, options);
|
|
1740
|
+
// Import child_process for shelling out
|
|
1741
|
+
const { spawn } = require('child_process');
|
|
1742
|
+
// Use the installed neounzip command directly (works in both dev and installed)
|
|
1743
|
+
// In development, this will use the local bin, in installed package it uses global bin
|
|
1744
|
+
const neounzipCommand = 'neounzip';
|
|
1745
|
+
return new Promise((resolve) => {
|
|
1746
|
+
// Build neounzip command arguments
|
|
1747
|
+
const args = ['-t'];
|
|
1748
|
+
// Add password if encryption is enabled
|
|
1749
|
+
if (options.encrypt && options.password) {
|
|
1750
|
+
args.push('-P', options.password);
|
|
1751
|
+
}
|
|
1752
|
+
args.push(archiveName);
|
|
1753
|
+
// Run neounzip with -t (test) flag directly
|
|
1754
|
+
const neounzip = spawn(neounzipCommand, args, {
|
|
1755
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
1756
|
+
});
|
|
1757
|
+
let stdout = '';
|
|
1758
|
+
let stderr = '';
|
|
1759
|
+
let fileCount = 0;
|
|
1760
|
+
let testResults = [];
|
|
1761
|
+
neounzip.stdout.on('data', (data) => {
|
|
1762
|
+
const output = data.toString();
|
|
1763
|
+
stdout += output;
|
|
1764
|
+
// Parse the output to extract file testing information
|
|
1765
|
+
const lines = output.split('\n');
|
|
1766
|
+
for (const line of lines) {
|
|
1767
|
+
// Look for lines like "testing: filename ...OK SHA-256" or "testing: filename ...OK"
|
|
1768
|
+
if (line.includes('testing:') && line.includes('...OK')) {
|
|
1769
|
+
fileCount++;
|
|
1770
|
+
testResults.push(line.trim());
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
neounzip.stderr.on('data', (data) => {
|
|
1775
|
+
stderr += data.toString();
|
|
1776
|
+
});
|
|
1777
|
+
neounzip.on('close', (code) => {
|
|
1778
|
+
if (code === 0) {
|
|
1779
|
+
// Show detailed integrity test results
|
|
1780
|
+
if (fileCount > 0) {
|
|
1781
|
+
(0, utils_1.log)(`📊 Testing ${fileCount} file(s):`, options);
|
|
1782
|
+
testResults.forEach(result => {
|
|
1783
|
+
(0, utils_1.log)(` ${result}`, options);
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
// Look for summary line like "No errors detected in compressed data of X files"
|
|
1787
|
+
const summaryMatch = stdout.match(/No errors detected in compressed data of (\d+) files?/);
|
|
1788
|
+
if (summaryMatch) {
|
|
1789
|
+
(0, utils_1.log)(`✅ Archive integrity test passed - ${summaryMatch[1]} files verified`, options);
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
(0, utils_1.log)(`✅ Archive integrity test passed`, options);
|
|
1793
|
+
}
|
|
1794
|
+
resolve(true);
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
(0, utils_1.log)(`❌ Archive integrity test failed (exit code: ${code})`, options);
|
|
1798
|
+
if (stderr) {
|
|
1799
|
+
(0, utils_1.log)(`Error output: ${stderr}`, options);
|
|
1800
|
+
}
|
|
1801
|
+
resolve(false);
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
neounzip.on('error', (error) => {
|
|
1805
|
+
(0, utils_1.log)(`❌ Failed to run integrity test: ${error.message}`, options);
|
|
1806
|
+
resolve(false);
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
catch (error) {
|
|
1811
|
+
(0, utils_1.log)(`❌ Error during integrity test: ${error instanceof Error ? error.message : String(error)}`, options);
|
|
1812
|
+
return false;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Creates the end of central directory record manually
|
|
1817
|
+
* @param totalEntries - Total number of entries in the ZIP
|
|
1818
|
+
* @param centralDirSize - Size of central directory
|
|
1819
|
+
* @param centralDirOffset - Offset to central directory
|
|
1820
|
+
* @param archiveComment - Optional archive comment
|
|
1821
|
+
* @returns Buffer containing end of central directory record
|
|
1822
|
+
*/
|
|
1823
|
+
function createEndOfCentralDirRecord(totalEntries, centralDirSize, centralDirOffset, archiveComment = '') {
|
|
1824
|
+
const commentBytes = Buffer.from(archiveComment, 'utf8');
|
|
1825
|
+
const commentLength = Math.min(commentBytes.length, 0xFFFF); // Max 65535 bytes
|
|
1826
|
+
const buffer = Buffer.alloc(22 + commentLength);
|
|
1827
|
+
let offset = 0;
|
|
1828
|
+
// End of central directory signature (4 bytes)
|
|
1829
|
+
buffer.writeUInt32LE(0x06054b50, offset);
|
|
1830
|
+
offset += 4;
|
|
1831
|
+
// Number of this disk (2 bytes)
|
|
1832
|
+
buffer.writeUInt16LE(0, offset);
|
|
1833
|
+
offset += 2;
|
|
1834
|
+
// Number of the disk with the start of the central directory (2 bytes)
|
|
1835
|
+
buffer.writeUInt16LE(0, offset);
|
|
1836
|
+
offset += 2;
|
|
1837
|
+
// Total number of entries in the central directory on this disk (2 bytes)
|
|
1838
|
+
buffer.writeUInt16LE(totalEntries, offset);
|
|
1839
|
+
offset += 2;
|
|
1840
|
+
// Total number of entries in the central directory (2 bytes)
|
|
1841
|
+
buffer.writeUInt16LE(totalEntries, offset);
|
|
1842
|
+
offset += 2;
|
|
1843
|
+
// Size of the central directory (4 bytes)
|
|
1844
|
+
buffer.writeUInt32LE(centralDirSize, offset);
|
|
1845
|
+
offset += 4;
|
|
1846
|
+
// Offset of start of central directory with respect to the starting disk number (4 bytes)
|
|
1847
|
+
buffer.writeUInt32LE(centralDirOffset, offset);
|
|
1848
|
+
offset += 4;
|
|
1849
|
+
// ZIP file comment length (2 bytes)
|
|
1850
|
+
buffer.writeUInt16LE(commentLength, offset);
|
|
1851
|
+
offset += 2;
|
|
1852
|
+
// ZIP file comment (variable length)
|
|
1853
|
+
if (commentLength > 0) {
|
|
1854
|
+
commentBytes.copy(buffer, offset, 0, commentLength);
|
|
1855
|
+
}
|
|
1856
|
+
return buffer;
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Upgrade existing ZIP file for tokenization by adding SHA-256 hashes to entries
|
|
1860
|
+
* that don't have them, preserving original compression methods
|
|
1861
|
+
*/
|
|
1862
|
+
async function upgradeZipForTokenization(inputZipPath, options) {
|
|
1863
|
+
const os = require('os');
|
|
1864
|
+
(0, utils_1.log)(`\n🔧 Upgrading ZIP file for tokenization: ${inputZipPath}`, options);
|
|
1865
|
+
// Validate input file exists
|
|
1866
|
+
if (!fs.existsSync(inputZipPath)) {
|
|
1867
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_FIND_ARCHIVE, `Error: Input ZIP file not found: ${inputZipPath}`);
|
|
1868
|
+
}
|
|
1869
|
+
// Determine output file path (final destination)
|
|
1870
|
+
let outputZipPath;
|
|
1871
|
+
if (options.overwrite) {
|
|
1872
|
+
outputZipPath = inputZipPath;
|
|
1873
|
+
(0, utils_1.log)(` Output: ${outputZipPath} (overwriting input)`, options);
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
const inputDir = path.dirname(inputZipPath);
|
|
1877
|
+
const inputBase = path.basename(inputZipPath, path.extname(inputZipPath));
|
|
1878
|
+
const inputExt = path.extname(inputZipPath);
|
|
1879
|
+
outputZipPath = path.join(inputDir, `${inputBase}-tokenized${inputExt}`);
|
|
1880
|
+
(0, utils_1.log)(` Output: ${outputZipPath}`, options);
|
|
1881
|
+
}
|
|
1882
|
+
// Create temporary file path for atomic operation (even when overwriting)
|
|
1883
|
+
const tempDir = options.tempPath || os.tmpdir();
|
|
1884
|
+
// Ensure temp directory exists
|
|
1885
|
+
if (!fs.existsSync(tempDir)) {
|
|
1886
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1887
|
+
}
|
|
1888
|
+
const tempOutputPath = path.join(tempDir, `${path.basename(outputZipPath)}.tmp.${Date.now()}`);
|
|
1889
|
+
// Load existing ZIP
|
|
1890
|
+
const sourceZip = new server_1.ZipkitServer();
|
|
1891
|
+
await sourceZip.loadZipFile(inputZipPath);
|
|
1892
|
+
const entries = await sourceZip.getDirectory();
|
|
1893
|
+
if (!entries || entries.length === 0) {
|
|
1894
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, 'Error: ZIP file is empty or invalid');
|
|
1895
|
+
}
|
|
1896
|
+
(0, utils_1.log)(` Found ${entries.length} entries in ZIP`, options);
|
|
1897
|
+
// Check for existing token metadata (ignore per requirement 3a)
|
|
1898
|
+
const hasTokenMetadata = entries.some((e) => e.filename === src_1.TOKENIZED_METADATA);
|
|
1899
|
+
if (hasTokenMetadata && options.verbose) {
|
|
1900
|
+
(0, utils_1.log)(` Note: ZIP already contains token metadata (will create new token)`, options);
|
|
1901
|
+
}
|
|
1902
|
+
// Process entries: extract if missing SHA-256, calculate hash, preserve compression
|
|
1903
|
+
const processedEntries = [];
|
|
1904
|
+
const hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
|
|
1905
|
+
let tempFiles = [];
|
|
1906
|
+
try {
|
|
1907
|
+
for (const entry of entries) {
|
|
1908
|
+
const filename = entry.filename || '';
|
|
1909
|
+
// Skip metadata entries and directories
|
|
1910
|
+
if (filename === src_1.TOKENIZED_METADATA ||
|
|
1911
|
+
filename === src_1.TIMESTAMP_METADATA ||
|
|
1912
|
+
filename === src_1.TIMESTAMP_SUBMITTED ||
|
|
1913
|
+
entry.isDirectory) {
|
|
1914
|
+
continue;
|
|
1915
|
+
}
|
|
1916
|
+
// Check if entry needs SHA-256 hash
|
|
1917
|
+
let needsHash = !entry.sha256;
|
|
1918
|
+
if (needsHash) {
|
|
1919
|
+
(0, utils_1.log)(` Processing: ${filename} (calculating SHA-256...)`, options);
|
|
1920
|
+
// Check if encrypted (requirement 5c: error if cannot process)
|
|
1921
|
+
if (entry.isEncrypted) {
|
|
1922
|
+
if (!options.password) {
|
|
1923
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, `Error: Entry "${filename}" is encrypted but no password provided. Use -P or --password option.`);
|
|
1924
|
+
}
|
|
1925
|
+
// Set password on source ZIP for decryption
|
|
1926
|
+
sourceZip.password = options.password;
|
|
1927
|
+
}
|
|
1928
|
+
// Extract entry to get uncompressed data
|
|
1929
|
+
let uncompressedData;
|
|
1930
|
+
try {
|
|
1931
|
+
// Check if ZIP is file-based or buffer-based
|
|
1932
|
+
const isFileBased = sourceZip.fileHandle !== undefined;
|
|
1933
|
+
if (isFileBased) {
|
|
1934
|
+
// File-based: extract to temp file
|
|
1935
|
+
const tempFile = path.join(tempDir, `neozip-upgrade-${Date.now()}-${filename.replace(/[^a-zA-Z0-9]/g, '_')}`);
|
|
1936
|
+
tempFiles.push(tempFile);
|
|
1937
|
+
await sourceZip.extractToFile(entry, tempFile, { skipHashCheck: false });
|
|
1938
|
+
uncompressedData = fs.readFileSync(tempFile);
|
|
1939
|
+
}
|
|
1940
|
+
else {
|
|
1941
|
+
// Buffer-based: extract directly
|
|
1942
|
+
const extracted = await sourceZip.extract(entry, false);
|
|
1943
|
+
if (!extracted) {
|
|
1944
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to extract entry "${filename}"`);
|
|
1945
|
+
}
|
|
1946
|
+
uncompressedData = extracted;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
catch (error) {
|
|
1950
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1951
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to extract entry "${filename}": ${errorMsg}`);
|
|
1952
|
+
}
|
|
1953
|
+
// Calculate SHA-256 hash
|
|
1954
|
+
const hashHex = (0, ZipCrypto_1.sha256)(uncompressedData);
|
|
1955
|
+
entry.sha256 = hashHex;
|
|
1956
|
+
if (options.verbose) {
|
|
1957
|
+
(0, utils_1.log)(` SHA-256: ${hashHex.substring(0, 16)}...`, options);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
if (options.verbose) {
|
|
1962
|
+
(0, utils_1.log)(` Skipping: ${filename} (already has SHA-256)`, options);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
// Read compressed data from original ZIP (preserve compression)
|
|
1966
|
+
let compressedData;
|
|
1967
|
+
try {
|
|
1968
|
+
const isFileBased = sourceZip.fileHandle !== undefined;
|
|
1969
|
+
if (isFileBased) {
|
|
1970
|
+
// Read local header to find data offset
|
|
1971
|
+
const localHeaderBuffer = Buffer.alloc(30);
|
|
1972
|
+
await sourceZip.fileHandle.read(localHeaderBuffer, 0, 30, entry.localHdrOffset);
|
|
1973
|
+
if (localHeaderBuffer.readUInt32LE(0) !== 0x04034b50) {
|
|
1974
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, `Error: Invalid local header for entry "${filename}"`);
|
|
1975
|
+
}
|
|
1976
|
+
const filenameLength = localHeaderBuffer.readUInt16LE(26);
|
|
1977
|
+
const extraFieldLength = localHeaderBuffer.readUInt16LE(28);
|
|
1978
|
+
const dataStart = entry.localHdrOffset + 30 + filenameLength + extraFieldLength;
|
|
1979
|
+
// Read compressed data
|
|
1980
|
+
compressedData = Buffer.alloc(entry.compressedSize);
|
|
1981
|
+
await sourceZip.fileHandle.read(compressedData, 0, entry.compressedSize, dataStart);
|
|
1982
|
+
}
|
|
1983
|
+
else {
|
|
1984
|
+
// Buffer-based: parse local header and read compressed data
|
|
1985
|
+
const buffer = sourceZip.ensureBuffer();
|
|
1986
|
+
compressedData = sourceZip.parseLocalHeader(entry, buffer);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
catch (error) {
|
|
1990
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1991
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to read compressed data for entry "${filename}": ${errorMsg}`);
|
|
1992
|
+
}
|
|
1993
|
+
// Store processed entry with compressed data
|
|
1994
|
+
processedEntries.push({ entry, compressedData });
|
|
1995
|
+
// Add hash to merkle root calculation
|
|
1996
|
+
if (entry.sha256) {
|
|
1997
|
+
const hashBuffer = Buffer.from(entry.sha256, 'hex');
|
|
1998
|
+
hashAccumulator.addHash(hashBuffer);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
// Calculate merkle root
|
|
2002
|
+
const merkleRoot = hashAccumulator.merkleRoot();
|
|
2003
|
+
if (!merkleRoot) {
|
|
2004
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, 'Error: Failed to calculate merkle root (no content files found)');
|
|
2005
|
+
}
|
|
2006
|
+
(0, utils_1.log)(` Calculated merkle root: ${merkleRoot.substring(0, 16)}...`, options);
|
|
2007
|
+
// Rebuild ZIP with updated entries (write to temp file first for atomic operation)
|
|
2008
|
+
(0, utils_1.log)(` Rebuilding ZIP with updated entries...`, options);
|
|
2009
|
+
const outputZip = new server_1.ZipkitServer();
|
|
2010
|
+
const writer = await outputZip.initializeZipFile(tempOutputPath);
|
|
2011
|
+
try {
|
|
2012
|
+
let currentOffset = 0;
|
|
2013
|
+
// Write all entries with updated SHA-256 hashes
|
|
2014
|
+
for (const { entry, compressedData } of processedEntries) {
|
|
2015
|
+
// Create new entry with same properties but updated SHA-256
|
|
2016
|
+
const newEntry = new src_1.ZipEntry(entry.filename || '', null, false);
|
|
2017
|
+
Object.assign(newEntry, {
|
|
2018
|
+
filename: entry.filename,
|
|
2019
|
+
cmpMethod: entry.cmpMethod,
|
|
2020
|
+
compressedSize: entry.compressedSize,
|
|
2021
|
+
uncompressedSize: entry.uncompressedSize,
|
|
2022
|
+
crc: entry.crc,
|
|
2023
|
+
sha256: entry.sha256,
|
|
2024
|
+
timeDateDOS: entry.timeDateDOS,
|
|
2025
|
+
lastModTimeDate: entry.lastModTimeDate,
|
|
2026
|
+
comment: entry.comment,
|
|
2027
|
+
isDirectory: entry.isDirectory,
|
|
2028
|
+
isEncrypted: entry.isEncrypted,
|
|
2029
|
+
encryptionMethod: entry.encryptionMethod,
|
|
2030
|
+
uid: entry.uid,
|
|
2031
|
+
gid: entry.gid,
|
|
2032
|
+
isSymlink: entry.isSymlink,
|
|
2033
|
+
linkTarget: entry.linkTarget,
|
|
2034
|
+
universalTime: entry.universalTime
|
|
2035
|
+
});
|
|
2036
|
+
// Create local header
|
|
2037
|
+
const localHeader = newEntry.createLocalHdr();
|
|
2038
|
+
newEntry.localHdrOffset = currentOffset;
|
|
2039
|
+
// Write local header
|
|
2040
|
+
await new Promise((resolve, reject) => {
|
|
2041
|
+
writer.outputStream.write(localHeader, (error) => {
|
|
2042
|
+
if (error) {
|
|
2043
|
+
reject(error);
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
writer.currentPosition += localHeader.length;
|
|
2047
|
+
resolve();
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
});
|
|
2051
|
+
currentOffset += localHeader.length;
|
|
2052
|
+
// Write compressed data (preserved from original)
|
|
2053
|
+
await new Promise((resolve, reject) => {
|
|
2054
|
+
writer.outputStream.write(compressedData, (error) => {
|
|
2055
|
+
if (error) {
|
|
2056
|
+
reject(error);
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
writer.currentPosition += compressedData.length;
|
|
2060
|
+
currentOffset += compressedData.length;
|
|
2061
|
+
resolve();
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
// Mint new token before finalizing ZIP (so we can add metadata)
|
|
2067
|
+
// Check for wallet passkey - don't show error yet
|
|
2068
|
+
const walletPasskey = (0, blockchain_2.getWalletPasskey)(options.walletKey, false);
|
|
2069
|
+
if (!walletPasskey) {
|
|
2070
|
+
// Show error message and prompt user
|
|
2071
|
+
console.error('❌ Error: Wallet private key is required for blockchain operations');
|
|
2072
|
+
console.error('');
|
|
2073
|
+
console.error(' Option 1 (Recommended): Run interactive setup');
|
|
2074
|
+
console.error(' $ neozip init');
|
|
2075
|
+
console.error('');
|
|
2076
|
+
console.error(' Option 2: Use command-line flag');
|
|
2077
|
+
console.error(' $ neozip --upgrade -w 0x... <archive>');
|
|
2078
|
+
console.error('');
|
|
2079
|
+
console.error(' Option 3: Set environment variable');
|
|
2080
|
+
console.error(' $ export NEOZIP_WALLET_PASSKEY="0x..."');
|
|
2081
|
+
console.error('');
|
|
2082
|
+
console.error(' Get testnet ETH from: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet');
|
|
2083
|
+
console.error('');
|
|
2084
|
+
// Prompt user to continue without blockchain tokenization
|
|
2085
|
+
if (!options.nonInteractive) {
|
|
2086
|
+
const answer = await (0, user_interaction_1.promptUser)('Continue upgrade without blockchain tokenization? (y/n): ');
|
|
2087
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
2088
|
+
(0, utils_1.log)('⚠️ Continuing upgrade without blockchain tokenization (SHA-256 hashes will be added)...', options);
|
|
2089
|
+
// Skip token minting - ZIP will be upgraded with SHA-256 hashes but no token metadata
|
|
2090
|
+
}
|
|
2091
|
+
else {
|
|
2092
|
+
(0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
|
|
2093
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
else {
|
|
2097
|
+
// Non-interactive mode - exit with error
|
|
2098
|
+
(0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
|
|
2099
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
// Only mint token if wallet passkey is available
|
|
2103
|
+
if (walletPasskey) {
|
|
2104
|
+
(0, utils_1.log)(` Minting new token on blockchain...`, options);
|
|
2105
|
+
const network = options.network || 'base-sepolia';
|
|
2106
|
+
if (!(0, blockchain_2.isSupportedNetwork)(network)) {
|
|
2107
|
+
(0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR, `Error: Unsupported network: ${network}`);
|
|
2108
|
+
}
|
|
2109
|
+
const minter = new src_1.ZipkitMinter(merkleRoot, {
|
|
2110
|
+
network,
|
|
2111
|
+
walletPrivateKey: walletPasskey,
|
|
2112
|
+
verbose: options.verbose,
|
|
2113
|
+
debug: options.debug,
|
|
2114
|
+
rpcUrlIndex: 0
|
|
2115
|
+
});
|
|
2116
|
+
// Create a temporary ZipkitServer instance for handleTokenMinting
|
|
2117
|
+
// (it needs a zip instance, but we'll add metadata manually)
|
|
2118
|
+
const tempZip = new server_1.ZipkitServer();
|
|
2119
|
+
const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, tempZip, options.nonInteractive || false, 0);
|
|
2120
|
+
if (mintResult.success && mintResult.tokenInfo) {
|
|
2121
|
+
// Add token metadata entry before central directory
|
|
2122
|
+
const tokenContent = JSON.stringify(mintResult.tokenInfo, null, 2);
|
|
2123
|
+
const tokenBuffer = Buffer.from(tokenContent, 'utf8');
|
|
2124
|
+
const tokenEntry = new src_1.ZipEntry(src_1.TOKENIZED_METADATA, null, false);
|
|
2125
|
+
tokenEntry.timeDateDOS = tokenEntry.setDateTime(new Date());
|
|
2126
|
+
tokenEntry.compressedSize = tokenBuffer.length;
|
|
2127
|
+
tokenEntry.uncompressedSize = tokenBuffer.length;
|
|
2128
|
+
tokenEntry.cmpMethod = 0; // STORED
|
|
2129
|
+
tokenEntry.crc = (0, src_1.crc32)(tokenBuffer);
|
|
2130
|
+
tokenEntry.localHdrOffset = currentOffset;
|
|
2131
|
+
// Write token metadata entry
|
|
2132
|
+
const tokenLocalHeader = tokenEntry.createLocalHdr();
|
|
2133
|
+
await new Promise((resolve, reject) => {
|
|
2134
|
+
writer.outputStream.write(tokenLocalHeader, (error) => {
|
|
2135
|
+
if (error) {
|
|
2136
|
+
reject(error);
|
|
2137
|
+
}
|
|
2138
|
+
else {
|
|
2139
|
+
writer.currentPosition += tokenLocalHeader.length;
|
|
2140
|
+
resolve();
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
});
|
|
2144
|
+
currentOffset += tokenLocalHeader.length;
|
|
2145
|
+
await new Promise((resolve, reject) => {
|
|
2146
|
+
writer.outputStream.write(tokenBuffer, (error) => {
|
|
2147
|
+
if (error) {
|
|
2148
|
+
reject(error);
|
|
2149
|
+
}
|
|
2150
|
+
else {
|
|
2151
|
+
writer.currentPosition += tokenBuffer.length;
|
|
2152
|
+
currentOffset += tokenBuffer.length;
|
|
2153
|
+
resolve();
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
});
|
|
2157
|
+
// Add token entry to processed entries for central directory
|
|
2158
|
+
processedEntries.push({ entry: tokenEntry, compressedData: tokenBuffer });
|
|
2159
|
+
// Clean up minter
|
|
2160
|
+
minter.destroy();
|
|
2161
|
+
(0, utils_1.log)(` ✓ Token minted successfully!`, options);
|
|
2162
|
+
(0, utils_1.log)(` ✓ Token ID: ${mintResult.tokenInfo.tokenId}`, options);
|
|
2163
|
+
(0, utils_1.log)(` ✓ Transaction: ${mintResult.tokenInfo.transactionHash}`, options);
|
|
2164
|
+
}
|
|
2165
|
+
else {
|
|
2166
|
+
// Clean up minter
|
|
2167
|
+
minter.destroy();
|
|
2168
|
+
(0, utils_1.log)(` ⚠️ Token minting failed, but ZIP upgrade will continue`, options);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
else {
|
|
2172
|
+
(0, utils_1.log)(` ⚠️ Skipping blockchain tokenization (no wallet key provided)`, options);
|
|
2173
|
+
}
|
|
2174
|
+
// Write central directory (including token entry if minting succeeded)
|
|
2175
|
+
const finalCentralDirOffset = currentOffset;
|
|
2176
|
+
let finalCentralDirSize = 0;
|
|
2177
|
+
for (const { entry } of processedEntries) {
|
|
2178
|
+
const newEntry = new src_1.ZipEntry(entry.filename || '', null, false);
|
|
2179
|
+
Object.assign(newEntry, {
|
|
2180
|
+
filename: entry.filename,
|
|
2181
|
+
cmpMethod: entry.cmpMethod,
|
|
2182
|
+
compressedSize: entry.compressedSize,
|
|
2183
|
+
uncompressedSize: entry.uncompressedSize,
|
|
2184
|
+
crc: entry.crc,
|
|
2185
|
+
sha256: entry.sha256,
|
|
2186
|
+
timeDateDOS: entry.timeDateDOS,
|
|
2187
|
+
lastModTimeDate: entry.lastModTimeDate,
|
|
2188
|
+
comment: entry.comment,
|
|
2189
|
+
isDirectory: entry.isDirectory,
|
|
2190
|
+
localHdrOffset: entry.localHdrOffset,
|
|
2191
|
+
uid: entry.uid,
|
|
2192
|
+
gid: entry.gid,
|
|
2193
|
+
isSymlink: entry.isSymlink,
|
|
2194
|
+
linkTarget: entry.linkTarget,
|
|
2195
|
+
universalTime: entry.universalTime
|
|
2196
|
+
});
|
|
2197
|
+
const centralDirEntry = newEntry.centralDirEntry();
|
|
2198
|
+
await new Promise((resolve, reject) => {
|
|
2199
|
+
writer.outputStream.write(centralDirEntry, (error) => {
|
|
2200
|
+
if (error) {
|
|
2201
|
+
reject(error);
|
|
2202
|
+
}
|
|
2203
|
+
else {
|
|
2204
|
+
writer.currentPosition += centralDirEntry.length;
|
|
2205
|
+
finalCentralDirSize += centralDirEntry.length;
|
|
2206
|
+
currentOffset += centralDirEntry.length;
|
|
2207
|
+
resolve();
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
// Write end of central directory
|
|
2213
|
+
await outputZip.writeEndOfCentralDirectory(writer, processedEntries.length, finalCentralDirSize, finalCentralDirOffset, '' // No comment
|
|
2214
|
+
);
|
|
2215
|
+
// Finalize ZIP file
|
|
2216
|
+
await outputZip.finalizeZipFile(writer);
|
|
2217
|
+
// Atomically move temp file to final location
|
|
2218
|
+
if (!fs.existsSync(tempOutputPath)) {
|
|
2219
|
+
throw new Error(`Temporary file not found: ${tempOutputPath}`);
|
|
2220
|
+
}
|
|
2221
|
+
// Use fs.rename() for atomic move (same filesystem)
|
|
2222
|
+
// This will overwrite existing file atomically if it exists
|
|
2223
|
+
fs.renameSync(tempOutputPath, outputZipPath);
|
|
2224
|
+
// Clean up temp file immediately after successful move
|
|
2225
|
+
if (fs.existsSync(tempOutputPath)) {
|
|
2226
|
+
try {
|
|
2227
|
+
fs.unlinkSync(tempOutputPath);
|
|
2228
|
+
}
|
|
2229
|
+
catch {
|
|
2230
|
+
// Ignore cleanup errors - file may have already been moved
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
(0, utils_1.log)(` ✓ ZIP rebuilt successfully`, options);
|
|
2234
|
+
}
|
|
2235
|
+
catch (error) {
|
|
2236
|
+
// Clean up temp output file on error
|
|
2237
|
+
if (fs.existsSync(tempOutputPath)) {
|
|
2238
|
+
try {
|
|
2239
|
+
fs.unlinkSync(tempOutputPath);
|
|
2240
|
+
}
|
|
2241
|
+
catch {
|
|
2242
|
+
// Ignore cleanup errors
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
throw error;
|
|
2246
|
+
}
|
|
2247
|
+
(0, utils_1.log)(`\n✅ ZIP upgrade completed successfully!`, options);
|
|
2248
|
+
(0, utils_1.log)(` Output: ${outputZipPath}`, options);
|
|
2249
|
+
}
|
|
2250
|
+
finally {
|
|
2251
|
+
// Clean up temporary extraction files
|
|
2252
|
+
for (const tempFile of tempFiles) {
|
|
2253
|
+
try {
|
|
2254
|
+
if (fs.existsSync(tempFile)) {
|
|
2255
|
+
fs.unlinkSync(tempFile);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
catch {
|
|
2259
|
+
// Ignore cleanup errors
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
// Ensure temp output file is cleaned up if it still exists (error case)
|
|
2263
|
+
if (fs.existsSync(tempOutputPath)) {
|
|
2264
|
+
try {
|
|
2265
|
+
fs.unlinkSync(tempOutputPath);
|
|
2266
|
+
}
|
|
2267
|
+
catch {
|
|
2268
|
+
// Ignore cleanup errors
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
//# sourceMappingURL=createZip.js.map
|