@dignetwork/dig-sdk 0.0.1-alpha.7 → 0.0.1-alpha.71

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 (56) hide show
  1. package/dist/DataIntegrityTree/DataIntegrityTree.d.ts +5 -2
  2. package/dist/DataIntegrityTree/DataIntegrityTree.d.ts.map +1 -1
  3. package/dist/DataIntegrityTree/DataIntegrityTree.js +94 -34
  4. package/dist/DigNetwork/ContentServer.d.ts +9 -4
  5. package/dist/DigNetwork/ContentServer.d.ts.map +1 -1
  6. package/dist/DigNetwork/ContentServer.js +55 -19
  7. package/dist/DigNetwork/DigNetwork.d.ts +3 -8
  8. package/dist/DigNetwork/DigNetwork.d.ts.map +1 -1
  9. package/dist/DigNetwork/DigNetwork.js +115 -171
  10. package/dist/DigNetwork/DigPeer.d.ts +9 -3
  11. package/dist/DigNetwork/DigPeer.d.ts.map +1 -1
  12. package/dist/DigNetwork/DigPeer.js +68 -114
  13. package/dist/DigNetwork/PropagationServer.d.ts +91 -33
  14. package/dist/DigNetwork/PropagationServer.d.ts.map +1 -1
  15. package/dist/DigNetwork/PropagationServer.js +412 -372
  16. package/dist/blockchain/DataStore.d.ts +13 -6
  17. package/dist/blockchain/DataStore.d.ts.map +1 -1
  18. package/dist/blockchain/DataStore.js +64 -91
  19. package/dist/blockchain/DataStoreSerializer.d.ts +1 -1
  20. package/dist/blockchain/DataStoreSerializer.d.ts.map +1 -1
  21. package/dist/blockchain/FullNodePeer.d.ts +9 -1
  22. package/dist/blockchain/FullNodePeer.d.ts.map +1 -1
  23. package/dist/blockchain/FullNodePeer.js +38 -10
  24. package/dist/blockchain/ServerCoin.d.ts +11 -3
  25. package/dist/blockchain/ServerCoin.d.ts.map +1 -1
  26. package/dist/blockchain/ServerCoin.js +45 -17
  27. package/dist/blockchain/Wallet.d.ts +2 -2
  28. package/dist/blockchain/Wallet.d.ts.map +1 -1
  29. package/dist/blockchain/Wallet.js +2 -2
  30. package/dist/blockchain/coins.d.ts +1 -1
  31. package/dist/blockchain/coins.d.ts.map +1 -1
  32. package/dist/blockchain/coins.js +1 -1
  33. package/dist/types.d.ts +1 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/utils/ContentScanner.d.ts +63 -0
  36. package/dist/utils/ContentScanner.d.ts.map +1 -0
  37. package/dist/utils/ContentScanner.js +175 -0
  38. package/dist/utils/FileTransfer.d.ts +47 -0
  39. package/dist/utils/FileTransfer.d.ts.map +1 -0
  40. package/dist/utils/FileTransfer.js +209 -0
  41. package/dist/utils/StoreArchiveManager.d.ts +45 -0
  42. package/dist/utils/StoreArchiveManager.d.ts.map +1 -0
  43. package/dist/utils/StoreArchiveManager.js +153 -0
  44. package/dist/utils/config.d.ts +0 -1
  45. package/dist/utils/config.d.ts.map +1 -1
  46. package/dist/utils/config.js +2 -3
  47. package/dist/utils/directoryUtils.d.ts +0 -6
  48. package/dist/utils/directoryUtils.d.ts.map +1 -1
  49. package/dist/utils/directoryUtils.js +30 -11
  50. package/dist/utils/index.d.ts +0 -1
  51. package/dist/utils/index.d.ts.map +1 -1
  52. package/dist/utils/index.js +0 -1
  53. package/package.json +12 -3
  54. package/dist/utils/deltaUtils.d.ts +0 -2
  55. package/dist/utils/deltaUtils.d.ts.map +0 -1
  56. package/dist/utils/deltaUtils.js +0 -83
