@dignetwork/dig-sdk 0.0.1-alpha.16 → 0.0.1-alpha.161
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 +11 -2
- package/dist/DataIntegrityTree/DataIntegrityTree.d.ts.map +1 -1
- package/dist/DataIntegrityTree/DataIntegrityTree.js +94 -13
- package/dist/DigNetwork/ContentServer.d.ts +24 -5
- package/dist/DigNetwork/ContentServer.d.ts.map +1 -1
- package/dist/DigNetwork/ContentServer.js +239 -27
- package/dist/DigNetwork/DigNetwork.d.ts +15 -9
- package/dist/DigNetwork/DigNetwork.d.ts.map +1 -1
- package/dist/DigNetwork/DigNetwork.js +129 -189
- package/dist/DigNetwork/DigPeer.d.ts +8 -3
- package/dist/DigNetwork/DigPeer.d.ts.map +1 -1
- package/dist/DigNetwork/DigPeer.js +68 -115
- package/dist/DigNetwork/IncentiveServer.d.ts.map +1 -1
- package/dist/DigNetwork/IncentiveServer.js +6 -5
- package/dist/DigNetwork/PropagationServer.d.ts +88 -33
- package/dist/DigNetwork/PropagationServer.d.ts.map +1 -1
- package/dist/DigNetwork/PropagationServer.js +636 -357
- package/dist/blockchain/DataStore.d.ts +25 -7
- package/dist/blockchain/DataStore.d.ts.map +1 -1
- package/dist/blockchain/DataStore.js +115 -159
- package/dist/blockchain/DataStoreSerializer.d.ts +1 -1
- package/dist/blockchain/DataStoreSerializer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.d.ts +94 -10
- package/dist/blockchain/FullNodePeer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.js +318 -104
- package/dist/blockchain/ServerCoin.d.ts +13 -5
- package/dist/blockchain/ServerCoin.d.ts.map +1 -1
- package/dist/blockchain/ServerCoin.js +90 -28
- package/dist/blockchain/StoreMonitorRegistry.d.ts +85 -0
- package/dist/blockchain/StoreMonitorRegistry.d.ts.map +1 -0
- package/dist/blockchain/StoreMonitorRegistry.js +241 -0
- package/dist/blockchain/Wallet.d.ts +2 -2
- package/dist/blockchain/Wallet.d.ts.map +1 -1
- package/dist/blockchain/Wallet.js +50 -18
- package/dist/blockchain/coins.d.ts +1 -2
- package/dist/blockchain/coins.d.ts.map +1 -1
- package/dist/blockchain/coins.js +1 -55
- package/dist/blockchain/index.d.ts +1 -0
- package/dist/blockchain/index.d.ts.map +1 -1
- package/dist/blockchain/index.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ContentScanner.d.ts +63 -0
- package/dist/utils/ContentScanner.d.ts.map +1 -0
- package/dist/utils/ContentScanner.js +175 -0
- package/dist/utils/Environment.d.ts +21 -0
- package/dist/utils/Environment.d.ts.map +1 -0
- package/dist/utils/Environment.js +93 -0
- package/dist/utils/FileCache.d.ts +1 -1
- package/dist/utils/FileCache.d.ts.map +1 -1
- package/dist/utils/FileCache.js +2 -2
- package/dist/utils/FileTransfer.d.ts +47 -0
- package/dist/utils/FileTransfer.d.ts.map +1 -0
- package/dist/utils/FileTransfer.js +209 -0
- package/dist/utils/NconfManager.js +2 -1
- package/dist/utils/PeerRanker.d.ts +55 -0
- package/dist/utils/PeerRanker.d.ts.map +1 -0
- package/dist/utils/PeerRanker.js +186 -0
- package/dist/utils/StoreArchiveManager.d.ts +45 -0
- package/dist/utils/StoreArchiveManager.d.ts.map +1 -0
- package/dist/utils/StoreArchiveManager.js +153 -0
- package/dist/utils/config.d.ts +4 -3
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -7
- package/dist/utils/directoryUtils.d.ts.map +1 -1
- package/dist/utils/directoryUtils.js +9 -12
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/merkle.d.ts +2 -0
- package/dist/utils/merkle.d.ts.map +1 -0
- package/dist/utils/merkle.js +28 -0
- package/dist/utils/network.d.ts +2 -1
- package/dist/utils/network.d.ts.map +1 -1
- package/dist/utils/network.js +40 -15
- package/dist/utils/promiseUtils.d.ts +19 -0
- package/dist/utils/promiseUtils.d.ts.map +1 -0
- package/dist/utils/promiseUtils.js +43 -0
- package/dist/utils/ssl.js +2 -2
- package/package.json +17 -5
- package/dist/utils/deltaUtils.d.ts +0 -2
- package/dist/utils/deltaUtils.d.ts.map +0 -1
- package/dist/utils/deltaUtils.js +0 -83
|
@@ -1,415 +1,694 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
27
|
};
|
|
5
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
29
|
exports.PropagationServer = void 0;
|
|
30
|
+
const axios_1 = __importDefault(require("axios"));
|
|
31
|
+
const cli_progress_1 = __importDefault(require("cli-progress"));
|
|
32
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
7
33
|
const fs_1 = __importDefault(require("fs"));
|
|
34
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
35
|
const https_1 = __importDefault(require("https"));
|
|
9
|
-
const
|
|
36
|
+
const os_1 = __importDefault(require("os"));
|
|
37
|
+
const path_1 = __importDefault(require("path"));
|
|
38
|
+
const progress_stream_1 = __importDefault(require("progress-stream"));
|
|
39
|
+
const zlib = __importStar(require("zlib"));
|
|
40
|
+
const promiseUtils_1 = require("../utils/promiseUtils");
|
|
41
|
+
const nanospinner_1 = require("nanospinner");
|
|
42
|
+
const hashUtils_1 = require("../utils/hashUtils");
|
|
10
43
|
const ssl_1 = require("../utils/ssl");
|
|
44
|
+
const colorette_1 = require("colorette");
|
|
45
|
+
const merkle_1 = require("../utils/merkle");
|
|
46
|
+
const stream_1 = require("stream");
|
|
11
47
|
const credentialsUtils_1 = require("../utils/credentialsUtils");
|
|
12
|
-
const
|
|
48
|
+
const config_1 = require("../utils/config");
|
|
13
49
|
const blockchain_1 = require("../blockchain");
|
|
50
|
+
const network_1 = require("../utils/network");
|
|
51
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
52
|
+
// Initialize cache with a TTL of 1 week (604800 seconds)
|
|
53
|
+
const storeExistsCache = new node_cache_1.default({ stdTTL: 86400 });
|
|
54
|
+
const pingUpdatecache = new node_cache_1.default({ stdTTL: 86400 });
|
|
55
|
+
// Helper function to trim long filenames with ellipsis and ensure consistent padding
|
|
56
|
+
function formatFilename(filename, maxLength = 30) {
|
|
57
|
+
if (!filename) {
|
|
58
|
+
return "Unknown File".padEnd(maxLength, " ");
|
|
59
|
+
}
|
|
60
|
+
if (filename.length > maxLength) {
|
|
61
|
+
return `...${filename.slice(-(maxLength - 3))}`.padEnd(maxLength, " ");
|
|
62
|
+
}
|
|
63
|
+
return filename.padEnd(maxLength, " ");
|
|
64
|
+
}
|
|
14
65
|
class PropagationServer {
|
|
15
66
|
constructor(ipAddress, storeId) {
|
|
16
|
-
this.ipAddress = ipAddress;
|
|
17
67
|
this.storeId = storeId;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async pushFile(filePath, relativePath) {
|
|
26
|
-
const { nonce, username, password } = await this.getUploadDetails();
|
|
27
|
-
const wallet = await blockchain_1.Wallet.load("default");
|
|
28
|
-
const keyOwnershipSig = await wallet.createKeyOwnershipSignature(nonce);
|
|
29
|
-
const publicKey = await wallet.getPublicSyntheticKey();
|
|
30
|
-
const uploadUrl = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${relativePath}`;
|
|
31
|
-
await this.retryOperation(() => this.uploadFileDirect(filePath, uploadUrl, username, password, keyOwnershipSig, publicKey.toString("hex"), nonce), `Upload failed for ${relativePath}`);
|
|
32
|
-
}
|
|
33
|
-
// Method to subscribe to a store on the propagation server
|
|
34
|
-
async subscribeToStore() {
|
|
35
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/subscribe`;
|
|
36
|
-
const data = JSON.stringify({ storeId: this.storeId });
|
|
37
|
-
await this.postRequest(url, data);
|
|
38
|
-
}
|
|
39
|
-
// Method to unsubscribe from a store on the propagation server
|
|
40
|
-
async unsubscribeFromStore() {
|
|
41
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/unsubscribe`;
|
|
42
|
-
const data = JSON.stringify({ storeId: this.storeId });
|
|
43
|
-
await this.postRequest(url, data);
|
|
44
|
-
}
|
|
45
|
-
// Method to check the status of a store on the propagation server
|
|
46
|
-
async getStoreStatus() {
|
|
47
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/status/${this.storeId}`;
|
|
48
|
-
return this.fetchJson(url);
|
|
49
|
-
}
|
|
50
|
-
async streamStoreData(dataPath) {
|
|
51
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${dataPath}`;
|
|
52
|
-
return this.createReadStreamWithRetries(url);
|
|
53
|
-
}
|
|
54
|
-
// Method to retrieve a store's key
|
|
55
|
-
async getStoreData(dataPath) {
|
|
56
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${dataPath}`;
|
|
57
|
-
return this.fetch(url);
|
|
58
|
-
}
|
|
59
|
-
async getStatus() {
|
|
60
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/status/${this.storeId}`;
|
|
61
|
-
const statusJson = await this.fetch(url);
|
|
62
|
-
return JSON.parse(statusJson);
|
|
68
|
+
this.sessionId = "";
|
|
69
|
+
this.publicKey = "";
|
|
70
|
+
this.ipAddress = ipAddress;
|
|
71
|
+
// Get or create SSL certificates
|
|
72
|
+
const { certPath, keyPath } = (0, ssl_1.getOrCreateSSLCerts)();
|
|
73
|
+
this.certPath = certPath;
|
|
74
|
+
this.keyPath = keyPath;
|
|
63
75
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Initialize the Wallet instance.
|
|
78
|
+
*/
|
|
79
|
+
async initializeWallet() {
|
|
80
|
+
this.wallet = await blockchain_1.Wallet.load("default");
|
|
81
|
+
this.publicKey = (await this.wallet.getPublicSyntheticKey()).toString("hex");
|
|
68
82
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Create an Axios HTTPS Agent with self-signed certificate allowance.
|
|
85
|
+
*/
|
|
86
|
+
createHttpsAgent() {
|
|
87
|
+
return new https_1.default.Agent({
|
|
88
|
+
cert: fs_1.default.readFileSync(this.certPath),
|
|
89
|
+
key: fs_1.default.readFileSync(this.keyPath),
|
|
90
|
+
rejectUnauthorized: false,
|
|
91
|
+
});
|
|
73
92
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Ping the current peer about an update to the store, passing rootHash.
|
|
95
|
+
* @param rootHash - The root hash for the store update.
|
|
96
|
+
*/
|
|
97
|
+
async getMyPublicIp(rootHash) {
|
|
98
|
+
try {
|
|
99
|
+
const httpsAgent = this.createHttpsAgent();
|
|
100
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/peer`;
|
|
101
|
+
const config = {
|
|
102
|
+
httpsAgent,
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
const response = await axios_1.default.get(url, config);
|
|
109
|
+
return response.data;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error((0, colorette_1.red)(error.message));
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error((0, colorette_1.red)(error.message));
|
|
118
|
+
}
|
|
88
119
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Ping the current peer about an update to the store, passing rootHash.
|
|
122
|
+
* If the combination of ipAddress-storeId-rootHash is cached as "successfully synced", skip the request.
|
|
123
|
+
* @param rootHash - The root hash for the store update.
|
|
124
|
+
*/
|
|
125
|
+
async pingUpdate(rootHash) {
|
|
126
|
+
const cacheKey = `${this.ipAddress}-${this.storeId}-${rootHash}`;
|
|
127
|
+
// Check if response for this combination is already cached
|
|
128
|
+
if (pingUpdatecache.get(cacheKey) === "successfully synced") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const httpsAgent = this.createHttpsAgent();
|
|
133
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/update`;
|
|
134
|
+
const config = {
|
|
135
|
+
httpsAgent,
|
|
98
136
|
headers: {
|
|
99
137
|
"Content-Type": "application/json",
|
|
100
|
-
"Content-Length": Buffer.byteLength(data),
|
|
101
138
|
},
|
|
102
|
-
key: fs_1.default.readFileSync(PropagationServer.keyPath),
|
|
103
|
-
cert: fs_1.default.readFileSync(PropagationServer.certPath),
|
|
104
|
-
rejectUnauthorized: false,
|
|
105
139
|
};
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
const data = {
|
|
141
|
+
storeId: this.storeId,
|
|
142
|
+
rootHash: rootHash,
|
|
143
|
+
updateTime: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
const response = await axios_1.default.post(url, data, config);
|
|
147
|
+
console.log((0, colorette_1.green)(`✔ Successfully pinged peer: ${this.ipAddress}`), response.data);
|
|
148
|
+
if (response.data.message === "Already Synced") {
|
|
149
|
+
// Cache the response as "successfully synced" to avoid redundant requests
|
|
150
|
+
pingUpdatecache.set(cacheKey, "successfully synced");
|
|
112
151
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
return response.data;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error((0, colorette_1.red)(`✖ Failed to ping peer: ${this.ipAddress}`), error.message);
|
|
156
|
+
console.error((0, colorette_1.red)(error.message));
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error((0, colorette_1.red)(error.message));
|
|
162
|
+
}
|
|
120
163
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Adds a custom inactivity timeout for large file transfers.
|
|
166
|
+
*/
|
|
167
|
+
addInactivityTimeout(stream, timeoutMs) {
|
|
168
|
+
let timeoutId;
|
|
169
|
+
const resetTimeout = () => {
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
timeoutId = setTimeout(() => {
|
|
172
|
+
stream.destroy(new Error(`Inactivity timeout after ${timeoutMs / 1000} seconds`));
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
};
|
|
175
|
+
// Reset timeout every time data is received
|
|
176
|
+
stream.on("data", resetTimeout);
|
|
177
|
+
// Set the initial timeout
|
|
178
|
+
resetTimeout();
|
|
179
|
+
// Clear the timeout when the stream ends
|
|
180
|
+
stream.on("end", () => clearTimeout(timeoutId));
|
|
181
|
+
stream.on("error", () => clearTimeout(timeoutId)); // Handle errors
|
|
182
|
+
return stream;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Checks if the store exists and optionally if a specific rootHash exists.
|
|
186
|
+
* Utilizes caching to improve performance.
|
|
187
|
+
*
|
|
188
|
+
* @param rootHash - (Optional) The root hash to check.
|
|
189
|
+
* @returns A promise that resolves to an object containing store existence and root hash existence.
|
|
190
|
+
*/
|
|
191
|
+
async checkStoreExists(rootHash) {
|
|
192
|
+
// Construct the cache key
|
|
193
|
+
const cacheKey = rootHash
|
|
194
|
+
? `${this.storeId}-${rootHash}`
|
|
195
|
+
: `${this.storeId}-nohash`;
|
|
196
|
+
// Check if the result is already cached
|
|
197
|
+
const cachedResult = storeExistsCache.get(cacheKey);
|
|
198
|
+
if (cachedResult !== undefined) {
|
|
199
|
+
return cachedResult;
|
|
200
|
+
}
|
|
201
|
+
// If not cached, proceed with the HTTP request
|
|
202
|
+
const spinner = (0, nanospinner_1.createSpinner)(`Checking if store ${this.storeId} exists...`).start();
|
|
203
|
+
try {
|
|
204
|
+
const config = {
|
|
205
|
+
httpsAgent: this.createHttpsAgent(),
|
|
206
|
+
};
|
|
207
|
+
let url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/${this.storeId}`;
|
|
208
|
+
if (rootHash) {
|
|
209
|
+
url += `?hasRootHash=${rootHash}`;
|
|
210
|
+
}
|
|
211
|
+
const response = await axios_1.default.head(url, config);
|
|
212
|
+
// Extract store existence and root hash existence from headers
|
|
213
|
+
const storeExists = response.headers["x-store-exists"] === "true";
|
|
214
|
+
const rootHashExists = response.headers["x-has-root-hash"] === "true";
|
|
215
|
+
if (storeExists) {
|
|
216
|
+
spinner.success({
|
|
217
|
+
text: (0, colorette_1.green)(`Store ${this.storeId} exists on peer: ${this.ipAddress}`),
|
|
162
218
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
spinner.error({
|
|
222
|
+
text: (0, colorette_1.yellow)(`Store ${this.storeId} does not exist on ${this.ipAddress}. Credentials will be required to push.`),
|
|
166
223
|
});
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
224
|
+
}
|
|
225
|
+
const result = { storeExists, rootHashExists };
|
|
226
|
+
storeExistsCache.set(cacheKey, result);
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
spinner.error({ text: (0, colorette_1.red)("Error checking if store exists:") });
|
|
231
|
+
console.error((0, colorette_1.red)(error.message));
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
170
234
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Start an upload session by sending a POST request with the rootHash.dat file.
|
|
237
|
+
*/
|
|
238
|
+
async startUploadSession(rootHash) {
|
|
239
|
+
const spinner = (0, nanospinner_1.createSpinner)(`Starting upload session for store ${this.storeId}...`).start();
|
|
240
|
+
try {
|
|
241
|
+
const formData = new form_data_1.default();
|
|
242
|
+
const datFilePath = path_1.default.join(config_1.STORE_PATH, this.storeId, `${rootHash}.dat`);
|
|
243
|
+
// Ensure the rootHash.dat file exists
|
|
244
|
+
if (!fs_1.default.existsSync(datFilePath)) {
|
|
245
|
+
throw new Error(`File not found: ${datFilePath}`);
|
|
246
|
+
}
|
|
247
|
+
formData.append("file", fs_1.default.createReadStream(datFilePath), {
|
|
248
|
+
filename: `${rootHash}.dat`,
|
|
249
|
+
});
|
|
250
|
+
const config = {
|
|
251
|
+
httpsAgent: this.createHttpsAgent(),
|
|
182
252
|
headers: {
|
|
183
|
-
|
|
184
|
-
"Content-Length": fileSize,
|
|
185
|
-
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
|
|
186
|
-
"x-key-ownership-sig": keyOwnershipSig,
|
|
187
|
-
"x-public-key": publicKey,
|
|
188
|
-
"x-nonce": nonce,
|
|
253
|
+
...formData.getHeaders(),
|
|
189
254
|
},
|
|
190
|
-
key: fs_1.default.readFileSync(PropagationServer.keyPath),
|
|
191
|
-
cert: fs_1.default.readFileSync(PropagationServer.certPath),
|
|
192
|
-
rejectUnauthorized: false,
|
|
193
255
|
};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
256
|
+
// Add Basic Auth if username and password are present
|
|
257
|
+
if (this.username && this.password) {
|
|
258
|
+
config.auth = {
|
|
259
|
+
username: this.username,
|
|
260
|
+
password: this.password,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/upload/${this.storeId}?roothash=${rootHash}`;
|
|
264
|
+
const response = await axios_1.default.post(url, formData, config);
|
|
265
|
+
this.sessionId = response.data.sessionId;
|
|
266
|
+
spinner.success({
|
|
267
|
+
text: (0, colorette_1.green)(`Upload session started for Root Hash: ${(0, colorette_1.cyan)(rootHash)} with session ID ${this.sessionId}`),
|
|
201
268
|
});
|
|
202
|
-
|
|
203
|
-
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
spinner.error({ text: (0, colorette_1.red)("Error starting upload session:") });
|
|
272
|
+
console.error((0, colorette_1.red)(error.message));
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Request a nonce for a file by sending a HEAD request to the server.
|
|
278
|
+
*/
|
|
279
|
+
async getFileNonce(filename) {
|
|
280
|
+
try {
|
|
281
|
+
const config = {
|
|
282
|
+
httpsAgent: this.createHttpsAgent(),
|
|
283
|
+
};
|
|
284
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${filename}`;
|
|
285
|
+
const response = await axios_1.default.head(url, config);
|
|
286
|
+
// Check for 'x-file-exists' header
|
|
287
|
+
const fileExists = response.headers["x-file-exists"] === "true";
|
|
288
|
+
// If file exists, no need to generate a nonce
|
|
289
|
+
const nonce = response.headers["x-nonce"];
|
|
290
|
+
return { nonce, fileExists };
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
console.error((0, colorette_1.red)(`Error generating nonce for file ${filename}:`), error.message);
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Upload a file to the server by sending a PUT request.
|
|
299
|
+
* Logs progress using a local cli-progress bar.
|
|
300
|
+
*/
|
|
301
|
+
async uploadFile(label, dataPath, uncompress = false) {
|
|
302
|
+
const filePath = path_1.default.join(config_1.STORE_PATH, this.storeId, dataPath);
|
|
303
|
+
const { nonce, fileExists } = await this.getFileNonce(dataPath);
|
|
304
|
+
if (fileExists) {
|
|
305
|
+
console.log((0, colorette_1.blue)(`File ${label} already exists. Skipping upload.`));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const wallet = await blockchain_1.Wallet.load("default");
|
|
309
|
+
const keyOwnershipSig = await wallet.createKeyOwnershipSignature(nonce);
|
|
310
|
+
const publicKey = await wallet.getPublicSyntheticKey();
|
|
311
|
+
// Get the file size
|
|
312
|
+
const fileSize = fs_1.default.statSync(filePath).size;
|
|
313
|
+
let progressBar;
|
|
314
|
+
try {
|
|
315
|
+
// Create a new progress bar for the file
|
|
316
|
+
progressBar = new cli_progress_1.default.SingleBar({
|
|
317
|
+
format: `${(0, colorette_1.blue)("[{bar}]")} | ${(0, colorette_1.yellow)("{filename}")} | {percentage}% | {value}/{total} bytes`,
|
|
318
|
+
hideCursor: true,
|
|
319
|
+
barsize: 30,
|
|
320
|
+
align: "left",
|
|
321
|
+
autopadding: true,
|
|
322
|
+
noTTYOutput: false,
|
|
323
|
+
stopOnComplete: true,
|
|
324
|
+
clearOnComplete: false,
|
|
325
|
+
}, cli_progress_1.default.Presets.legacy);
|
|
326
|
+
progressBar.start(fileSize, 0, {
|
|
327
|
+
filename: formatFilename(label),
|
|
204
328
|
});
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
329
|
+
// Create the read stream
|
|
330
|
+
const fileReadStream = fs_1.default.createReadStream(filePath);
|
|
331
|
+
// Create a progress stream
|
|
332
|
+
const progressStream = (0, progress_stream_1.default)({
|
|
333
|
+
length: fileSize,
|
|
334
|
+
time: 500, // Adjust as needed
|
|
208
335
|
});
|
|
209
|
-
|
|
210
|
-
|
|
336
|
+
progressStream.on("progress", (progress) => {
|
|
337
|
+
progressBar.update(progress.transferred);
|
|
211
338
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
339
|
+
// Add inactivity timeout to the progress stream
|
|
340
|
+
const passThroughStream = this.addInactivityTimeout(new stream_1.PassThrough(), PropagationServer.inactivityTimeout);
|
|
341
|
+
// Decide whether to uncompress the file during upload
|
|
342
|
+
if (uncompress) {
|
|
343
|
+
// Create a gunzip (uncompression) stream
|
|
344
|
+
const gunzip = zlib.createGunzip();
|
|
345
|
+
// Pipe the streams: fileReadStream -> gunzip -> progressStream -> passThroughStream
|
|
346
|
+
fileReadStream
|
|
347
|
+
.pipe(gunzip)
|
|
348
|
+
.pipe(progressStream)
|
|
349
|
+
.pipe(passThroughStream);
|
|
221
350
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
351
|
+
else {
|
|
352
|
+
// Pipe the streams: fileReadStream -> progressStream -> passThroughStream
|
|
353
|
+
fileReadStream.pipe(progressStream).pipe(passThroughStream);
|
|
354
|
+
}
|
|
355
|
+
// Use form-data to construct the request body
|
|
356
|
+
const formData = new form_data_1.default();
|
|
357
|
+
formData.append("file", passThroughStream);
|
|
358
|
+
const headers = {
|
|
359
|
+
...formData.getHeaders(),
|
|
360
|
+
"x-nonce": nonce,
|
|
361
|
+
"x-public-key": publicKey.toString("hex"),
|
|
362
|
+
"x-key-ownership-sig": keyOwnershipSig,
|
|
363
|
+
};
|
|
364
|
+
const config = {
|
|
365
|
+
headers: headers,
|
|
366
|
+
httpsAgent: this.createHttpsAgent(),
|
|
367
|
+
maxContentLength: Infinity,
|
|
368
|
+
maxBodyLength: Infinity,
|
|
369
|
+
};
|
|
370
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${dataPath}`;
|
|
371
|
+
// Create a promise that resolves when the progress stream ends
|
|
372
|
+
const progressPromise = new Promise((resolve, reject) => {
|
|
373
|
+
progressStream.on("end", resolve);
|
|
374
|
+
progressStream.on("error", reject);
|
|
375
|
+
});
|
|
376
|
+
// Start the upload request
|
|
377
|
+
const uploadPromise = axios_1.default.put(url, formData, config);
|
|
378
|
+
// Wait for both the upload and the progress stream to finish
|
|
379
|
+
await Promise.all([uploadPromise, progressPromise]);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
if (progressBar) {
|
|
386
|
+
progressBar.stop();
|
|
232
387
|
}
|
|
233
|
-
attempt++;
|
|
234
388
|
}
|
|
235
|
-
throw new Error(errorMessage);
|
|
236
389
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
250
|
-
const request = https_1.default.request(options, (response) => {
|
|
251
|
-
let data = "";
|
|
252
|
-
if (response.statusCode === 200) {
|
|
253
|
-
response.on("data", (chunk) => {
|
|
254
|
-
data += chunk;
|
|
255
|
-
});
|
|
256
|
-
response.on("end", () => {
|
|
257
|
-
resolve(data);
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
else if (response.statusCode === 301 || response.statusCode === 302) {
|
|
261
|
-
const redirectUrl = response.headers.location;
|
|
262
|
-
if (redirectUrl) {
|
|
263
|
-
this.fetch(redirectUrl).then(resolve).catch(reject);
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
reject(new Error("Redirected without a location header"));
|
|
390
|
+
/**
|
|
391
|
+
* Commit the upload session by sending a POST request to the server.
|
|
392
|
+
*/
|
|
393
|
+
async commitUploadSession() {
|
|
394
|
+
const spinner = (0, nanospinner_1.createSpinner)(`Committing upload session for store ${this.storeId}...`).start();
|
|
395
|
+
try {
|
|
396
|
+
const config = {
|
|
397
|
+
httpsAgent: this.createHttpsAgent(),
|
|
398
|
+
auth: this.username && this.password
|
|
399
|
+
? {
|
|
400
|
+
username: this.username,
|
|
401
|
+
password: this.password,
|
|
267
402
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
console.error(`Request error for ${url}:`, error);
|
|
275
|
-
reject(error);
|
|
403
|
+
: undefined,
|
|
404
|
+
};
|
|
405
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/commit/${this.storeId}/${this.sessionId}`;
|
|
406
|
+
const response = await axios_1.default.post(url, {}, config);
|
|
407
|
+
spinner.success({
|
|
408
|
+
text: (0, colorette_1.green)(`Upload session ${this.sessionId} successfully committed.`),
|
|
276
409
|
});
|
|
277
|
-
|
|
410
|
+
return response.data;
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
spinner.error({ text: (0, colorette_1.red)("Error committing upload session:") });
|
|
414
|
+
console.error((0, colorette_1.red)(error.message));
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Static function to handle the entire upload process for multiple files based on rootHash.
|
|
420
|
+
*/
|
|
421
|
+
static async uploadStore(storeId, rootHash, ipAddress) {
|
|
422
|
+
const propagationServer = new PropagationServer(ipAddress, storeId);
|
|
423
|
+
// Initialize wallet
|
|
424
|
+
await propagationServer.initializeWallet();
|
|
425
|
+
// Check if the store exists
|
|
426
|
+
const { storeExists, rootHashExists } = await propagationServer.checkStoreExists(rootHash);
|
|
427
|
+
// If the store does not exist, prompt for credentials
|
|
428
|
+
if (!storeExists) {
|
|
429
|
+
console.log((0, colorette_1.red)(`Store ${storeId} does not exist. Prompting for credentials...`));
|
|
430
|
+
const credentials = await (0, credentialsUtils_1.promptCredentials)(propagationServer.ipAddress);
|
|
431
|
+
propagationServer.username = credentials.username;
|
|
432
|
+
propagationServer.password = credentials.password;
|
|
433
|
+
}
|
|
434
|
+
if (rootHashExists) {
|
|
435
|
+
console.log((0, colorette_1.blue)(`Root hash ${rootHash} already exists in the store. Skipping upload.`));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Start the upload session
|
|
439
|
+
await propagationServer.startUploadSession(rootHash);
|
|
440
|
+
const dataStore = blockchain_1.DataStore.from(storeId);
|
|
441
|
+
const files = await dataStore.getFileSetForRootHash(rootHash, true);
|
|
442
|
+
// Prepare upload tasks
|
|
443
|
+
const uploadTasks = files.map((file) => ({
|
|
444
|
+
label: file.name,
|
|
445
|
+
dataPath: file.path,
|
|
446
|
+
}));
|
|
447
|
+
// Limit the number of concurrent uploads
|
|
448
|
+
const concurrencyLimit = 10; // Adjust this number as needed
|
|
449
|
+
await (0, promiseUtils_1.asyncPool)(concurrencyLimit, uploadTasks, async (task) => {
|
|
450
|
+
await propagationServer.uploadFile(task.label, task.dataPath, true);
|
|
278
451
|
});
|
|
452
|
+
// Commit the session after all files have been uploaded
|
|
453
|
+
await propagationServer.commitUploadSession();
|
|
454
|
+
console.log((0, colorette_1.green)(`✔ All files have been uploaded to for Root Hash: ${(0, colorette_1.cyan)(rootHash)}.`));
|
|
279
455
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Fetch a file from the server by sending a GET request and return its content in memory.
|
|
458
|
+
* Logs progress using a local cli-progress bar.
|
|
459
|
+
*/
|
|
460
|
+
async fetchFile(dataPath) {
|
|
461
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/fetch/${this.storeId}/${dataPath}`;
|
|
462
|
+
const config = {
|
|
463
|
+
responseType: "stream",
|
|
464
|
+
httpsAgent: this.createHttpsAgent(),
|
|
465
|
+
};
|
|
466
|
+
let progressBar;
|
|
467
|
+
try {
|
|
468
|
+
const response = await axios_1.default.get(url, config);
|
|
469
|
+
const totalLengthHeader = response.headers["content-length"];
|
|
470
|
+
const totalLength = totalLengthHeader
|
|
471
|
+
? parseInt(totalLengthHeader, 10)
|
|
472
|
+
: null;
|
|
473
|
+
if (!totalLength) {
|
|
474
|
+
throw new Error("Content-Length header is missing");
|
|
475
|
+
}
|
|
476
|
+
// Create a new progress bar for the file
|
|
477
|
+
progressBar = new cli_progress_1.default.SingleBar({
|
|
478
|
+
format: `${(0, colorette_1.blue)("[{bar}]")} | ${(0, colorette_1.yellow)("{filename}")} | {percentage}% | {value}/{total} bytes`,
|
|
479
|
+
hideCursor: true,
|
|
480
|
+
barsize: 30,
|
|
481
|
+
align: "left",
|
|
482
|
+
autopadding: true,
|
|
483
|
+
noTTYOutput: false,
|
|
484
|
+
stopOnComplete: true,
|
|
485
|
+
clearOnComplete: false,
|
|
486
|
+
}, cli_progress_1.default.Presets.legacy);
|
|
487
|
+
progressBar.start(totalLength, 0, {
|
|
488
|
+
filename: formatFilename(dataPath),
|
|
300
489
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
490
|
+
let dataBuffers = [];
|
|
491
|
+
const progressStream = (0, progress_stream_1.default)({
|
|
492
|
+
length: totalLength,
|
|
493
|
+
time: 500, // Adjust as needed
|
|
304
494
|
});
|
|
305
|
-
|
|
306
|
-
|
|
495
|
+
progressStream.on("progress", (progress) => {
|
|
496
|
+
progressBar.update(progress.transferred);
|
|
497
|
+
});
|
|
498
|
+
// Add inactivity timeout to the progress stream
|
|
499
|
+
const passThroughStream = this.addInactivityTimeout(new stream_1.PassThrough(), PropagationServer.inactivityTimeout);
|
|
500
|
+
response.data.pipe(progressStream).pipe(passThroughStream);
|
|
501
|
+
progressStream.on("data", (chunk) => {
|
|
502
|
+
dataBuffers.push(chunk);
|
|
503
|
+
});
|
|
504
|
+
// Wait for the progress stream to finish
|
|
505
|
+
await new Promise((resolve, reject) => {
|
|
506
|
+
progressStream.on("end", resolve);
|
|
507
|
+
progressStream.on("error", reject);
|
|
508
|
+
});
|
|
509
|
+
return Buffer.concat(dataBuffers);
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
storeExistsCache.del(`${this.storeId}-nohash`);
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
if (progressBar) {
|
|
517
|
+
progressBar.stop();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
307
520
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
521
|
+
/**
|
|
522
|
+
* Get details of a file, including whether it exists and its size.
|
|
523
|
+
*/
|
|
524
|
+
async getFileDetails(dataPath, rootHash) {
|
|
525
|
+
try {
|
|
526
|
+
const config = {
|
|
527
|
+
httpsAgent: this.createHttpsAgent(),
|
|
528
|
+
};
|
|
529
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/store/${this.storeId}/${rootHash}/${dataPath}`;
|
|
530
|
+
const response = await axios_1.default.head(url, config);
|
|
531
|
+
// Check the headers for file existence and size
|
|
532
|
+
const fileExists = response.headers["x-file-exists"] === "true";
|
|
533
|
+
const fileSize = parseInt(response.headers["x-file-size"], 10);
|
|
534
|
+
return {
|
|
535
|
+
exists: fileExists,
|
|
536
|
+
size: fileExists ? fileSize : 0,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
console.error((0, colorette_1.red)(`✖ Error checking file details for ${dataPath}:`), error.message);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
312
543
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Download a file from the server by sending a GET request.
|
|
546
|
+
* Logs progress using a local cli-progress bar.
|
|
547
|
+
*/
|
|
548
|
+
async downloadFile(label, dataPath, rootHash, baseDir) {
|
|
549
|
+
const url = `https://${(0, network_1.formatHost)(this.ipAddress)}:${PropagationServer.port}/fetch/${this.storeId}/${dataPath}`;
|
|
550
|
+
let downloadPath = path_1.default.join(baseDir, dataPath);
|
|
551
|
+
// Ensure that the directory for the file exists
|
|
552
|
+
fs_1.default.mkdirSync(path_1.default.dirname(downloadPath), { recursive: true });
|
|
553
|
+
const config = {
|
|
554
|
+
responseType: "stream",
|
|
555
|
+
httpsAgent: this.createHttpsAgent(),
|
|
556
|
+
};
|
|
557
|
+
let progressBar;
|
|
323
558
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
332
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
333
|
-
delay = Math.min(maxDelay, delay * delayMultiplier);
|
|
334
|
-
} else {
|
|
335
|
-
console.error(`Failed to retrieve data from ${url}. Aborting.`);
|
|
336
|
-
throw new Error(`Failed to retrieve data: ${error.message}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
attempt++;
|
|
340
|
-
}
|
|
341
|
-
throw new Error(
|
|
342
|
-
`Failed to retrieve data from ${url} after ${maxRetries} attempts.`
|
|
343
|
-
);
|
|
344
|
-
}*/
|
|
345
|
-
// Helper method to create a stream with retry logic
|
|
346
|
-
async createReadStreamWithRetries(url) {
|
|
347
|
-
let attempt = 0;
|
|
348
|
-
const maxRetries = 1;
|
|
349
|
-
const initialDelay = PropagationServer.initialDelay;
|
|
350
|
-
const maxDelay = PropagationServer.maxDelay;
|
|
351
|
-
const delayMultiplier = PropagationServer.delayMultiplier;
|
|
352
|
-
let delay = initialDelay;
|
|
353
|
-
while (attempt < maxRetries) {
|
|
354
|
-
try {
|
|
355
|
-
return await this.createReadStream(url); // Stream data chunk by chunk
|
|
559
|
+
const response = await axios_1.default.get(url, config);
|
|
560
|
+
const totalLengthHeader = response.headers["content-length"];
|
|
561
|
+
const totalLength = totalLengthHeader
|
|
562
|
+
? parseInt(totalLengthHeader, 10)
|
|
563
|
+
: null;
|
|
564
|
+
if (!totalLength) {
|
|
565
|
+
throw new Error("Content-Length header is missing");
|
|
356
566
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
567
|
+
// Create a new progress bar for the file
|
|
568
|
+
progressBar = new cli_progress_1.default.SingleBar({
|
|
569
|
+
format: `${(0, colorette_1.blue)("[{bar}]")} | ${(0, colorette_1.yellow)("{filename}")} | {percentage}% | {value}/{total} bytes`,
|
|
570
|
+
hideCursor: true,
|
|
571
|
+
barsize: 30,
|
|
572
|
+
align: "left",
|
|
573
|
+
autopadding: true,
|
|
574
|
+
noTTYOutput: false,
|
|
575
|
+
stopOnComplete: true,
|
|
576
|
+
clearOnComplete: false,
|
|
577
|
+
}, cli_progress_1.default.Presets.legacy);
|
|
578
|
+
progressBar.start(totalLength, 0, {
|
|
579
|
+
filename: formatFilename(label),
|
|
580
|
+
});
|
|
581
|
+
const fileWriteStream = fs_1.default.createWriteStream(downloadPath);
|
|
582
|
+
const progressStream = (0, progress_stream_1.default)({
|
|
583
|
+
length: totalLength,
|
|
584
|
+
time: 500, // Adjust as needed
|
|
585
|
+
});
|
|
586
|
+
progressStream.on("progress", (progress) => {
|
|
587
|
+
progressBar.update(progress.transferred);
|
|
588
|
+
});
|
|
589
|
+
// Add inactivity timeout to the progress stream
|
|
590
|
+
const passThroughStream = this.addInactivityTimeout(new stream_1.PassThrough(), PropagationServer.inactivityTimeout);
|
|
591
|
+
response.data
|
|
592
|
+
.pipe(progressStream)
|
|
593
|
+
.pipe(passThroughStream)
|
|
594
|
+
.pipe(fileWriteStream);
|
|
595
|
+
// Wait for both the file write stream and the progress stream to finish
|
|
596
|
+
await Promise.all([
|
|
597
|
+
new Promise((resolve, reject) => {
|
|
598
|
+
fileWriteStream.on("finish", resolve);
|
|
599
|
+
fileWriteStream.on("error", reject);
|
|
600
|
+
}),
|
|
601
|
+
new Promise((resolve, reject) => {
|
|
602
|
+
progressStream.on("end", resolve);
|
|
603
|
+
progressStream.on("error", reject);
|
|
604
|
+
}),
|
|
605
|
+
]);
|
|
606
|
+
if (dataPath.includes("/data")) {
|
|
607
|
+
const integrity = await (0, merkle_1.merkleIntegrityCheck)(path_1.default.join(baseDir, `${rootHash}.dat`), baseDir, dataPath, rootHash);
|
|
608
|
+
if (!integrity) {
|
|
609
|
+
throw new Error(`Integrity check failed for file: ${dataPath}`);
|
|
366
610
|
}
|
|
611
|
+
console.log("integrity check");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
storeExistsCache.del(`${this.storeId}-nohash`);
|
|
616
|
+
storeExistsCache.del(`${this.storeId}-${rootHash}`);
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
if (progressBar) {
|
|
621
|
+
progressBar.stop();
|
|
367
622
|
}
|
|
368
|
-
attempt++;
|
|
369
623
|
}
|
|
370
|
-
throw new Error(`Failed to create stream from ${url} after ${maxRetries} attempts.`);
|
|
371
624
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Static function to handle downloading multiple files from a DataStore based on file paths.
|
|
627
|
+
*/
|
|
628
|
+
static async downloadStore(storeId, rootHash, ipAddress) {
|
|
629
|
+
const propagationServer = new PropagationServer(ipAddress, storeId);
|
|
630
|
+
// Initialize wallet
|
|
631
|
+
await propagationServer.initializeWallet();
|
|
632
|
+
// Check if the store exists
|
|
633
|
+
const { storeExists, rootHashExists } = await propagationServer.checkStoreExists(rootHash);
|
|
634
|
+
if (!storeExists || !rootHashExists) {
|
|
635
|
+
throw new Error(`Store ${storeId} does not exist.`);
|
|
636
|
+
}
|
|
637
|
+
// Fetch the rootHash.dat file
|
|
638
|
+
const datFileContent = await propagationServer.fetchFile(`${rootHash}.dat`);
|
|
639
|
+
const root = JSON.parse(datFileContent.toString());
|
|
640
|
+
// Prepare download tasks
|
|
641
|
+
const downloadTasks = [];
|
|
642
|
+
for (const [fileKey, fileData] of Object.entries(root.files)) {
|
|
643
|
+
const dataPath = (0, hashUtils_1.getFilePathFromSha256)(root.files[fileKey].sha256, "data");
|
|
644
|
+
const label = Buffer.from(fileKey, "hex").toString("utf-8");
|
|
645
|
+
downloadTasks.push({ label, dataPath });
|
|
646
|
+
}
|
|
647
|
+
// Limit the number of concurrent downloads
|
|
648
|
+
const concurrencyLimit = 10; // Adjust this number as needed
|
|
649
|
+
// Create a temporary directory
|
|
650
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "downloadStore-"));
|
|
651
|
+
try {
|
|
652
|
+
// Download files to the temporary directory
|
|
653
|
+
await (0, promiseUtils_1.asyncPool)(concurrencyLimit, downloadTasks, async (task) => {
|
|
654
|
+
await propagationServer.downloadFile(task.label, task.dataPath, rootHash, tempDir);
|
|
655
|
+
});
|
|
656
|
+
// Save the rootHash.dat file to the temporary directory
|
|
657
|
+
if (!fs_1.default.existsSync(tempDir)) {
|
|
658
|
+
fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
659
|
+
}
|
|
660
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, `${rootHash}.dat`), datFileContent);
|
|
661
|
+
// Integrity check for the downloaded files was done during the download
|
|
662
|
+
// Here we want to make sure we got all the files or we reject the download session
|
|
663
|
+
for (const [fileKey, fileData] of Object.entries(root.files)) {
|
|
664
|
+
const dataPath = (0, hashUtils_1.getFilePathFromSha256)(root.files[fileKey].sha256, "data");
|
|
665
|
+
if (!fs_1.default.existsSync(path_1.default.join(tempDir, dataPath))) {
|
|
666
|
+
if (!fs_1.default.existsSync(path_1.default.join(config_1.STORE_PATH, storeId, dataPath))) {
|
|
667
|
+
throw new Error(`Missing file: ${Buffer.from(fileKey, "hex")}, aborting session.`);
|
|
396
668
|
}
|
|
397
669
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
reject(error);
|
|
670
|
+
}
|
|
671
|
+
// After all downloads are complete, copy from temp directory to the main directory
|
|
672
|
+
const destinationDir = path_1.default.join(config_1.STORE_PATH, storeId);
|
|
673
|
+
fs_extra_1.default.copySync(tempDir, destinationDir, {
|
|
674
|
+
overwrite: false, // Prevents overwriting existing files
|
|
675
|
+
errorOnExist: false, // No error if file already exists
|
|
405
676
|
});
|
|
406
|
-
|
|
407
|
-
|
|
677
|
+
// Generate the manifest file in the main directory
|
|
678
|
+
const dataStore = blockchain_1.DataStore.from(storeId);
|
|
679
|
+
await dataStore.cacheStoreCreationHeight();
|
|
680
|
+
await dataStore.generateManifestFile();
|
|
681
|
+
console.log((0, colorette_1.green)(`✔ All files have been downloaded to ${storeId}.`));
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
console.log((0, colorette_1.red)("✖ Error downloading files:"), error);
|
|
685
|
+
}
|
|
686
|
+
finally {
|
|
687
|
+
// Clean up the temporary directory
|
|
688
|
+
fs_extra_1.default.removeSync(tempDir);
|
|
689
|
+
}
|
|
408
690
|
}
|
|
409
691
|
}
|
|
410
692
|
exports.PropagationServer = PropagationServer;
|
|
411
|
-
PropagationServer.port = 4159;
|
|
412
|
-
PropagationServer.
|
|
413
|
-
PropagationServer.initialDelay = 2000; // 2 seconds
|
|
414
|
-
PropagationServer.maxDelay = 10000; // 10 seconds
|
|
415
|
-
PropagationServer.delayMultiplier = 1.5;
|
|
693
|
+
PropagationServer.port = 4159; // Static port used for all requests
|
|
694
|
+
PropagationServer.inactivityTimeout = 5000; // Inactivity timeout in milliseconds (5 seconds)
|