@dignetwork/dig-sdk 0.0.1-alpha.17 → 0.0.1-alpha.171

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