@@ -4,412 +4,452 @@ 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 spinnerUtils_1 = require("../utils/spinnerUtils");
13
- const blockchain_1 = require("../blockchain");
14
+ const https_1 = __importDefault(require("https"));
15
+ const cli_progress_1 = __importDefault(require("cli-progress"));
16
+ const colorette_1 = require("colorette"); // For colored output
17
+ const config_1 = require("../utils/config");
18
+ const hashUtils_1 = require("../utils/hashUtils");
19
+ const nanospinner_1 = require("nanospinner");
20
+ // Helper function to trim long filenames with ellipsis and ensure consistent padding
21
+ function formatFilename(filename, maxLength = 30) {
22
+ if (filename.length > maxLength) {
23
+ // Trim the filename and add ellipsis
24
+ return `...${filename.slice(-(maxLength - 3))}`.padEnd(maxLength, " ");
25
+ }
26
+ // For shorter filenames, just pad to the maxLength
27
+ return filename.padEnd(maxLength, " ");
28
+ }
14
29
  class PropagationServer {
15
30
  constructor(ipAddress, storeId) {
16
- this.ipAddress = ipAddress;
17
31
  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);
32
+ this.sessionId = ""; // Session ID will be set after starting the upload session
33
+ this.publicKey = ""; // Public key will be set after initializing the wallet
34
+ this.ipAddress = ipAddress;
35
+ // Get or create SSL certificates
36
+ const { certPath, keyPath } = (0, ssl_1.getOrCreateSSLCerts)();
37
+ this.certPath = certPath;
38
+ this.keyPath = keyPath;
63
39
  }
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);
40
+ /**
41
+ * Initialize the Wallet instance.
42
+ */
43
+ async initializeWallet() {
44
+ this.wallet = await blockchain_1.Wallet.load("default");
45
+ this.publicKey = await this.wallet.getPublicSyntheticKey().toString("hex");
68
46
  }
