@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.
- package/dist/DataIntegrityTree/DataIntegrityTree.d.ts +5 -2
- package/dist/DataIntegrityTree/DataIntegrityTree.d.ts.map +1 -1
- package/dist/DataIntegrityTree/DataIntegrityTree.js +94 -34
- package/dist/DigNetwork/ContentServer.d.ts +9 -4
- package/dist/DigNetwork/ContentServer.d.ts.map +1 -1
- package/dist/DigNetwork/ContentServer.js +55 -19
- package/dist/DigNetwork/DigNetwork.d.ts +3 -8
- package/dist/DigNetwork/DigNetwork.d.ts.map +1 -1
- package/dist/DigNetwork/DigNetwork.js +115 -171
- package/dist/DigNetwork/DigPeer.d.ts +9 -3
- package/dist/DigNetwork/DigPeer.d.ts.map +1 -1
- package/dist/DigNetwork/DigPeer.js +68 -114
- package/dist/DigNetwork/PropagationServer.d.ts +91 -33
- package/dist/DigNetwork/PropagationServer.d.ts.map +1 -1
- package/dist/DigNetwork/PropagationServer.js +412 -372
- package/dist/blockchain/DataStore.d.ts +13 -6
- package/dist/blockchain/DataStore.d.ts.map +1 -1
- package/dist/blockchain/DataStore.js +64 -91
- package/dist/blockchain/DataStoreSerializer.d.ts +1 -1
- package/dist/blockchain/DataStoreSerializer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.d.ts +9 -1
- package/dist/blockchain/FullNodePeer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.js +38 -10
- package/dist/blockchain/ServerCoin.d.ts +11 -3
- package/dist/blockchain/ServerCoin.d.ts.map +1 -1
- package/dist/blockchain/ServerCoin.js +45 -17
- package/dist/blockchain/Wallet.d.ts +2 -2
- package/dist/blockchain/Wallet.d.ts.map +1 -1
- package/dist/blockchain/Wallet.js +2 -2
- package/dist/blockchain/coins.d.ts +1 -1
- package/dist/blockchain/coins.d.ts.map +1 -1
- package/dist/blockchain/coins.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ContentScanner.d.ts +63 -0
- package/dist/utils/ContentScanner.d.ts.map +1 -0
- package/dist/utils/ContentScanner.js +175 -0
- package/dist/utils/FileTransfer.d.ts +47 -0
- package/dist/utils/FileTransfer.d.ts.map +1 -0
- package/dist/utils/FileTransfer.js +209 -0
- package/dist/utils/StoreArchiveManager.d.ts +45 -0
- package/dist/utils/StoreArchiveManager.d.ts.map +1 -0
- package/dist/utils/StoreArchiveManager.js +153 -0
- package/dist/utils/config.d.ts +0 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +2 -3
- package/dist/utils/directoryUtils.d.ts +0 -6
- package/dist/utils/directoryUtils.d.ts.map +1 -1
- package/dist/utils/directoryUtils.js +30 -11
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +0 -1
- package/package.json +12 -3
- package/dist/utils/deltaUtils.d.ts +0 -2
- package/dist/utils/deltaUtils.d.ts.map +0 -1
- 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
|
|
9
|
-
const
|
|
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
|
|
13
|
-
const
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async pushFile(filePath, relativePath) {
|
|
26
|
-
const { nonce, username, password } = await this.getUploadDetails();
|
|
27
|
-
const wallet = await blockchain_1.Wallet.load("default");
|
|
28
|
-
const keyOwnershipSig = await wallet.createKeyOwnershipSignature(nonce);
|
|
29
|
-
const publicKey = await wallet.getPublicSyntheticKey();
|
|
30
|
-
const uploadUrl = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${relativePath}`;
|
|
31
|
-
await this.retryOperation(() => this.uploadFileDirect(filePath, uploadUrl, username, password, keyOwnershipSig, publicKey.toString("hex"), nonce), `Upload failed for ${relativePath}`);
|
|
32
|
-
}
|
|
33
|
-
// Method to subscribe to a store on the propagation server
|
|
34
|
-
async subscribeToStore() {
|
|
35
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/subscribe`;
|
|
36
|
-
const data = JSON.stringify({ storeId: this.storeId });
|
|
37
|
-
await this.postRequest(url, data);
|
|
38
|
-
}
|
|
39
|
-
// Method to unsubscribe from a store on the propagation server
|
|
40
|
-
async unsubscribeFromStore() {
|
|
41
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/unsubscribe`;
|
|
42
|
-
const data = JSON.stringify({ storeId: this.storeId });
|
|
43
|
-
await this.postRequest(url, data);
|
|
44
|
-
}
|
|
45
|
-
// Method to check the status of a store on the propagation server
|
|
46
|
-
async getStoreStatus() {
|
|
47
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/status/${this.storeId}`;
|
|
48
|
-
return this.fetchJson(url);
|
|
49
|
-
}
|
|
50
|
-
async streamStoreData(dataPath) {
|
|
51
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${dataPath}`;
|
|
52
|
-
return this.createReadStreamWithRetries(url);
|
|
53
|
-
}
|
|
54
|
-
// Method to retrieve a store's key
|
|
55
|
-
async getStoreData(dataPath) {
|
|
56
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/${this.storeId}/${dataPath}`;
|
|
57
|
-
return this.fetch(url);
|
|
58
|
-
}
|
|
59
|
-
async getStatus() {
|
|
60
|
-
const url = `https://${this.ipAddress}:${PropagationServer.port}/status/${this.storeId}`;
|
|
61
|
-
const statusJson = await this.fetch(url);
|
|
62
|
-
return JSON.parse(statusJson);
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
});
|
|
305
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
PropagationServer.
|
|
415
|
-
|
|
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);
|