@dignetwork/dig-sdk 0.0.1-alpha.51 → 0.0.1-alpha.54

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.
@@ -4,444 +4,343 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PropagationServer = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
7
8
  const fs_1 = __importDefault(require("fs"));
8
- const https_1 = __importDefault(require("https"));
9
- const url_1 = require("url");
9
+ const path_1 = __importDefault(require("path"));
10
+ const form_data_1 = __importDefault(require("form-data"));
11
+ const blockchain_1 = require("../blockchain");
10
12
  const ssl_1 = require("../utils/ssl");
11
13
  const credentialsUtils_1 = require("../utils/credentialsUtils");
12
- const blockchain_1 = require("../blockchain");
14
+ const https_1 = __importDefault(require("https"));
15
+ const cli_progress_1 = __importDefault(require("cli-progress"));
16
+ const chalk_1 = __importDefault(require("chalk")); // For colored output
17
+ const ora_1 = __importDefault(require("ora")); // For spinners
18
+ const config_1 = require("../utils/config");
19
+ const hashUtils_1 = require("../utils/hashUtils");
13
20
  class PropagationServer {
14
- constructor(ipAddress, storeId) {
15
- this.ipAddress = ipAddress;
21
+ constructor(storeId, ipAddress) {
16
22
  this.storeId = storeId;
17
- if (!PropagationServer.certPath || !PropagationServer.keyPath) {
18
- const { certPath, keyPath } = (0, ssl_1.getOrCreateSSLCerts)();
19
- PropagationServer.certPath = certPath;
20
- PropagationServer.keyPath = keyPath;
21
- }
22
- }
23
- // Method to upload a file to the propagation server
24
- async pushFile(fileLocation, dataPath) {
25
- const { nonce, username, password } = await this.getUploadDetails();
26
- const wallet = await blockchain_1.Wallet.load("default");
27
- const keyOwnershipSig = await wallet.createKeyOwnershipSignature(nonce);
28
- const publicKey = await wallet.getPublicSyntheticKey();
29
- const uploadUrl = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${dataPath}`;
30
- await this.retryOperation(() => this.uploadFileDirect(fileLocation, uploadUrl, username, password, keyOwnershipSig, publicKey.toString("hex"), nonce), `Upload failed for ${dataPath}`);
31
- }
32
- // Method to subscribe to a store on the propagation server
33
- async subscribeToStore() {
34
- const url = `https://${this.ipAddress}:${PropagationServer.port}/subscribe`;
35
- const data = JSON.stringify({ storeId: this.storeId });
36
- await this.postRequest(url, data);
37
- }
38
- // Method to unsubscribe from a store on the propagation server
39
- async unsubscribeFromStore() {
40
- const url = `https://${this.ipAddress}:${PropagationServer.port}/unsubscribe`;
41
- const data = JSON.stringify({ storeId: this.storeId });
42
- await this.postRequest(url, data);
43
- }
44
- // Method to check the status of a store on the propagation server
45
- async getStoreStatus() {
46
- const url = `https://${this.ipAddress}:${PropagationServer.port}/status/${this.storeId}`;
47
- return this.fetchJson(url);
48
- }
49
- async streamStoreData(dataPath) {
50
- const url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${dataPath}`;
51
- return this.createReadStreamWithRetries(url);
52
- }
53
- // Method to retrieve a store's key
54
- async getStoreData(dataPath) {
55
- const url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${dataPath}`;
56
- return this.fetch(url);
57
- }
58
- async getStatus() {
59
- const url = `https://${this.ipAddress}:${PropagationServer.port}/status/${this.storeId}`;
60
- const statusJson = await this.fetch(url);
61
- return JSON.parse(statusJson);
62
- }
63
- // Method to check if a specific store exists (HEAD request)
64
- async headStore(options) {
65
- let url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}`;
66
- if (options?.hasRootHash) {
67
- url += `?hasRootHash=${options.hasRootHash}`;
68
- }
69
- return this.head(url);
70
- }
71
- // In PropagationServer.ts:
72
- async isStoreSynced() {
73
- const status = await this.getStatus();
74
- return status.synced === true;
75
- }
76
- // Method to handle upload details including nonce
77
- async getUploadDetails() {
78
- let username;
79
- let password;
80
- if (!username || !password) {
81
- const credentials = await (0, credentialsUtils_1.promptCredentials)(this.ipAddress);
82
- username = credentials.username;
83
- password = credentials.password;
84
- }
85
- const uploadDetails = await this.fetchUploadDetails(username, password);
86
- if (!uploadDetails) {
87
- throw new Error("Failed to retrieve upload details.");
88
- }
89
- return { ...uploadDetails, username, password };
23
+ this.sessionId = ""; // Session ID will be set after starting the upload session
24
+ this.publicKey = ""; // Public key will be set after initializing the wallet
25
+ this.ipAddress = ipAddress;
26
+ // Get or create SSL certificates
27
+ const { certPath, keyPath } = (0, ssl_1.getOrCreateSSLCerts)();
28
+ this.certPath = certPath;
29
+ this.keyPath = keyPath;
90
30
  }
91
- // Method to send a POST request
92
- async postRequest(url, data) {
93
- return new Promise((resolve, reject) => {
94
- const urlObj = new url_1.URL(url);
95
- const options = {
96
- hostname: urlObj.hostname,
97
- port: urlObj.port || PropagationServer.port,
98
- path: urlObj.pathname,
99
- method: "POST",
100
- headers: {
101
- "Content-Type": "application/json",
102
- "Content-Length": Buffer.byteLength(data),
103
- },
104
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
105
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
106
- rejectUnauthorized: false,
107
- };
108
- const req = https_1.default.request(options, (res) => {
109
- if (res.statusCode === 200) {
110
- resolve();
111
- }
112
- else {
113
- reject(new Error(`POST request failed with status ${res.statusCode}: ${res.statusMessage}`));
114
- }
115
- });
116
- req.on("error", (err) => {
117
- reject(err);
118
- });
119
- req.write(data);
120
- req.end();
121
- });
31
+ /**
32
+ * Initialize the Wallet instance.
33
+ */
34
+ async initializeWallet() {
35
+ this.wallet = await blockchain_1.Wallet.load("default");
36
+ this.publicKey = await this.wallet.getPrivateSyntheticKey();
122
37
  }
123
- // Method to fetch upload details from the server
124
- // Method to fetch upload details from the server
125
- async fetchUploadDetails(username, password) {
126
- const remote = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}`;
127
- return new Promise((resolve, reject) => {
128
- const url = new url_1.URL(remote);
129
- const options = {
130
- hostname: url.hostname,
131
- port: url.port,
132
- path: url.pathname,
133
- method: "HEAD",
134
- headers: {
135
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
136
- },
137
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
138
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
139
- rejectUnauthorized: false, // Allow self-signed certificates
140
- };
141
- const req = https_1.default.request(options, (res) => {
142
- if (res.statusCode === 200) {
143
- // Check if the headers are present and valid
144
- const nonce = res.headers["x-nonce"];
145
- const lastUploadedHash = res.headers?.["x-last-uploaded-hash"] ||
146
- "0000000000000000000000000000000000000000000000000000000000000000";
147
- const generationIndex = res.headers?.["x-generation-index"] || 0;
148
- console.log({ nonce, lastUploadedHash, generationIndex });
149
- if (nonce) {
150
- resolve({
151
- nonce: nonce,
152
- lastUploadedHash: lastUploadedHash,
153
- generationIndex: Number(generationIndex),
154
- });
155
- }
156
- else {
157
- reject(new Error("Missing required headers in the response."));
158
- }
159
- }
160
- else {
161
- reject(new Error(`Failed to perform preflight check: ${res.statusCode} ${res.statusMessage}`));
162
- }
163
- });
164
- req.on("error", (err) => {
165
- console.error(err.message);
166
- resolve(false); // Resolve to false if there is an error
167
- });
168
- req.end();
38
+ /**
39
+ * Create an Axios HTTPS Agent with self-signed certificate allowance.
40
+ */
41
+ createHttpsAgent() {
42
+ return new https_1.default.Agent({
43
+ cert: fs_1.default.readFileSync(this.certPath),
44
+ key: fs_1.default.readFileSync(this.keyPath),
45
+ rejectUnauthorized: false, // Allow self-signed certificates
169
46
  });
170
47
  }
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",
182
- 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,
189
- },
190
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
191
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
192
- rejectUnauthorized: false,
48
+ /**
49
+ * Check if the store and optional root hash exist by making a HEAD request.
50
+ *
51
+ * @param {string} [rootHash] - Optional root hash to check for existence.
52
+ * @returns {Promise<{ storeExists: boolean, rootHashExists: boolean }>} - An object indicating if the store and root hash exist.
53
+ */
54
+ async checkStoreExists(rootHash) {
55
+ const spinner = (0, ora_1.default)(`Checking if store ${this.storeId} exists...`).start();
56
+ try {
57
+ const config = {
58
+ httpsAgent: this.createHttpsAgent(),
193
59
  };
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
- }
201
- });
202
- req.on("error", (err) => {
203
- reject(err);
204
- });
205
- fileStream.pipe(req);
206
- fileStream.on("error", (err) => {
207
- reject(err);
208
- });
209
- req.on("end", () => {
210
- resolve();
211
- });
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();
60
+ let url = `https://${this.ipAddress}:${PropagationServer.port}/stores/${this.storeId}`;
61
+ if (rootHash) {
62
+ url += `?hasRootHash=${rootHash}`;
63
+ }
64
+ const response = await axios_1.default.head(url, config);
65
+ // Extract store existence and root hash existence from headers
66
+ const storeExists = response.headers["x-store-exists"] === "true";
67
+ const rootHashExists = response.headers["x-has-root-hash"] === "true";
68
+ if (storeExists) {
69
+ spinner.succeed(chalk_1.default.green(`Store ${this.storeId} exists!`));
221
70
  }
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);
71
+ else {
72
+ spinner.fail(chalk_1.default.red(`Store ${this.storeId} does not exist.`));
73
+ }
74
+ if (rootHash) {
75
+ if (rootHashExists) {
76
+ console.log(chalk_1.default.green(`Root hash ${rootHash} exists in the store.`));
227
77
  }
228
78
  else {
229
- console.error(`${errorMessage}. Aborting.`);
230
- throw error;
79
+ console.log(chalk_1.default.red(`Root hash ${rootHash} does not exist in the store.`));
231
80
  }
232
81
  }
233
- attempt++;
82
+ return { storeExists, rootHashExists };
83
+ }
84
+ catch (error) {
85
+ spinner.fail(chalk_1.default.red("Error checking if store exists:"));
86
+ console.error(chalk_1.default.red(error));
87
+ throw error;
234
88
  }