69
- // In PropagationServer.ts:
70
- async isStoreSynced() {
71
- const status = await this.getStatus();
72
- return status.synced === true;
47
+ /**
48
+ * Create an Axios HTTPS Agent with self-signed certificate allowance.
49
+ */
50
+ createHttpsAgent() {
51
+ return new https_1.default.Agent({
52
+ cert: fs_1.default.readFileSync(this.certPath),
53
+ key: fs_1.default.readFileSync(this.keyPath),
54
+ rejectUnauthorized: false, // Allow self-signed certificates
55
+ });
73
56
  }
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;
57
+ /**
58
+ * Check if the store and optional root hash exist by making a HEAD request.
59
+ *
60
+ * @param {string} [rootHash] - Optional root hash to check for existence.
61
+ * @returns {Promise<{ storeExists: boolean, rootHashExists: boolean }>} - An object indicating if the store and root hash exist.
62
+ */
63
+ async checkStoreExists(rootHash) {
64
+ const spinner = (0, nanospinner_1.createSpinner)(`Checking if store ${this.storeId} exists...`).start();
65
+ try {
66
+ const config = {
67
+ httpsAgent: this.createHttpsAgent(),
68
+ };
69
+ let url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}`;
70
+ if (rootHash) {
71
+ url += `?hasRootHash=${rootHash}`;
72
+ }
73
+ const response = await axios_1.default.head(url, config);
74
+ // Extract store existence and root hash existence from headers
75
+ const storeExists = response.headers["x-store-exists"] === "true";
76
+ const rootHashExists = response.headers["x-has-root-hash"] === "true";
77
+ if (storeExists) {
78
+ spinner.success({
79
+ text: (0, colorette_1.green)(`Store ${this.storeId} exists on peer!`),
80
+ });
81
+ }
82
+ else {
83
+ spinner.error({
84
+ text: (0, colorette_1.red)(`Store ${this.storeId} does not exist. Credentials will be required to push.`),
85
+ });
86
+ }
87
+ return { storeExists, rootHashExists };
82
88
  }
83
- const uploadDetails = await this.fetchUploadDetails(username, password);
84
- if (!uploadDetails) {
85
- throw new Error("Failed to retrieve upload details.");
89
+ catch (error) {
90
+ spinner.error({ text: (0, colorette_1.red)("Error checking if store exists:") });
91
+ console.error((0, colorette_1.red)(error.message));
92
+ throw error;
86
93
  }
87
- return { ...uploadDetails, username, password };
88
94
  }
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",
95
+ /**
96
+ * Start an upload session by sending a POST request with the rootHash.dat file.
97
+ *
98
+ * @param {string} rootHash - The root hash used to name the .dat file.
99
+ * @param {string} datFilePath - The full path to the rootHash.dat file.
100
+ */
101
+ async startUploadSession(rootHash) {
102
+ const spinner = (0, nanospinner_1.createSpinner)(`Starting upload session for store ${this.storeId}...`).start();
103
+ try {
104
+ const formData = new form_data_1.default();
105
+ const datFilePath = path_1.default.join(config_1.STORE_PATH, this.storeId, `${rootHash}.dat`);
106
+ // Ensure the rootHash.dat file exists
107
+ if (!fs_1.default.existsSync(datFilePath)) {
108
+ throw new Error(`File not found: ${datFilePath}`);
109
+ }
110
+ formData.append("file", fs_1.default.createReadStream(datFilePath), {
111
+ filename: `${rootHash}.dat`,
112
+ });
113
+ const config = {
114
+ httpsAgent: this.createHttpsAgent(),
98
115
  headers: {
99
- "Content-Type": "application/json",
100
- "Content-Length": Buffer.byteLength(data),
116
+ ...formData.getHeaders(),
101
117
  },
102
- key: fs_1.default.readFileSync(PropagationServer.keyPath),
103
- cert: fs_1.default.readFileSync(PropagationServer.certPath),
104
- rejectUnauthorized: false,
105
118
  };
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}`));
112
- }
113
- });
114
- req.on("error", (err) => {
115
- reject(err);
116
- });
117
- req.write(data);
118
- req.end();
119
- });
120
- }
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
119
+ // Add Basic Auth if username and password are present
120
+ if (this.username && this.password) {
121
+ config.auth = {
122
+ username: this.username,
123
+ password: this.password,
139
124
  };
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
- req.on("error", (err) => {
164
- console.error(err.message);
165
- resolve(false); // Resolve to false if there is an error
166
- });
167
- req.end();
125
+ }
126
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}?roothash=${rootHash}`;
127
+ const response = await axios_1.default.post(url, formData, config);
128
+ this.sessionId = response.data.sessionId;
129
+ spinner.success({
130
+ text: (0, colorette_1.green)(`Upload session started for DataStore ${this.storeId} with session ID ${this.sessionId}`),
168
131
  });
169
- }, "Performing remote preflight", "Preflight succeeded.", "Error on preflight.");
132
+ }
133
+ catch (error) {
134
+ spinner.error({ text: (0, colorette_1.red)("Error starting upload session:") });
135
+ console.error((0, colorette_1.red)(error.message));
136
+ throw error;
137
+ }
170
138
  }
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,
139
+ /**
140
+ * Request a nonce for a file by sending a HEAD request to the server.
141
+ * Checks if the file already exists based on the 'x-file-exists' header.
142
+ */
143
+ async getFileNonce(filename) {
144
+ try {
145
+ const config = {
146
+ httpsAgent: this.createHttpsAgent(),
193
147
  };
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();
221
- }
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
- }
232
- }
233
- attempt++;
148
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${filename}`;
149
+ const response = await axios_1.default.head(url, config);
150
+ // Check for 'x-file-exists' header
151
+ const fileExists = response.headers["x-file-exists"] === "true";
152
+ // If file exists, no need to generate a nonce
153
+ const nonce = response.headers["x-nonce"];
154
+ return { nonce, fileExists };
155
+ }
156
+ catch (error) {
157
+ console.error((0, colorette_1.red)(`Error generating nonce for file ${filename}:`), error.message);
158
+ throw error;
234
159
  }
