@dignetwork/dig-sdk 0.0.1-alpha.52 → 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,447 +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 wallet = await blockchain_1.Wallet.load("default");
86
- const publicKey = await wallet.getPublicSyntheticKey();
87
- const uploadDetails = await this.fetchUploadDetails(username, password, publicKey.toString("hex"));
88
- if (!uploadDetails) {
89
- throw new Error("Failed to retrieve upload details.");
90
- }
91
- 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;
92
30
  }
93
- // Method to send a POST request
94
- async postRequest(url, data) {
95
- return new Promise((resolve, reject) => {
96
- const urlObj = new url_1.URL(url);
97
- const options = {
98
- hostname: urlObj.hostname,
99
- port: urlObj.port || PropagationServer.port,
100
- path: urlObj.pathname,
101
- method: "POST",
102
- headers: {
103
- "Content-Type": "application/json",
104
- "Content-Length": Buffer.byteLength(data),
105
- },
106
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
107
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
108
- rejectUnauthorized: false,
109
- };
110
- const req = https_1.default.request(options, (res) => {
111
- if (res.statusCode === 200) {
112
- resolve();
113
- }
114
- else {
115
- reject(new Error(`POST request failed with status ${res.statusCode}: ${res.statusMessage}`));
116
- }
117
- });
118
- req.on("error", (err) => {
119
- reject(err);
120
- });
121
- req.write(data);
122
- req.end();
123
- });
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();
124
37
  }
125
- // Method to fetch upload details from the server
126
- // Method to fetch upload details from the server
127
- async fetchUploadDetails(username, password, publicKey) {
128
- const remote = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}`;
129
- return new Promise((resolve, reject) => {
130
- const url = new url_1.URL(remote);
131
- const options = {
132
- hostname: url.hostname,
133
- port: url.port,
134
- path: url.pathname,
135
- method: "HEAD",
136
- headers: {
137
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
138
- 'X-Public-Key': publicKey,
139
- },
140
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
141
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
142
- rejectUnauthorized: false, // Allow self-signed certificates
143
- };
144
- const req = https_1.default.request(options, (res) => {
145
- if (res.statusCode === 200) {
146
- // Check if the headers are present and valid
147
- const nonce = res.headers["x-nonce"];
148
- const lastUploadedHash = res.headers?.["x-last-uploaded-hash"] ||
149
- "0000000000000000000000000000000000000000000000000000000000000000";
150
- const generationIndex = res.headers?.["x-generation-index"] || 0;
151
- console.log({ nonce, lastUploadedHash, generationIndex });
152
- if (nonce) {
153
- resolve({
154
- nonce: nonce,
155
- lastUploadedHash: lastUploadedHash,
156
- generationIndex: Number(generationIndex),
157
- });
158
- }
159
- else {
160
- reject(new Error("Missing required headers in the response."));
161
- }
162
- }
163
- else {
164
- reject(new Error(`Failed to perform preflight check: ${res.statusCode} ${res.statusMessage}`));
165
- }
166
- });
167
- req.on("error", (err) => {
168
- console.error(err.message);
169
- resolve(false); // Resolve to false if there is an error
170
- });
171
- 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
172
46
  });
173
47
  }
174
- // Core method to upload a file directly to the server using a stream
175
- async uploadFileDirect(filePath, uploadUrl, username, password, keyOwnershipSig, publicKey, nonce) {
176
- const fileStream = fs_1.default.createReadStream(filePath);
177
- const fileSize = fs_1.default.statSync(filePath).size;
178
- return new Promise((resolve, reject) => {
179
- const url = new url_1.URL(uploadUrl);
180
- const options = {
181
- hostname: url.hostname,
182
- port: url.port,
183
- path: url.pathname,
184
- method: "PUT",
185
- headers: {
186
- "Content-Type": "application/octet-stream",
187
- "Content-Length": fileSize,
188
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
189
- "x-key-ownership-sig": keyOwnershipSig,
190
- "x-public-key": publicKey,
191
- "x-nonce": nonce,
192
- },
193
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
194
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
195
- 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(),
196
59
  };
197
- const req = https_1.default.request(options, (res) => {
198
- if (res.statusCode === 200) {
199
- resolve();
200
- }
201
- else {
202
- reject(new Error(`Upload failed with status ${res.statusCode}: ${res.statusMessage}`));
203
- }
204
- });
205
- req.on("error", (err) => {
206
- reject(err);
207
- });
208
- fileStream.pipe(req);
209
- fileStream.on("error", (err) => {
210
- reject(err);
211
- });
212
- req.on("end", () => {
213
- resolve();
214
- });
215
- });
216
- }
217
- // Generic retry operation handler to reduce redundancy
218
- async retryOperation(operation, errorMessage) {
219
- let attempt = 0;
220
- let delay = PropagationServer.initialDelay;
221
- while (attempt < PropagationServer.maxRetries) {
222
- try {
223
- 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!`));
224
70
  }