235
- throw new Error(errorMessage);
236
89
  }
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,
90
+ /**
91
+ * Start an upload session by sending a POST request to the server.
92
+ */
93
+ async startUploadSession() {
94
+ const spinner = (0, ora_1.default)(`Starting upload session for store ${this.storeId}...`).start();
95
+ try {
96
+ const config = {
97
+ httpsAgent: this.createHttpsAgent(),
249
98
  };
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"));
267
- }
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(`GET Request error for ${url}:`, error);
275
- reject(error);
276
- });
277
- request.end();
278
- });
279
- }
280
- // Helper method to perform HEAD requests
281
- async head(url, maxRedirects = 5) {
282
- return new Promise((resolve, reject) => {
283
- try {
284
- // Parse the input URL
285
- const urlObj = new url_1.URL(url);
286
- const requestOptions = {
287
- hostname: urlObj.hostname,
288
- port: urlObj.port || (urlObj.protocol === "https:" ? 443 : undefined),
289
- path: urlObj.pathname + urlObj.search,
290
- method: "HEAD",
291
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
292
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
293
- rejectUnauthorized: false,
99
+ // Add Basic Auth headers if credentials are provided
100
+ if (this.username && this.password) {
101
+ config.auth = {
102
+ username: this.username,
103
+ password: this.password,
294
104
  };
295
- const request = https_1.default.request(requestOptions, (response) => {
296
- const { statusCode, headers } = response;
297
- // If status code is 2xx, return success
298
- if (statusCode && statusCode >= 200 && statusCode < 300) {
299
- resolve({ success: true, headers });
300
- }
301
- // Handle 3xx redirection
302
- else if (statusCode && statusCode >= 300 && statusCode < 400 && headers.location) {
303
- if (maxRedirects > 0) {
304
- let redirectUrl = headers.location;
305
- // Check if the redirect URL is relative
306
- if (!/^https?:\/\//i.test(redirectUrl)) {
307
- // Resolve the relative URL based on the original URL
308
- redirectUrl = new url_1.URL(redirectUrl, url).toString();
309
- console.log(`Resolved relative redirect to: ${redirectUrl}`);
310
- }
311
- else {
312
- console.log(`Redirecting to: ${redirectUrl}`);
313
- }
314
- // Recursively follow the redirection
315
- this.head(redirectUrl, maxRedirects - 1)
316
- .then(resolve)
317
- .catch(reject);
318
- }
319
- else {
320
- reject({ success: false, message: "Too many redirects" });
321
- }
322
- }
323
- else {
324
- // For other status codes, consider it a failure
325
- resolve({ success: false });
326
- }
327
- });
328
- request.on("error", (error) => {
329
- console.error(`HEAD ${url}:`, error.message);
330
- reject({ success: false });
331
- });
332
- request.end();
333
- }
334
- catch (err) {
335
- console.error(`Invalid URL: ${url}`, err.message);
336
- reject({ success: false, message: "Invalid URL" });
337
105
  }
106
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}`;
107
+ const response = await axios_1.default.post(url, { publicKey: this.publicKey }, config);
108
+ this.sessionId = response.data.sessionId;
109
+ spinner.succeed(chalk_1.default.green(`Upload session started for DataStore ${this.storeId} with session ID ${this.sessionId}`));
110
+ }
111
+ catch (error) {
112
+ spinner.fail(chalk_1.default.red("Error starting upload session:"));
113
+ console.error(chalk_1.default.red(error));
114
+ throw error;
115
+ }
116
+ }
117
+ /**
118
+ * Request a nonce for a file by sending a HEAD request to the server.
119
+ */
120
+ async getFileNonce(filename) {
121
+ try {
122
+ const config = {
123
+ httpsAgent: this.createHttpsAgent(),
124
+ };
125
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${filename}`;
126
+ const response = await axios_1.default.head(url, config);
127
+ const nonce = response.headers["x-nonce"];
128
+ console.log(chalk_1.default.blue(`Nonce received for file ${filename}: ${nonce}`));
129
+ return nonce;
130
+ }
131
+ catch (error) {
132
+ console.error(chalk_1.default.red(`Error generating nonce for file ${filename}:`), error);
133
+ throw error;
134
+ }
135
+ }
136
+ /**
137
+ * Upload a file to the server by sending a PUT request.
138
+ * Logs progress using cli-progress for each file.
139
+ */
140
+ async uploadFile(filePath) {
141
+ const filename = path_1.default.basename(filePath);
142
+ const nonce = await this.getFileNonce(filename);
143
+ const keyOwnershipSig = await this.wallet.createKeyOwnershipSignature(nonce);
144
+ const formData = new form_data_1.default();
145
+ formData.append("file", fs_1.default.createReadStream(filePath));
146
+ const fileSize = fs_1.default.statSync(filePath).size;
147
+ // Create a new progress bar for each file
148
+ const progressBar = PropagationServer.multiBar.create(fileSize, 0, {
149
+ filename: chalk_1.default.yellow(filename),
150
+ percentage: 0,
338
151
  });
152
+ let uploadedBytes = 0;
153
+ const config = {
154
+ headers: {
155
+ "Content-Type": "multipart/form-data",
156
+ "x-nonce": nonce,
157
+ "x-public-key": this.publicKey,
158
+ "x-key-ownership-sig": keyOwnershipSig,
159
+ ...formData.getHeaders(),
160
+ },
161
+ httpsAgent: this.createHttpsAgent(),
162
+ // Tracking upload progress and updating the progress bar
163
+ onUploadProgress: (progressEvent) => {
164
+ uploadedBytes += progressEvent.loaded;
165
+ const percentage = Math.round((uploadedBytes / fileSize) * 100);
166
+ progressBar.update(uploadedBytes, { percentage });
167
+ },
168
+ };
169
+ try {
170
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${filename}`;
171
+ const response = await axios_1.default.put(url, formData, config);
172
+ console.log(chalk_1.default.green(`✔ File ${filename} uploaded successfully.`));
173
+ // Complete the progress bar
174
+ progressBar.update(fileSize, { percentage: 100 });
175
+ progressBar.stop();
176
+ return response.data;
177
+ }
178
+ catch (error) {
179
+ console.error(chalk_1.default.red(`✖ Error uploading file ${filename}:`), error);
180
+ progressBar.stop(); // Stop the progress bar in case of error
181
+ throw error;
182
+ }
339
183
  }
340
- // Helper method to fetch JSON data from a URL
341
- async fetchJson(url) {
342
- const response = await this.fetch(url);
343
- return JSON.parse(response);
184
+ /**
185
+ * Static function to handle the entire upload process for multiple files based on rootHash.
186
+ * @param {string} storeId - The ID of the DataStore.
187
+ * @param {string} rootHash - The root hash used to derive the file set.
188
+ * @param {string} publicKey - The public key of the user.
189
+ * @param {string} ipAddress - The IP address of the server.
190
+ */
191
+ static async uploadStore(storeId, rootHash, ipAddress) {
192
+ const propagationServer = new PropagationServer(storeId, ipAddress);
193
+ // Initialize wallet
194
+ await propagationServer.initializeWallet();
195
+ // Check if the store exists
196
+ const storeExists = await propagationServer.checkStoreExists();
197
+ // If the store does not exist, prompt for credentials
198
+ if (!storeExists) {
199
+ console.log(chalk_1.default.red(`Store ${storeId} does not exist. Prompting for credentials...`));
200
+ const credentials = await (0, credentialsUtils_1.promptCredentials)(propagationServer.ipAddress);
201
+ propagationServer.username = credentials.username;
202
+ propagationServer.password = credentials.password;
203
+ }
204
+ // Start the upload session
205
+ await propagationServer.startUploadSession();
206
+ const dataStore = blockchain_1.DataStore.from(storeId);
207
+ const filePaths = await dataStore.getFileSetForRootHash(rootHash);
208
+ // Upload each file
209
+ for (const filePath of filePaths) {
210
+ await propagationServer.uploadFile(filePath);
211
+ }
212
+ // Stop all progress bars after the files are uploaded
213
+ PropagationServer.multiBar.stop();
214
+ console.log(chalk_1.default.green(`✔ All files have been uploaded to DataStore ${storeId}.`));
344
215
  }
345
- // Helper method to fetch content with retries and redirection handling
346
- /*private async fetchWithRetries(url: string): Promise<string> {
347
- let attempt = 0;
348
- const maxRetries = 5;
349
- const initialDelay = 2000; // 2 seconds
350
- const maxDelay = 10000; // 10 seconds
351
- const delayMultiplier = 1.5;
352
- let delay = initialDelay;
353
-
354
- while (attempt < maxRetries) {
216
+ /**
217
+ * Fetch a file from the server by sending a GET request and return its content in memory.
218
+ * Logs progress using cli-progress.
219
+ * @param {string} dataPath - The data path of the file to download.
220
+ * @returns {Promise<Buffer>} - The file content in memory as a Buffer.
221
+ */
222
+ async fetchFile(dataPath) {
223
+ const config = {
224
+ responseType: "arraybuffer", // To store the file content in memory
225
+ httpsAgent: this.createHttpsAgent(),
226
+ };
227
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/fetch/${this.storeId}/${dataPath}`;
355
228
  try {
356
- return await this.fetch(url);
357
- } catch (error: any) {
358
- if (attempt < maxRetries - 1) {
359
- console.warn(
360
- `Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${
361
- delay / 1000
362
- } seconds...`
363
- );
364
- await new Promise((resolve) => setTimeout(resolve, delay));
365
- delay = Math.min(maxDelay, delay * delayMultiplier);
366
- } else {
367
- console.error(`Failed to retrieve data from ${url}. Aborting.`);
368
- throw new Error(`Failed to retrieve data: ${error.message}`);
369
- }
229
+ const response = await axios_1.default.get(url, config);
230
+ const totalLength = parseInt(response.headers["content-length"], 10);
231
+ console.log(chalk_1.default.cyan(`Starting fetch for ${dataPath}...`));
232
+ // Create a progress bar for the download
233
+ const progressBar = PropagationServer.multiBar.create(totalLength, 0, {
234
+ dataPath: chalk_1.default.yellow(dataPath),
235
+ percentage: 0,
236
+ });
237
+ let downloadedBytes = 0;
238
+ // Track progress of downloading the file
239
+ response.data.on("data", (chunk) => {
240
+ downloadedBytes += chunk.length;
241
+ progressBar.update(downloadedBytes, {
242
+ percentage: Math.round((downloadedBytes / totalLength) * 100),
243
+ });
244
+ });
245
+ // Complete the progress bar once done
246
+ progressBar.update(totalLength, { percentage: 100 });
247
+ progressBar.stop();
248
+ console.log(chalk_1.default.green(`✔ File ${dataPath} fetched successfully.`));
249
+ // Return the file contents as a Buffer
250
+ return Buffer.from(response.data);
370
251
  }
371
- attempt++;
372
- }
373
- throw new Error(
374
- `Failed to retrieve data from ${url} after ${maxRetries} attempts.`
375
- );
376
- }*/
377
- // Helper method to create a stream with retry logic
378
- async createReadStreamWithRetries(url) {
379
- let attempt = 0;
380
- const maxRetries = 1;
381
- const initialDelay = PropagationServer.initialDelay;
382
- const maxDelay = PropagationServer.maxDelay;
383
- const delayMultiplier = PropagationServer.delayMultiplier;
384
- let delay = initialDelay;
385
- while (attempt < maxRetries) {
386
- try {
387
- return await this.createReadStream(url); // Stream data chunk by chunk
388
- }
389
- catch (error) {
390
- if (attempt < maxRetries - 1) {
391
- console.warn(`Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${delay / 1000} seconds...`);
392
- await new Promise((resolve) => setTimeout(resolve, delay));
393
- delay = Math.min(maxDelay, delay * delayMultiplier);
394
- }
395
- else {
396
- console.error(`Failed to create stream from ${url}. Aborting.`);
397
- throw new Error(`Failed to create stream: ${error.message}`);
398
- }
399
- }
400
- attempt++;
252
+ catch (error) {
253
+ console.error(chalk_1.default.red(`✖ Error fetching file ${dataPath}:`), error);
254
+ throw error;
401
255
  }
402
- throw new Error(`Failed to create stream from ${url} after ${maxRetries} attempts.`);
403
256
  }
404
- // Helper method to fetch a readable stream from a URL
405
- async createReadStream(url) {
406
- return new Promise((resolve, reject) => {
407
- const urlObj = new url_1.URL(url);
408
- const options = {
409
- hostname: urlObj.hostname,
410
- port: urlObj.port || PropagationServer.port,
411
- path: urlObj.pathname + urlObj.search,
412
- method: "GET",
413
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
414
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
415
- rejectUnauthorized: false,
416
- };
417
- const request = https_1.default.request(options, (response) => {
418
- if (response.statusCode === 200) {
419
- resolve(response); // Return the response stream chunk by chunk
420
- }
421
- else if (response.statusCode === 301 || response.statusCode === 302) {
422
- const redirectUrl = response.headers.location;
423
- if (redirectUrl) {
424
- this.createReadStream(redirectUrl).then(resolve).catch(reject);
425
- }
426
- else {
427
- reject(new Error("Redirected without a location header"));
428
- }
429
- }
430
- else {
431
- reject(new Error(`Failed to retrieve stream from ${url}. Status code: ${response.statusCode}`));
432
- }
257
+ /**
258
+ * Download a file from the server by sending a GET request.
259
+ * Logs progress using cli-progress.
260
+ * @param {string} dataPath - The data path of the file to download.
261
+ */
262
+ async downloadFile(dataPath) {
263
+ const config = {
264
+ responseType: "stream", // Make sure the response is streamed
265
+ httpsAgent: this.createHttpsAgent(),
266
+ };
267
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/fetch/${this.storeId}/${dataPath}`;
268
+ const downloadPath = path_1.default.join(config_1.STORE_PATH, this.storeId, dataPath); // Save to the correct store directory
269
+ // Ensure that the directory for the file exists
270
+ fs_1.default.mkdirSync(path_1.default.dirname(downloadPath), { recursive: true });
271
+ const fileWriteStream = fs_1.default.createWriteStream(downloadPath);
272
+ try {
273
+ const response = await axios_1.default.get(url, config);
274
+ const totalLength = parseInt(response.headers["content-length"], 10);
275
+ console.log(chalk_1.default.cyan(`Starting download for ${dataPath}...`));
276
+ // Create a progress bar for the download
277
+ const progressBar = PropagationServer.multiBar.create(totalLength, 0, {
278
+ dataPath: chalk_1.default.yellow(dataPath),
279
+ percentage: 0,
433
280
  });
434
- request.on("error", (error) => {
435
- console.error(`GET ${url}:`, error.message);
436
- reject(error);
281
+ let downloadedBytes = 0;
282
+ // Pipe the response data to the file stream and track progress
283
+ response.data.on("data", (chunk) => {
284
+ downloadedBytes += chunk.length;
285
+ progressBar.update(downloadedBytes, {
286
+ percentage: Math.round((downloadedBytes / totalLength) * 100),
287
+ });
437
288
  });
438
- request.end();
439
- });
289
+ // Pipe the data into the file and finalize
290
+ response.data.pipe(fileWriteStream);
291
+ return new Promise((resolve, reject) => {
292
+ fileWriteStream.on("finish", () => {
293
+ progressBar.update(totalLength, { percentage: 100 });
294
+ progressBar.stop();
295
+ console.log(chalk_1.default.green(`✔ File ${dataPath} downloaded successfully to ${downloadPath}.`));
296
+ resolve();
297
+ });
298
+ fileWriteStream.on("error", (error) => {
299
+ progressBar.stop();
300
+ console.error(chalk_1.default.red(`✖ Error downloading file ${dataPath}:`), error);
301
+ reject(error);
302
+ });
303
+ });
304
+ }
305
+ catch (error) {
306
+ console.error(chalk_1.default.red(`✖ Error downloading file ${dataPath}:`), error);
307
+ throw error;
308
+ }
309
+ }
310
+ /**
311
+ * Static function to handle downloading multiple files from a DataStore based on file paths.
312
+ * @param {string} storeId - The ID of the DataStore.
313
+ * @param {string[]} dataPaths - The list of data paths to download.
314
+ * @param {string} ipAddress - The IP address of the server.
315
+ */
316
+ static async downloadStore(storeId, rootHash, ipAddress) {
317
+ const propagationServer = new PropagationServer(storeId, ipAddress);
318
+ // Initialize wallet
319
+ await propagationServer.initializeWallet();
320
+ // Check if the store exists
321
+ const storeExists = await propagationServer.checkStoreExists();
322
+ if (!storeExists) {
323
+ throw new Error(`Store ${storeId} does not exist.`);
324
+ }
325
+ await propagationServer.downloadFile("height.json");
326
+ const datFileContent = await propagationServer.fetchFile(`${rootHash}.dat`);
327
+ const root = JSON.parse(datFileContent.toString());
328
+ for (const [fileKey, fileData] of Object.entries(root.files)) {
329
+ const dataPath = (0, hashUtils_1.getFilePathFromSha256)(root.files[fileKey].sha256, "data");
330
+ await propagationServer.downloadFile(dataPath);
331
+ }
332
+ console.log(chalk_1.default.green(`✔ All files have been downloaded to ${storeId}.`));
440
333
  }
441
334
  }
442
335
  exports.PropagationServer = PropagationServer;
443
- PropagationServer.port = 4159;
444
- PropagationServer.maxRetries = 5;
445
- PropagationServer.initialDelay = 2000; // 2 seconds
446
- PropagationServer.maxDelay = 10000; // 10 seconds
447
- PropagationServer.delayMultiplier = 1.5;
336
+ PropagationServer.port = 4159; // Static port used for all requests
337
+ // MultiBar for handling multiple progress bars
338
+ PropagationServer.multiBar = new cli_progress_1.default.MultiBar({
339
+ clearOnComplete: false,
340
+ hideCursor: true,
341
+ format: chalk_1.default.green(" {bar} ") +
342
+ chalk_1.default.cyan(" | {filename} | {percentage}% | {value}/{total} bytes"),
343
+ barsize: 40,
344
+ stopOnComplete: true,
345
+ align: "center",
346
+ }, cli_progress_1.default.Presets.shades_classic);