235
- throw new Error(errorMessage);
236
160
  }
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"));
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(`Request error for ${url}:`, error);
275
- reject(error);
276
- });
277
- request.end();
161
+ /**
162
+ * Upload a file to the server by sending a PUT request.
163
+ * Logs progress using cli-progress for each file.
164
+ */
165
+ async uploadFile(label, dataPath) {
166
+ const filePath = path_1.default.join(config_1.STORE_PATH, this.storeId, dataPath);
167
+ const { nonce, fileExists } = await this.getFileNonce(dataPath);
168
+ if (fileExists) {
169
+ console.log((0, colorette_1.blue)(`File ${label} already exists. Skipping upload.`));
170
+ return;
171
+ }
172
+ const wallet = await blockchain_1.Wallet.load("default");
173
+ const keyOwnershipSig = await wallet.createKeyOwnershipSignature(nonce);
174
+ const publicKey = await wallet.getPublicSyntheticKey();
175
+ const formData = new form_data_1.default();
176
+ formData.append("file", fs_1.default.createReadStream(filePath));
177
+ const fileSize = fs_1.default.statSync(filePath).size;
178
+ // Create a new progress bar for each file
179
+ const progressBar = PropagationServer.multiBar.create(fileSize, 0, {
180
+ filename: (0, colorette_1.yellow)(path_1.default.basename(label)),
181
+ percentage: 0,
278
182
  });
183
+ let uploadedBytes = 0;
184
+ const config = {
185
+ headers: {
186
+ "Content-Type": "multipart/form-data",
187
+ "x-nonce": nonce,
188
+ "x-public-key": publicKey.toString("hex"),
189
+ "x-key-ownership-sig": keyOwnershipSig,
190
+ ...formData.getHeaders(),
191
+ },
192
+ httpsAgent: this.createHttpsAgent(),
193
+ // Tracking upload progress and updating the progress bar
194
+ onUploadProgress: (progressEvent) => {
195
+ const uploadedBytes = progressEvent.loaded;
196
+ const percentage = Math.round((100 * progressEvent.loaded) / progressEvent.total);
197
+ progressBar.update(uploadedBytes, { percentage });
198
+ },
199
+ };
200
+ try {
201
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/upload/${this.storeId}/${this.sessionId}/${dataPath}`;
202
+ const response = await axios_1.default.put(url, formData, config);
203
+ // Complete the progress bar
204
+ progressBar.update(fileSize, { percentage: 100 });
205
+ progressBar.stop();
206
+ return response.data;
207
+ }
208
+ catch (error) {
209
+ console.error((0, colorette_1.red)(`✖ Error uploading file ${label}:`), error.message);
210
+ progressBar.stop(); // Stop the progress bar in case of error
211
+ throw error;
212
+ }
279
213
  }
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,
214
+ /**
215
+ * Commit the upload session by sending a POST request to the server.
216
+ * This finalizes the upload and moves files from the temporary session directory to the permanent location.
217
+ */
218
+ async commitUploadSession() {
219
+ const spinner = (0, nanospinner_1.createSpinner)(`Committing upload session for store ${this.storeId}...`).start();
220
+ try {
221
+ const config = {
222
+ httpsAgent: this.createHttpsAgent(),
223
+ auth: this.username && this.password
224
+ ? {
225
+ username: this.username,
226
+ password: this.password,
227
+ }
228
+ : undefined,
292
229
  };
293
- const request = https_1.default.request(options, (response) => {
294
- if (response.statusCode === 200) {
295
- resolve(true);
296
- }
297
- else {
298
- resolve(false);
299
- }
230
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/commit/${this.storeId}/${this.sessionId}`;
231
+ const response = await axios_1.default.post(url, {}, config);
232
+ spinner.success({
233
+ text: (0, colorette_1.green)(`Upload session ${this.sessionId} successfully committed.`),
300
234
  });
301
- request.on("error", (error) => {
302
- console.error(`Request error for ${url}:`, error);
303
- reject(false);
304
- });
305
- request.end();
306
- });
235
+ return response.data;
236
+ }
237
+ catch (error) {
238
+ spinner.error({ text: (0, colorette_1.red)("Error committing upload session:") });
239
+ console.error((0, colorette_1.red)(error.message));
240
+ throw error;
241
+ }
307
242
  }
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);
243
+ /**
244
+ * Static function to handle the entire upload process for multiple files based on rootHash.
245
+ * @param {string} storeId - The ID of the DataStore.
246
+ * @param {string} rootHash - The root hash used to derive the file set.
247
+ * @param {string} publicKey - The public key of the user.
248
+ * @param {string} ipAddress - The IP address of the server.
249
+ */
250
+ static async uploadStore(storeId, rootHash, ipAddress) {
251
+ const propagationServer = new PropagationServer(ipAddress, storeId);
252
+ // Initialize wallet
253
+ await propagationServer.initializeWallet();
254
+ // Check if the store exists
255
+ const { storeExists, rootHashExists } = await propagationServer.checkStoreExists(rootHash);
256
+ // If the store does not exist, prompt for credentials
257
+ if (!storeExists) {
258
+ console.log((0, colorette_1.red)(`Store ${storeId} does not exist. Prompting for credentials...`));
259
+ const credentials = await (0, credentialsUtils_1.promptCredentials)(propagationServer.ipAddress);
260
+ propagationServer.username = credentials.username;
261
+ propagationServer.password = credentials.password;
262
+ }
263
+ if (rootHashExists) {
264
+ console.log((0, colorette_1.blue)(`Root hash ${rootHash} already exists in the store. Skipping upload.`));
265
+ return;
266
+ }
267
+ // Start the upload session
268
+ await propagationServer.startUploadSession(rootHash);
269
+ const dataStore = blockchain_1.DataStore.from(storeId);
270
+ const files = await dataStore.getFileSetForRootHash(rootHash);
271
+ // Upload each file
272
+ for (const file of files) {
273
+ await propagationServer.uploadFile(file.name, file.path);
274
+ }
275
+ // Commit the session after all files have been uploaded
276
+ await propagationServer.commitUploadSession();
277
+ console.log((0, colorette_1.green)(`✔ All files have been uploaded to DataStore ${storeId}.`));
312
278
  }
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) {
279
+ /**
280
+ * Fetch a file from the server by sending a GET request and return its content in memory.
281
+ * Logs progress using cli-progress.
282
+ * @param {string} dataPath - The data path of the file to download.
283
+ * @returns {Promise<Buffer>} - The file content in memory as a Buffer.
284
+ */
285
+ async fetchFile(dataPath) {
286
+ const config = {
287
+ responseType: "arraybuffer", // To store the file content in memory
288
+ httpsAgent: this.createHttpsAgent(),
289
+ onDownloadProgress: (progressEvent) => {
290
+ const totalLength = progressEvent.total || 0;
291
+ const downloadedBytes = progressEvent.loaded;
292
+ // Update progress bar
293
+ const progressBar = PropagationServer.multiBar.create(totalLength, 0, {
294
+ dataPath: (0, colorette_1.yellow)(dataPath),
295
+ percentage: 0,
296
+ });
297
+ progressBar.update(downloadedBytes, {
298
+ percentage: Math.round((downloadedBytes / totalLength) * 100),
299
+ });
300
+ if (downloadedBytes === totalLength) {
301
+ progressBar.update(totalLength, { percentage: 100 });
302
+ progressBar.stop();
303
+ console.log((0, colorette_1.green)(`✔ File ${dataPath} fetched successfully.`));
304
+ }
305
+ },
306
+ };
307
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/fetch/${this.storeId}/${dataPath}`;
323
308
  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
- }
309
+ const response = await axios_1.default.get(url, config);
310
+ // Return the file contents as a Buffer
311
+ return Buffer.from(response.data);
338
312
  }
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
356
- }
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}`);
366
- }
367
- }
368
- attempt++;
313
+ catch (error) {
314
+ console.error((0, colorette_1.red)(`✖ Error fetching file ${dataPath}:`), error);
315
+ throw error;
369
316
  }