225
- catch (error) {
226
- if (attempt < PropagationServer.maxRetries - 1) {
227
- console.warn(`Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${delay / 1000} seconds...`);
228
- await new Promise((resolve) => setTimeout(resolve, delay));
229
- 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.`));
230
77
  }
231
78
  else {
232
- console.error(`${errorMessage}. Aborting.`);
233
- throw error;
79
+ console.log(chalk_1.default.red(`Root hash ${rootHash} does not exist in the store.`));
234
80
  }
235
81
  }
236
- 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;
237
88
  }
238
- throw new Error(errorMessage);
239
89
  }
240
- // Core method to fetch content from a URL
241
- async fetch(url) {
242
- return new Promise((resolve, reject) => {
243
- const urlObj = new url_1.URL(url);
244
- const options = {
245
- hostname: urlObj.hostname,
246
- port: urlObj.port,
247
- path: urlObj.pathname + urlObj.search,
248
- method: "GET",
249
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
250
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
251
- 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(),
252
98
  };
253
- const request = https_1.default.request(options, (response) => {
254
- let data = "";
255
- if (response.statusCode === 200) {
256
- response.on("data", (chunk) => {
257
- data += chunk;
258
- });
259
- response.on("end", () => {
260
- resolve(data);
261
- });
262
- }
263
- else if (response.statusCode === 301 || response.statusCode === 302) {
264
- const redirectUrl = response.headers.location;
265
- if (redirectUrl) {
266
- this.fetch(redirectUrl).then(resolve).catch(reject);
267
- }
268
- else {
269
- reject(new Error("Redirected without a location header"));
270
- }
271
- }
272
- else {
273
- reject(new Error(`Failed to retrieve data from ${url}. Status code: ${response.statusCode}`));
274
- }
275
- });
276
- request.on("error", (error) => {
277
- console.error(`GET Request error for ${url}:`, error);
278
- reject(error);
279
- });
280
- request.end();
281
- });
282
- }
283
- // Helper method to perform HEAD requests
284
- async head(url, maxRedirects = 5) {
285
- return new Promise((resolve, reject) => {
286
- try {
287
- // Parse the input URL
288
- const urlObj = new url_1.URL(url);
289
- const requestOptions = {
290
- hostname: urlObj.hostname,
291
- port: urlObj.port || (urlObj.protocol === "https:" ? 443 : undefined),
292
- path: urlObj.pathname + urlObj.search,
293
- method: "HEAD",
294
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
295
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
296
- 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,
297
104
  };
298
- const request = https_1.default.request(requestOptions, (response) => {
299
- const { statusCode, headers } = response;
300
- // If status code is 2xx, return success
301
- if (statusCode && statusCode >= 200 && statusCode < 300) {
302
- resolve({ success: true, headers });
303
- }
304
- // Handle 3xx redirection
305
- else if (statusCode && statusCode >= 300 && statusCode < 400 && headers.location) {
306
- if (maxRedirects > 0) {
307
- let redirectUrl = headers.location;
308
- // Check if the redirect URL is relative
309
- if (!/^https?:\/\//i.test(redirectUrl)) {
310
- // Resolve the relative URL based on the original URL
311
- redirectUrl = new url_1.URL(redirectUrl, url).toString();
312
- console.log(`Resolved relative redirect to: ${redirectUrl}`);
313
- }
314
- else {
315
- console.log(`Redirecting to: ${redirectUrl}`);
316
- }
317
- // Recursively follow the redirection
318
- this.head(redirectUrl, maxRedirects - 1)
319
- .then(resolve)
320
- .catch(reject);
321
- }
322
- else {
323
- reject({ success: false, message: "Too many redirects" });
324
- }
325
- }
326
- else {
327
- // For other status codes, consider it a failure
328
- resolve({ success: false });
329
- }
330
- });
331
- request.on("error", (error) => {
332
- console.error(`HEAD ${url}:`, error.message);
333
- reject({ success: false });
334
- });
335
- request.end();
336
- }
337
- catch (err) {
338
- console.error(`Invalid URL: ${url}`, err.message);
339
- reject({ success: false, message: "Invalid URL" });
340
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,
341
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
+ }
342
183
  }
343
- // Helper method to fetch JSON data from a URL
344
- async fetchJson(url) {
345
- const response = await this.fetch(url);
346
- 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}.`));
347
215
  }
