@dignetwork/dig-sdk 0.0.1-alpha.57 → 0.0.1-alpha.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DataIntegrityTree/DataIntegrityTree.d.ts +3 -0
- package/dist/DataIntegrityTree/DataIntegrityTree.d.ts.map +1 -1
- package/dist/DataIntegrityTree/DataIntegrityTree.js +5 -2
- package/dist/DigNetwork/PropagationServer.d.ts +16 -4
- package/dist/DigNetwork/PropagationServer.d.ts.map +1 -1
- package/dist/DigNetwork/PropagationServer.js +116 -40
- package/dist/blockchain/DataStore.d.ts +10 -1
- package/dist/blockchain/DataStore.d.ts.map +1 -1
- package/dist/blockchain/DataStore.js +44 -11
- package/package.json +1 -1
|
@@ -78,6 +78,9 @@ declare class DataIntegrityTree {
|
|
|
78
78
|
* @returns The serialized Merkle tree.
|
|
79
79
|
*/
|
|
80
80
|
serialize(rootHash?: string | null): object;
|
|
81
|
+
static getRootOfForeignTree(treeData: {
|
|
82
|
+
leaves: string[];
|
|
83
|
+
}): string;
|
|
81
84
|
/**
|
|
82
85
|
* Deserialize a JSON object to a Merkle tree.
|
|
83
86
|
* @param rootHash - The root hash of the tree.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataIntegrityTree.d.ts","sourceRoot":"","sources":["../../src/DataIntegrityTree/DataIntegrityTree.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"DataIntegrityTree.d.ts","sourceRoot":"","sources":["../../src/DataIntegrityTree/DataIntegrityTree.ts"],"names":[],"mappings":"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"}
|
|
@@ -313,6 +313,11 @@ class DataIntegrityTree {
|
|
|
313
313
|
files: Object.fromEntries(this.files),
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
|
+
static getRootOfForeignTree(treeData) {
|
|
317
|
+
const leaves = treeData.leaves.map((leaf) => Buffer.from(leaf, "hex"));
|
|
318
|
+
const tree = new merkletreejs_1.MerkleTree(leaves, crypto_js_1.SHA256, { sortPairs: true });
|
|
319
|
+
return tree.getRoot().toString("hex");
|
|
320
|
+
}
|
|
316
321
|
/**
|
|
317
322
|
* Deserialize a JSON object to a Merkle tree.
|
|
318
323
|
* @param rootHash - The root hash of the tree.
|
|
@@ -603,7 +608,6 @@ class DataIntegrityTree {
|
|
|
603
608
|
decompressStream.on("end", () => {
|
|
604
609
|
const uncompressedSha256 = hash.digest("hex");
|
|
605
610
|
const isValid = uncompressedSha256 === sha256;
|
|
606
|
-
console.log(`SHA-256 of uncompressed file: ${uncompressedSha256}`);
|
|
607
611
|
if (!isValid) {
|
|
608
612
|
return resolve(false);
|
|
609
613
|
}
|
|
@@ -661,7 +665,6 @@ class DataIntegrityTree {
|
|
|
661
665
|
});
|
|
662
666
|
decompressStream.on("end", () => {
|
|
663
667
|
const uncompressedSha256 = hash.digest("hex");
|
|
664
|
-
console.log(`SHA-256 of uncompressed file: ${uncompressedSha256}`);
|
|
665
668
|
if (uncompressedSha256 !== sha256) {
|
|
666
669
|
console.warn(`File hash mismatch. Expected: ${sha256}, got: ${uncompressedSha256}`);
|
|
667
670
|
return resolve(false);
|
|
@@ -31,18 +31,30 @@ export declare class PropagationServer {
|
|
|
31
31
|
rootHashExists: boolean;
|
|
32
32
|
}>;
|
|
33
33
|
/**
|
|
34
|
-
* Start an upload session by sending a POST request
|
|
34
|
+
* Start an upload session by sending a POST request with the rootHash.dat file.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} rootHash - The root hash used to name the .dat file.
|
|
37
|
+
* @param {string} datFilePath - The full path to the rootHash.dat file.
|
|
35
38
|
*/
|
|
36
|
-
startUploadSession(): Promise<void>;
|
|
39
|
+
startUploadSession(rootHash: string): Promise<void>;
|
|
37
40
|
/**
|
|
38
41
|
* Request a nonce for a file by sending a HEAD request to the server.
|
|
42
|
+
* Checks if the file already exists based on the 'x-file-exists' header.
|
|
39
43
|
*/
|
|
40
|
-
getFileNonce(filename: string): Promise<
|
|
44
|
+
getFileNonce(filename: string): Promise<{
|
|
45
|
+
nonce: string;
|
|
46
|
+
fileExists: boolean;
|
|
47
|
+
}>;
|
|
41
48
|
/**
|
|
42
49
|
* Upload a file to the server by sending a PUT request.
|
|
43
50
|
* Logs progress using cli-progress for each file.
|
|
44
51
|
*/
|
|
45
|
-
uploadFile(
|
|
52
|
+
uploadFile(label: string, dataPath: string): Promise<any>;
|
|
53
|
+
/**
|
|
54
|
+
* Commit the upload session by sending a POST request to the server.
|
|
55
|
+
* This finalizes the upload and moves files from the temporary session directory to the permanent location.
|
|
56
|
+
*/
|
|
57
|
+
commitUploadSession(): Promise<any>;
|
|
46
58
|
/**
|
|
47
59
|
* Static function to handle the entire upload process for multiple files based on rootHash.
|
|
48
60
|
* @param {string} storeId - The ID of the DataStore.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PropagationServer.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/PropagationServer.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"PropagationServer.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/PropagationServer.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,MAAM,OAAO,CAAC;AAiB1B,qBAAa,iBAAiB;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,GAAG,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAE7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAIpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAqCrB;gBAEU,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAY9C;;OAEG;IACG,gBAAgB;IAKtB;;OAEG;IACH,gBAAgB;IAQhB;;;;;OAKG;IACG,gBAAgB,CACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,WAAW,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IAwC7D;;;;;OAKG;IACG,kBAAkB,CAAC,QAAQ,EAAE,MAAM;IAqDzC;;;OAGG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IAyBlD;;;OAGG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IA+DhD;;;OAGG;IACG,mBAAmB;IAgCzB;;;;;;OAMG;WACU,WAAW,CACtB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;IAiDnB;;;;;OAKG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA4ClD;;;;OAIG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM;IAgEnC;;;;;OAKG;WACU,aAAa,CACxB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;CA4BpB"}
|
|
@@ -17,6 +17,15 @@ const colorette_1 = require("colorette"); // For colored output
|
|
|
17
17
|
const config_1 = require("../utils/config");
|
|
18
18
|
const hashUtils_1 = require("../utils/hashUtils");
|
|
19
19
|
const nanospinner_1 = require("nanospinner");
|
|
20
|
+
// Helper function to trim long filenames with ellipsis and ensure consistent padding
|
|
21
|
+
function formatFilename(filename, maxLength = 30) {
|
|
22
|
+
if (filename.length > maxLength) {
|
|
23
|
+
// Trim the filename and add ellipsis
|
|
24
|
+
return `...${filename.slice(-(maxLength - 3))}`.padEnd(maxLength, " ");
|
|
25
|
+
}
|
|
26
|
+
// For shorter filenames, just pad to the maxLength
|
|
27
|
+
return filename.padEnd(maxLength, " ");
|
|
28
|
+
}
|
|
20
29
|
class PropagationServer {
|
|
21
30
|
constructor(storeId, ipAddress) {
|
|
22
31
|
this.storeId = storeId;
|
|
@@ -33,7 +42,7 @@ class PropagationServer {
|
|
|
33
42
|
*/
|
|
34
43
|
async initializeWallet() {
|
|
35
44
|
this.wallet = await blockchain_1.Wallet.load("default");
|
|
36
|
-
this.publicKey = await this.wallet.getPublicSyntheticKey().toString(
|
|
45
|
+
this.publicKey = await this.wallet.getPublicSyntheticKey().toString("hex");
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
39
48
|
* Create an Axios HTTPS Agent with self-signed certificate allowance.
|
|
@@ -66,18 +75,14 @@ class PropagationServer {
|
|
|
66
75
|
const storeExists = response.headers["x-store-exists"] === "true";
|
|
67
76
|
const rootHashExists = response.headers["x-has-root-hash"] === "true";
|
|
68
77
|
if (storeExists) {
|
|
69
|
-
spinner.success({
|
|
78
|
+
spinner.success({
|
|
79
|
+
text: (0, colorette_1.green)(`Store ${this.storeId} exists on peer!`),
|
|
80
|
+
});
|
|
70
81
|
}
|
|
71
82
|
else {
|
|
72
|
-
spinner.error({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (rootHashExists) {
|
|
76
|
-
console.log((0, colorette_1.green)(`Root hash ${rootHash} exists in the store.`));
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
console.log((0, colorette_1.red)(`Root hash ${rootHash} does not exist in the store.`));
|
|
80
|
-
}
|
|
83
|
+
spinner.error({
|
|
84
|
+
text: (0, colorette_1.red)(`Store ${this.storeId} does not exist. Credentials will be required to push.`),
|
|
85
|
+
});
|
|
81
86
|
}
|
|
82
87
|
return { storeExists, rootHashExists };
|
|
83
88
|
}
|
|
@@ -88,23 +93,38 @@ class PropagationServer {
|
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
/**
|
|
91
|
-
* Start an upload session by sending a POST request
|
|
96
|
+
* Start an upload session by sending a POST request with the rootHash.dat file.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} rootHash - The root hash used to name the .dat file.
|
|
99
|
+
* @param {string} datFilePath - The full path to the rootHash.dat file.
|
|
92
100
|
*/
|
|
93
|
-
async startUploadSession() {
|
|
101
|
+
async startUploadSession(rootHash) {
|
|
94
102
|
const spinner = (0, nanospinner_1.createSpinner)(`Starting upload session for store ${this.storeId}...`).start();
|
|
95
103
|
try {
|
|
104
|
+
const formData = new form_data_1.default();
|
|
105
|
+
const datFilePath = path_1.default.join(config_1.STORE_PATH, this.storeId, `${rootHash}.dat`);
|
|
106
|
+
// Ensure the rootHash.dat file exists
|
|
107
|
+
if (!fs_1.default.existsSync(datFilePath)) {
|
|
108
|
+
throw new Error(`File not found: ${datFilePath}`);
|
|
109
|
+
}
|
|
110
|
+
formData.append("file", fs_1.default.createReadStream(datFilePath), {
|
|
111
|
+
filename: `${rootHash}.dat`,
|
|
112
|
+
});
|
|
96
113
|
const config = {
|
|
97
114
|
httpsAgent: this.createHttpsAgent(),
|
|
115
|
+
headers: {
|
|
116
|
+
...formData.getHeaders(),
|
|
117
|
+
},
|
|
98
118
|
};
|
|
99
|
-
// Add Basic Auth
|
|
119
|
+
// Add Basic Auth if username and password are present
|
|
100
120
|
if (this.username && this.password) {
|
|
101
121
|
config.auth = {
|
|
102
122
|
username: this.username,
|
|
103
123
|
password: this.password,
|
|
104
124
|
};
|
|
105
125
|
}
|
|
106
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}`;
|
|
107
|
-
const response = await axios_1.default.post(url,
|
|
126
|
+
const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}?roothash=${rootHash}`;
|
|
127
|
+
const response = await axios_1.default.post(url, formData, config);
|
|
108
128
|
this.sessionId = response.data.sessionId;
|
|
109
129
|
spinner.success({
|
|
110
130
|
text: (0, colorette_1.green)(`Upload session started for DataStore ${this.storeId} with session ID ${this.sessionId}`),
|
|
@@ -118,6 +138,7 @@ class PropagationServer {
|
|
|
118
138
|
}
|
|
119
139
|
/**
|
|
120
140
|
* Request a nonce for a file by sending a HEAD request to the server.
|
|
141
|
+
* Checks if the file already exists based on the 'x-file-exists' header.
|
|
121
142
|
*/
|
|
122
143
|
async getFileNonce(filename) {
|
|
123
144
|
try {
|
|
@@ -126,9 +147,11 @@ class PropagationServer {
|
|
|
126
147
|
};
|
|
127
148
|
const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${filename}`;
|
|
128
149
|
const response = await axios_1.default.head(url, config);
|
|
150
|
+
// Check for 'x-file-exists' header
|
|
151
|
+
const fileExists = response.headers["x-file-exists"] === "true";
|
|
152
|
+
// If file exists, no need to generate a nonce
|
|
129
153
|
const nonce = response.headers["x-nonce"];
|
|
130
|
-
|
|
131
|
-
return nonce;
|
|
154
|
+
return { nonce, fileExists };
|
|
132
155
|
}
|
|
133
156
|
catch (error) {
|
|
134
157
|
console.error((0, colorette_1.red)(`Error generating nonce for file ${filename}:`), error.message);
|
|
@@ -139,9 +162,13 @@ class PropagationServer {
|
|
|
139
162
|
* Upload a file to the server by sending a PUT request.
|
|
140
163
|
* Logs progress using cli-progress for each file.
|
|
141
164
|
*/
|
|
142
|
-
async uploadFile(
|
|
143
|
-
const
|
|
144
|
-
const nonce = await this.getFileNonce(
|
|
165
|
+
async uploadFile(label, dataPath) {
|
|
166
|
+
const filePath = path_1.default.join(config_1.STORE_PATH, this.storeId, dataPath);
|
|
167
|
+
const { nonce, fileExists } = await this.getFileNonce(dataPath);
|
|
168
|
+
if (fileExists) {
|
|
169
|
+
console.log((0, colorette_1.blue)(`File ${label} already exists. Skipping upload.`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
145
172
|
const wallet = await blockchain_1.Wallet.load("default");
|
|
146
173
|
const keyOwnershipSig = await wallet.createKeyOwnershipSignature(nonce);
|
|
147
174
|
const publicKey = await wallet.getPublicSyntheticKey();
|
|
@@ -150,7 +177,7 @@ class PropagationServer {
|
|
|
150
177
|
const fileSize = fs_1.default.statSync(filePath).size;
|
|
151
178
|
// Create a new progress bar for each file
|
|
152
179
|
const progressBar = PropagationServer.multiBar.create(fileSize, 0, {
|
|
153
|
-
filename: (0, colorette_1.yellow)(
|
|
180
|
+
filename: (0, colorette_1.yellow)(path_1.default.basename(label)),
|
|
154
181
|
percentage: 0,
|
|
155
182
|
});
|
|
156
183
|
let uploadedBytes = 0;
|
|
@@ -165,26 +192,54 @@ class PropagationServer {
|
|
|
165
192
|
httpsAgent: this.createHttpsAgent(),
|
|
166
193
|
// Tracking upload progress and updating the progress bar
|
|
167
194
|
onUploadProgress: (progressEvent) => {
|
|
168
|
-
uploadedBytes
|
|
169
|
-
const percentage = Math.round((
|
|
195
|
+
const uploadedBytes = progressEvent.loaded;
|
|
196
|
+
const percentage = Math.round((100 * progressEvent.loaded) / progressEvent.total);
|
|
170
197
|
progressBar.update(uploadedBytes, { percentage });
|
|
171
198
|
},
|
|
172
199
|
};
|
|
173
200
|
try {
|
|
174
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${
|
|
201
|
+
const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${dataPath}`;
|
|
175
202
|
const response = await axios_1.default.put(url, formData, config);
|
|
176
|
-
console.log((0, colorette_1.green)(`✔ File ${filename} uploaded successfully.`));
|
|
177
203
|
// Complete the progress bar
|
|
178
204
|
progressBar.update(fileSize, { percentage: 100 });
|
|
179
205
|
progressBar.stop();
|
|
180
206
|
return response.data;
|
|
181
207
|
}
|
|
182
208
|
catch (error) {
|
|
183
|
-
console.error((0, colorette_1.red)(`✖ Error uploading file ${
|
|
209
|
+
console.error((0, colorette_1.red)(`✖ Error uploading file ${label}:`), error.message);
|
|
184
210
|
progressBar.stop(); // Stop the progress bar in case of error
|
|
185
211
|
throw error;
|
|
186
212
|
}
|
|
187
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Commit the upload session by sending a POST request to the server.
|
|
216
|
+
* This finalizes the upload and moves files from the temporary session directory to the permanent location.
|
|
217
|
+
*/
|
|
218
|
+
async commitUploadSession() {
|
|
219
|
+
const spinner = (0, nanospinner_1.createSpinner)(`Committing upload session for store ${this.storeId}...`).start();
|
|
220
|
+
try {
|
|
221
|
+
const config = {
|
|
222
|
+
httpsAgent: this.createHttpsAgent(),
|
|
223
|
+
auth: this.username && this.password
|
|
224
|
+
? {
|
|
225
|
+
username: this.username,
|
|
226
|
+
password: this.password,
|
|
227
|
+
}
|
|
228
|
+
: undefined,
|
|
229
|
+
};
|
|
230
|
+
const url = `https://${this.ipAddress}:${PropagationServer.port}/commit/${this.storeId}/${this.sessionId}`;
|
|
231
|
+
const response = await axios_1.default.post(url, {}, config);
|
|
232
|
+
spinner.success({
|
|
233
|
+
text: (0, colorette_1.green)(`Upload session ${this.sessionId} successfully committed.`),
|
|
234
|
+
});
|
|
235
|
+
return response.data;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
spinner.error({ text: (0, colorette_1.red)("Error committing upload session:") });
|
|
239
|
+
console.error((0, colorette_1.red)(error.message));
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
188
243
|
/**
|
|
189
244
|
* Static function to handle the entire upload process for multiple files based on rootHash.
|
|
190
245
|
* @param {string} storeId - The ID of the DataStore.
|
|
@@ -197,7 +252,7 @@ class PropagationServer {
|
|
|
197
252
|
// Initialize wallet
|
|
198
253
|
await propagationServer.initializeWallet();
|
|
199
254
|
// Check if the store exists
|
|
200
|
-
const storeExists = await propagationServer.checkStoreExists();
|
|
255
|
+
const { storeExists, rootHashExists } = await propagationServer.checkStoreExists(rootHash);
|
|
201
256
|
// If the store does not exist, prompt for credentials
|
|
202
257
|
if (!storeExists) {
|
|
203
258
|
console.log((0, colorette_1.red)(`Store ${storeId} does not exist. Prompting for credentials...`));
|
|
@@ -205,16 +260,20 @@ class PropagationServer {
|
|
|
205
260
|
propagationServer.username = credentials.username;
|
|
206
261
|
propagationServer.password = credentials.password;
|
|
207
262
|
}
|
|
263
|
+
if (rootHashExists) {
|
|
264
|
+
console.log((0, colorette_1.blue)(`Root hash ${rootHash} already exists in the store. Skipping upload.`));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
208
267
|
// Start the upload session
|
|
209
|
-
await propagationServer.startUploadSession();
|
|
268
|
+
await propagationServer.startUploadSession(rootHash);
|
|
210
269
|
const dataStore = blockchain_1.DataStore.from(storeId);
|
|
211
|
-
const
|
|
270
|
+
const files = await dataStore.getFileSetForRootHash(rootHash);
|
|
212
271
|
// Upload each file
|
|
213
|
-
for (const
|
|
214
|
-
await propagationServer.uploadFile(
|
|
272
|
+
for (const file of files) {
|
|
273
|
+
await propagationServer.uploadFile(file.name, file.path);
|
|
215
274
|
}
|
|
216
|
-
//
|
|
217
|
-
|
|
275
|
+
// Commit the session after all files have been uploaded
|
|
276
|
+
await propagationServer.commitUploadSession();
|
|
218
277
|
console.log((0, colorette_1.green)(`✔ All files have been uploaded to DataStore ${storeId}.`));
|
|
219
278
|
}
|
|
220
279
|
/**
|
|
@@ -296,7 +355,8 @@ class PropagationServer {
|
|
|
296
355
|
fileWriteStream.on("finish", () => {
|
|
297
356
|
progressBar.update(totalLength, { percentage: 100 });
|
|
298
357
|
progressBar.stop();
|
|
299
|
-
console.log((0, colorette_1.green)(
|
|
358
|
+
console.log((0, colorette_1.green)(`
|
|
359
|
+
File ${dataPath} downloaded successfully to ${downloadPath}.`));
|
|
300
360
|
resolve();
|
|
301
361
|
});
|
|
302
362
|
fileWriteStream.on("error", (error) => {
|
|
@@ -338,13 +398,29 @@ class PropagationServer {
|
|
|
338
398
|
}
|
|
339
399
|
exports.PropagationServer = PropagationServer;
|
|
340
400
|
PropagationServer.port = 4159; // Static port used for all requests
|
|
341
|
-
//
|
|
401
|
+
// Adjust cliProgress settings to align output properly and handle potential undefined values
|
|
402
|
+
// MultiBar for handling multiple progress bars with green progress and margin between bars
|
|
342
403
|
PropagationServer.multiBar = new cli_progress_1.default.MultiBar({
|
|
343
404
|
clearOnComplete: false,
|
|
344
405
|
hideCursor: true,
|
|
345
|
-
format: (
|
|
346
|
-
|
|
347
|
-
|
|
406
|
+
format: (options, params, payload) => {
|
|
407
|
+
// Bar length and padding settings
|
|
408
|
+
const barCompleteChar = (0, colorette_1.green)(options.barCompleteChar || "="); // Green bar for completed portion
|
|
409
|
+
const barIncompleteChar = options.barIncompleteChar || "-"; // Default incomplete character
|
|
410
|
+
const barSize = options.barsize || 40; // Default size of the bar
|
|
411
|
+
// Calculate the bar progress
|
|
412
|
+
const progressBar = `${barCompleteChar.repeat(Math.round(params.progress * barSize))}${barIncompleteChar.repeat(barSize - Math.round(params.progress * barSize))}`;
|
|
413
|
+
// Calculate the percentage manually
|
|
414
|
+
const percentage = Math.round((params.value / params.total) * 100);
|
|
415
|
+
// Format the filename to a fixed length
|
|
416
|
+
const formattedFilename = formatFilename(payload.filename, 30); // Trim to max 30 chars
|
|
417
|
+
// Padding the filename, percentage, and size
|
|
418
|
+
const percentageStr = `${percentage}%`.padEnd(4); // Ensure percentage is always 7 characters wide
|
|
419
|
+
const size = `${params.value}/${params.total} bytes`.padEnd(20); // Ensure size is always 20 characters wide
|
|
420
|
+
// Return the complete formatted progress bar with padding
|
|
421
|
+
return `${progressBar} | ${formattedFilename.padEnd(35)} | ${percentageStr} | ${size}`;
|
|
422
|
+
},
|
|
348
423
|
stopOnComplete: true,
|
|
349
|
-
|
|
424
|
+
barsize: 40,
|
|
425
|
+
align: "left",
|
|
350
426
|
}, cli_progress_1.default.Presets.shades_classic);
|
|
@@ -48,6 +48,15 @@ export declare class DataStore {
|
|
|
48
48
|
isSynced(): Promise<boolean>;
|
|
49
49
|
hasMetaWritePermissions(publicSyntheticKey?: Buffer): Promise<boolean>;
|
|
50
50
|
updateMetadata(metadata: DataStoreMetadata): Promise<DataStoreDriver>;
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Retrieve the file set for a given root hash and validate file integrity.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} rootHash - The root hash to fetch the file set.
|
|
55
|
+
* @returns {Promise<{ fileName: string, file: string }[]>} - An array of unique file objects.
|
|
56
|
+
*/
|
|
57
|
+
getFileSetForRootHash(rootHash: string): Promise<{
|
|
58
|
+
name: string;
|
|
59
|
+
path: string;
|
|
60
|
+
}[]>;
|
|
52
61
|
}
|
|
53
62
|
//# sourceMappingURL=DataStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataStore.d.ts","sourceRoot":"","sources":["../../src/blockchain/DataStore.ts"],"names":[],"mappings":"AAEA,OAAO,EAOL,SAAS,IAAI,eAAe,EAE5B,iBAAiB,EAIlB,MAAM,8BAA8B,CAAC;AAWtC,OAAO,EAAE,eAAe,EAAW,MAAM,UAAU,CAAC;AAGpD,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"DataStore.d.ts","sourceRoot":"","sources":["../../src/blockchain/DataStore.ts"],"names":[],"mappings":"AAEA,OAAO,EAOL,SAAS,IAAI,eAAe,EAE5B,iBAAiB,EAIlB,MAAM,8BAA8B,CAAC;AAWtC,OAAO,EAAE,eAAe,EAAW,MAAM,UAAU,CAAC;AAGpD,OAAO,EACL,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAYjD,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAAoB;gBAEpB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB;IAqB/D,IAAW,OAAO,IAAI,MAAM,CAE3B;IAED,IAAW,IAAI,IAAI,iBAAiB,CAEnC;IAEM,QAAQ,IAAI,MAAM;IAIlB,QAAQ,IAAI,MAAM;IAIlB,SAAS,IAAI,MAAM;WAMN,cAAc,IAAI,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;WAOtD,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS;WAK1C,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;WAYnC,MAAM,CACxB,MAAM,GAAE,qBAA0B,GACjC,OAAO,CAAC,SAAS,CAAC;mBAyCA,IAAI;IA4FzB;;;OAGG;IACU,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAK5C;;;OAGG;WACiB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IAIxD;;;;OAIG;mBACkB,mBAAmB;WAqB1B,YAAY,IAAI,SAAS,EAAE;IAW5B,aAAa,IAAI,OAAO,CAAC;QACpC,WAAW,EAAE,eAAe,CAAC;QAC7B,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IAyGW,iBAAiB,IAAI,OAAO,CAAC;QACxC,eAAe,EAAE,MAAM,CAAC;QACxB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;YA0BY,iBAAiB;IAYlB,cAAc,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IA+C5C,WAAW,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAKzC,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAM5B,uBAAuB,CAClC,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,OAAO,CAAC;IAkBN,cAAc,CACzB,QAAQ,EAAE,iBAAiB,GAC1B,OAAO,CAAC,eAAe,CAAC;IA+C3B;;;;;OAKG;IACU,qBAAqB,CAChC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAsD7C"}
|
|
@@ -18,11 +18,17 @@ const DataIntegrityTree_1 = require("../DataIntegrityTree");
|
|
|
18
18
|
const prompts_1 = require("../prompts");
|
|
19
19
|
const FileCache_1 = require("../utils/FileCache");
|
|
20
20
|
const DataStoreSerializer_1 = require("./DataStoreSerializer");
|
|
21
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
22
|
+
// Initialize the cache with a TTL of 180 seconds (3 minutes)
|
|
23
|
+
const rootHistoryCache = new node_cache_1.default({ stdTTL: 180 });
|
|
21
24
|
const stat = (0, util_1.promisify)(fs_1.default.stat);
|
|
22
25
|
const readdir = (0, util_1.promisify)(fs_1.default.readdir);
|
|
23
26
|
class DataStore {
|
|
24
27
|
constructor(storeId, options) {
|
|
25
28
|
this.storeId = storeId;
|
|
29
|
+
// This will create the manifest file if it doesn't exist
|
|
30
|
+
// might be a hacky way to do this, but it works for now
|
|
31
|
+
this.getRootHistory();
|
|
26
32
|
let _options;
|
|
27
33
|
if (options) {
|
|
28
34
|
_options = options;
|
|
@@ -284,18 +290,32 @@ class DataStore {
|
|
|
284
290
|
});
|
|
285
291
|
}
|
|
286
292
|
async getRootHistory() {
|
|
293
|
+
// Check if the root history is cached for this storeId
|
|
294
|
+
const cachedHistory = rootHistoryCache.get(this.storeId);
|
|
295
|
+
if (cachedHistory) {
|
|
296
|
+
return cachedHistory;
|
|
297
|
+
}
|
|
298
|
+
// Fetch root history from peer if not cached
|
|
287
299
|
const peer = await FullNodePeer_1.FullNodePeer.connect();
|
|
288
300
|
const { createdAtHeight, createdAtHash } = await this.getCreationHeight();
|
|
289
301
|
const { rootHashes, rootHashesTimestamps } = await peer.syncStoreFromLauncherId(Buffer.from(this.storeId, "hex"), createdAtHeight, createdAtHash, true);
|
|
290
302
|
if (!rootHashes) {
|
|
291
303
|
return [];
|
|
292
304
|
}
|
|
293
|
-
//
|
|
294
|
-
|
|
305
|
+
// Build the root history list
|
|
306
|
+
const rootHistory = rootHashes.map((rootHash, index) => ({
|
|
295
307
|
root_hash: rootHash.toString("hex"),
|
|
296
308
|
timestamp: Number(rootHashesTimestamps?.[index].toString()),
|
|
297
309
|
synced: fs_1.default.existsSync(path_1.default.join(config_1.STORE_PATH, this.storeId, `${rootHash.toString("hex")}.dat`)),
|
|
298
310
|
}));
|
|
311
|
+
// Store the root history in the cache
|
|
312
|
+
rootHistoryCache.set(this.storeId, rootHistory);
|
|
313
|
+
// Need this for the dataintegrity tree to work properly
|
|
314
|
+
fs_1.default.writeFileSync(path_1.default.join(config_1.STORE_PATH, this.storeId, "manifest.dat"), rootHistory
|
|
315
|
+
.filter((root) => root.synced)
|
|
316
|
+
.map((root) => root.root_hash)
|
|
317
|
+
.join("\n"));
|
|
318
|
+
return rootHistory;
|
|
299
319
|
}
|
|
300
320
|
async getMetaData() {
|
|
301
321
|
const { latestStore } = await this.fetchCoinInfo();
|
|
@@ -337,29 +357,42 @@ class DataStore {
|
|
|
337
357
|
}
|
|
338
358
|
return updateStoreResponse.newStore;
|
|
339
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Retrieve the file set for a given root hash and validate file integrity.
|
|
362
|
+
*
|
|
363
|
+
* @param {string} rootHash - The root hash to fetch the file set.
|
|
364
|
+
* @returns {Promise<{ fileName: string, file: string }[]>} - An array of unique file objects.
|
|
365
|
+
*/
|
|
340
366
|
async getFileSetForRootHash(rootHash) {
|
|
341
367
|
const datFilePath = path_1.default.join(config_1.STORE_PATH, this.storeId, `${rootHash}.dat`);
|
|
342
368
|
const datFileContent = JSON.parse(fs_1.default.readFileSync(datFilePath, "utf-8"));
|
|
343
369
|
const heightDatFilePath = path_1.default.join(config_1.STORE_PATH, this.storeId, "height.json");
|
|
344
|
-
|
|
345
|
-
filesInvolved
|
|
346
|
-
filesInvolved.
|
|
370
|
+
// Use a Set to ensure uniqueness
|
|
371
|
+
const filesInvolved = new Set();
|
|
372
|
+
filesInvolved.add({
|
|
373
|
+
name: "height.json",
|
|
374
|
+
path: "height.json",
|
|
375
|
+
});
|
|
347
376
|
// Iterate over each file and perform an integrity check
|
|
348
377
|
for (const [fileKey, fileData] of Object.entries(datFileContent.files)) {
|
|
349
|
-
const filePath = (0, hashUtils_1.getFilePathFromSha256)(datFileContent.files[fileKey].sha256,
|
|
350
|
-
// Perform the integrity check
|
|
378
|
+
const filePath = (0, hashUtils_1.getFilePathFromSha256)(datFileContent.files[fileKey].sha256, "data");
|
|
379
|
+
// Perform the integrity check
|
|
351
380
|
const integrityCheck = await DataIntegrityTree_1.DataIntegrityTree.validateKeyIntegrityWithForeignTree(fileKey, datFileContent.files[fileKey].sha256, datFileContent, rootHash, path_1.default.join(config_1.STORE_PATH, this.storeId, "data"));
|
|
352
381
|
if (integrityCheck) {
|
|
353
|
-
console.log((0, colorette_1.green)(
|
|
354
|
-
// Add the file to the
|
|
355
|
-
filesInvolved.
|
|
382
|
+
console.log((0, colorette_1.green)(`✔ File ${fileKey} has passed the integrity check.`));
|
|
383
|
+
// Add the file to the Set
|
|
384
|
+
filesInvolved.add({
|
|
385
|
+
name: Buffer.from(fileKey, "hex").toString("utf-8"),
|
|
386
|
+
path: filePath,
|
|
387
|
+
});
|
|
356
388
|
}
|
|
357
389
|
else {
|
|
358
390
|
console.error((0, colorette_1.red)(`File ${fileKey} failed the integrity check.`));
|
|
359
391
|
throw new Error(`Integrity check failed for file: ${fileKey}. Aborting.`);
|
|
360
392
|
}
|
|
361
393
|
}
|
|
362
|
-
return
|
|
394
|
+
// Convert Set to Array and return
|
|
395
|
+
return Array.from(filesInvolved);
|
|
363
396
|
}
|
|
364
397
|
}
|
|
365
398
|
exports.DataStore = DataStore;
|