370
- throw new Error(`Failed to create stream from ${url} after ${maxRetries} attempts.`);
371
317
  }
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,
318
+ /**
319
+ * Get details of a file, including whether it exists and its size.
320
+ * Makes a HEAD request to the server and checks the response headers.
321
+ *
322
+ * @param {string} dataPath - The path of the file within the DataStore.
323
+ * @param {string} rootHash - The root hash associated with the DataStore.
324
+ * @returns {Promise<{ exists: boolean; size: number }>} - An object containing file existence and size information.
325
+ */
326
+ async getFileDetails(dataPath, rootHash) {
327
+ try {
328
+ const config = {
329
+ httpsAgent: this.createHttpsAgent(),
384
330
  };
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"));
396
- }
397
- }
398
- else {
399
- reject(new Error(`Failed to retrieve stream from ${url}. Status code: ${response.statusCode}`));
400
- }
331
+ // Construct the URL for the HEAD request to check file details
332
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/store/${this.storeId}/${rootHash}/${dataPath}`;
333
+ const response = await axios_1.default.head(url, config);
334
+ // Check the headers for file existence and size
335
+ const fileExists = response.headers["x-file-exists"] === "true";
336
+ const fileSize = parseInt(response.headers["x-file-size"], 10);
337
+ return {
338
+ exists: fileExists,
339
+ size: fileExists ? fileSize : 0, // Return 0 size if file doesn't exist
340
+ };
341
+ }
342
+ catch (error) {
343
+ console.error((0, colorette_1.red)(`✖ Error checking file details for ${dataPath}:`), error.message);
344
+ throw error;
345
+ }
346
+ }
347
+ /**
348
+ * Download a file from the server by sending a GET request.
349
+ * Logs progress using cli-progress.
350
+ * @param {string} dataPath - The data path of the file to download.
351
+ */
352
+ async downloadFile(dataPath) {
353
+ const config = {
354
+ responseType: "stream", // Make sure the response is streamed
355
+ httpsAgent: this.createHttpsAgent(),
356
+ };
357
+ const url = `https://${this.ipAddress}:${PropagationServer.port}/fetch/${this.storeId}/${dataPath}`;
358
+ const downloadPath = path_1.default.join(config_1.STORE_PATH, this.storeId, dataPath); // Save to the correct store directory
359
+ // Ensure that the directory for the file exists
360
+ fs_1.default.mkdirSync(path_1.default.dirname(downloadPath), { recursive: true });
361
+ const fileWriteStream = fs_1.default.createWriteStream(downloadPath);
362
+ try {
363
+ const response = await axios_1.default.get(url, config);
364
+ const totalLength = parseInt(response.headers["content-length"], 10);
365
+ console.log((0, colorette_1.cyan)(`Starting download for ${dataPath}...`));
366
+ // Create a progress bar for the download
367
+ const progressBar = PropagationServer.multiBar.create(totalLength, 0, {
368
+ dataPath: (0, colorette_1.yellow)(dataPath),
369
+ percentage: 0,
401
370
  });
402
- request.on("error", (error) => {
403
- console.error(`Request error for ${url}:`, error);
404
- reject(error);
371
+ let downloadedBytes = 0;
372
+ // Pipe the response data to the file stream and track progress
373
+ response.data.on("data", (chunk) => {
374
+ downloadedBytes += chunk.length;
375
+ progressBar.update(downloadedBytes, {
376
+ percentage: Math.round((downloadedBytes / totalLength) * 100),
377
+ });
405
378
  });
406
- request.end();
407
- });
379
+ // Pipe the data into the file and finalize
380
+ response.data.pipe(fileWriteStream);
381
+ return new Promise((resolve, reject) => {
382
+ fileWriteStream.on("finish", () => {
383
+ progressBar.update(totalLength, { percentage: 100 });
384
+ progressBar.stop();
385
+ console.log((0, colorette_1.green)(`
386
+ File ${dataPath} downloaded successfully to ${downloadPath}.`));
387
+ resolve();
388
+ });
389
+ fileWriteStream.on("error", (error) => {
390
+ progressBar.stop();
391
+ console.error((0, colorette_1.red)(`✖ Error downloading file ${dataPath}:`), error);
392
+ reject(error);
393
+ });
394
+ });
395
+ }
396
+ catch (error) {
397
+ console.error((0, colorette_1.red)(`✖ Error downloading file ${dataPath}:`), error);
398
+ throw error;
399
+ }
400
+ }
401
+ /**
402
+ * Static function to handle downloading multiple files from a DataStore based on file paths.
403
+ * @param {string} storeId - The ID of the DataStore.
404
+ * @param {string[]} dataPaths - The list of data paths to download.
405
+ * @param {string} ipAddress - The IP address of the server.
406
+ */
407
+ static async downloadStore(storeId, rootHash, ipAddress) {
408
+ const propagationServer = new PropagationServer(ipAddress, storeId);
409
+ // Initialize wallet
410
+ await propagationServer.initializeWallet();
411
+ // Check if the store exists
412
+ const { storeExists, rootHashExists } = await propagationServer.checkStoreExists(rootHash);
413
+ if (!storeExists || !rootHashExists) {
414
+ throw new Error(`Store ${storeId} does not exist.`);
415
+ }
416
+ await propagationServer.downloadFile("height.json");
417
+ const datFileContent = await propagationServer.fetchFile(`${rootHash}.dat`);
418
+ const root = JSON.parse(datFileContent.toString());
419
+ for (const [fileKey, fileData] of Object.entries(root.files)) {
420
+ const dataPath = (0, hashUtils_1.getFilePathFromSha256)(root.files[fileKey].sha256, "data");
421
+ await propagationServer.downloadFile(dataPath);
422
+ }
423
+ const dataStore = blockchain_1.DataStore.from(storeId);
424
+ await dataStore.generateManifestFile();
425
+ console.log((0, colorette_1.green)(`✔ All files have been downloaded to ${storeId}.`));
408
426
  }