348
- // Helper method to fetch content with retries and redirection handling
349
- /*private async fetchWithRetries(url: string): Promise<string> {
350
- let attempt = 0;
351
- const maxRetries = 5;
352
- const initialDelay = 2000; // 2 seconds
353
- const maxDelay = 10000; // 10 seconds
354
- const delayMultiplier = 1.5;
355
- let delay = initialDelay;
356
-
357
- 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}`;
358
228
  try {
359
- return await this.fetch(url);
360
- } catch (error: any) {
361
- if (attempt < maxRetries - 1) {
362
- console.warn(
363
- `Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${
364
- delay / 1000
365
- } seconds...`
366
- );
367
- await new Promise((resolve) => setTimeout(resolve, delay));
368
- delay = Math.min(maxDelay, delay * delayMultiplier);
369
- } else {
370
- console.error(`Failed to retrieve data from ${url}. Aborting.`);
371
- throw new Error(`Failed to retrieve data: ${error.message}`);
372
- }
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);
373
251
  }
374
- attempt++;
375
- }
376
- throw new Error(
377
- `Failed to retrieve data from ${url} after ${maxRetries} attempts.`
378
- );
379
- }*/
380
- // Helper method to create a stream with retry logic
381
- async createReadStreamWithRetries(url) {
382
- let attempt = 0;
383
- const maxRetries = 1;
384
- const initialDelay = PropagationServer.initialDelay;
385
- const maxDelay = PropagationServer.maxDelay;
386
- const delayMultiplier = PropagationServer.delayMultiplier;
387
- let delay = initialDelay;
388
- while (attempt < maxRetries) {
389
- try {
390
- return await this.createReadStream(url); // Stream data chunk by chunk
391
- }
392
- catch (error) {
393
- if (attempt < maxRetries - 1) {
394
- console.warn(`Attempt ${attempt + 1} failed: ${error.message}. Retrying in ${delay / 1000} seconds...`);
395
- await new Promise((resolve) => setTimeout(resolve, delay));
396
- delay = Math.min(maxDelay, delay * delayMultiplier);
397
- }
398
- else {
399
- console.error(`Failed to create stream from ${url}. Aborting.`);
400
- throw new Error(`Failed to create stream: ${error.message}`);
401
- }
402
- }
403
- attempt++;
252
+ catch (error) {
253
+ console.error(chalk_1.default.red(`✖ Error fetching file ${dataPath}:`), error);
254
+ throw error;
404
255
  }
405
- throw new Error(`Failed to create stream from ${url} after ${maxRetries} attempts.`);
406
256
  }
407
- // Helper method to fetch a readable stream from a URL
408
- async createReadStream(url) {
409
- return new Promise((resolve, reject) => {
410
- const urlObj = new url_1.URL(url);
411
- const options = {
412
- hostname: urlObj.hostname,
413
- port: urlObj.port || PropagationServer.port,
414
- path: urlObj.pathname + urlObj.search,
415
- method: "GET",
416
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
417
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
418
- rejectUnauthorized: false,
419
- };
420
- const request = https_1.default.request(options, (response) => {
421
- if (response.statusCode === 200) {
422
- resolve(response); // Return the response stream chunk by chunk
423
- }
424
- else if (response.statusCode === 301 || response.statusCode === 302) {
425
- const redirectUrl = response.headers.location;
426
- if (redirectUrl) {
427
- this.createReadStream(redirectUrl).then(resolve).catch(reject);
428
- }
429
- else {
430
- reject(new Error("Redirected without a location header"));
431
- }
432
- }
433
- else {
434
- reject(new Error(`Failed to retrieve stream from ${url}. Status code: ${response.statusCode}`));
435
- }
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,
436
280
  });
437
- request.on("error", (error) => {
438
- console.error(`GET ${url}:`, error.message);
439
- 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
+ });
440
288
  });
441
- request.end();
442
- });
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}.`));
443
333
  }
444
334
  }
445
335
  exports.PropagationServer = PropagationServer;
446
- PropagationServer.port = 4159;
447
- PropagationServer.maxRetries = 5;
448
- PropagationServer.initialDelay = 2000; // 2 seconds
449
- PropagationServer.maxDelay = 10000; // 10 seconds
450
- 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);