409
427
  }
410
428
  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;
429
+ PropagationServer.port = 4159; // Static port used for all requests
430
+ // Adjust cliProgress settings to align output properly and handle potential undefined values
431
+ // MultiBar for handling multiple progress bars with green progress and margin between bars
432
+ PropagationServer.multiBar = new cli_progress_1.default.MultiBar({
433
+ clearOnComplete: false,
434
+ hideCursor: true,
435
+ format: (options, params, payload) => {
436
+ // Bar length and padding settings
437
+ const barCompleteChar = (0, colorette_1.green)(options.barCompleteChar || "="); // Green bar for completed portion
438
+ const barIncompleteChar = options.barIncompleteChar || "-"; // Default incomplete character
439
+ const barSize = options.barsize || 40; // Default size of the bar
440
+ // Calculate the bar progress
441
+ const progressBar = `${barCompleteChar.repeat(Math.round(params.progress * barSize))}${barIncompleteChar.repeat(barSize - Math.round(params.progress * barSize))}`;
442
+ // Calculate the percentage manually
443
+ const percentage = Math.round((params.value / params.total) * 100);
444
+ // Format the filename to a fixed length
445
+ const formattedFilename = formatFilename(payload.filename, 30); // Trim to max 30 chars
446
+ // Padding the filename, percentage, and size
447
+ const percentageStr = `${percentage}%`.padEnd(4); // Ensure percentage is always 7 characters wide
448
+ const size = `${params.value}/${params.total} bytes`.padEnd(20); // Ensure size is always 20 characters wide
449
+ // Return the complete formatted progress bar with padding
450
+ return `${progressBar} | ${formattedFilename.padEnd(35)} | ${percentageStr} | ${size}`;
451
+ },
452
+ stopOnComplete: true,
453
+ barsize: 40,
454
+ align: "left",
455
+ }, cli_progress_1.default.Presets.shades_classic);