nodio-cli 1.0.8 → 1.0.9
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/package.json +1 -1
- package/src/user/commands.js +39 -20
- package/src/user/index.js +2 -0
package/package.json
CHANGED
package/src/user/commands.js
CHANGED
|
@@ -120,6 +120,7 @@ async function uploadFile(options) {
|
|
|
120
120
|
const serverUrl = options.server;
|
|
121
121
|
const shardSizeMb = Number(options.shardSizeMb || 1);
|
|
122
122
|
const replicas = Number(options.replicas || 5);
|
|
123
|
+
const directTimeoutMs = Number(options.directTimeoutMs || 4000);
|
|
123
124
|
|
|
124
125
|
if (!Number.isFinite(shardSizeMb) || shardSizeMb <= 0) {
|
|
125
126
|
throw new Error('shard-size-mb must be greater than 0');
|
|
@@ -127,6 +128,9 @@ async function uploadFile(options) {
|
|
|
127
128
|
if (!Number.isInteger(replicas) || replicas < 5) {
|
|
128
129
|
throw new Error('replicas must be an integer >= 5');
|
|
129
130
|
}
|
|
131
|
+
if (!Number.isFinite(directTimeoutMs) || directTimeoutMs <= 0) {
|
|
132
|
+
throw new Error('direct-timeout-ms must be greater than 0');
|
|
133
|
+
}
|
|
130
134
|
|
|
131
135
|
const shardSizeBytes = Math.floor(shardSizeMb * 1024 * 1024);
|
|
132
136
|
const plainBuffer = await fs.readFile(filePath);
|
|
@@ -173,21 +177,29 @@ async function uploadFile(options) {
|
|
|
173
177
|
throw new Error(`placement failed for ${shardId}: expected ${replicas} replicas`);
|
|
174
178
|
}
|
|
175
179
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (const replica of plannedReplicas) {
|
|
180
|
-
try {
|
|
180
|
+
const directWriteResults = await Promise.allSettled(
|
|
181
|
+
plannedReplicas.map(async (replica) => {
|
|
181
182
|
const putUrl = `${normalizeUrl(replica.url)}/shards/${shardId}`;
|
|
182
183
|
await axios.put(putUrl, encrypted.cipherText, {
|
|
183
184
|
headers: { 'Content-Type': 'application/octet-stream' },
|
|
184
|
-
timeout:
|
|
185
|
+
timeout: directTimeoutMs
|
|
185
186
|
});
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
return replica;
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const successfulReplicas = directWriteResults
|
|
192
|
+
.filter((result) => result.status === 'fulfilled')
|
|
193
|
+
.map((result) => result.value);
|
|
194
|
+
|
|
195
|
+
const failedReplicas = directWriteResults
|
|
196
|
+
.map((result, index) => ({ result, replica: plannedReplicas[index] }))
|
|
197
|
+
.filter((entry) => entry.result.status === 'rejected')
|
|
198
|
+
.map((entry) => ({
|
|
199
|
+
nodeId: entry.replica.nodeId,
|
|
200
|
+
url: entry.replica.url,
|
|
201
|
+
error: entry.result.reason?.message || 'direct write failed'
|
|
202
|
+
}));
|
|
191
203
|
|
|
192
204
|
if (failedReplicas.length > 0) {
|
|
193
205
|
try {
|
|
@@ -260,6 +272,11 @@ async function downloadFile(options) {
|
|
|
260
272
|
const serverUrl = options.server;
|
|
261
273
|
const output = options.output ? path.resolve(options.output) : path.resolve(`./${fileId}.downloaded`);
|
|
262
274
|
const keyBuffer = parseAesKey(options.keyBase64);
|
|
275
|
+
const directTimeoutMs = Number(options.directTimeoutMs || 4000);
|
|
276
|
+
|
|
277
|
+
if (!Number.isFinite(directTimeoutMs) || directTimeoutMs <= 0) {
|
|
278
|
+
throw new Error('direct-timeout-ms must be greater than 0');
|
|
279
|
+
}
|
|
263
280
|
|
|
264
281
|
const api = createApiClient(serverUrl);
|
|
265
282
|
const manifestResponse = await api.get(`/files/${fileId}/manifest`);
|
|
@@ -283,22 +300,24 @@ async function downloadFile(options) {
|
|
|
283
300
|
}
|
|
284
301
|
|
|
285
302
|
let encryptedBuffer = null;
|
|
286
|
-
|
|
287
|
-
|
|
303
|
+
const directFetchResults = await Promise.allSettled(
|
|
304
|
+
(shard.replicas || []).map(async (replica) => {
|
|
288
305
|
const getUrl = `${normalizeUrl(replica.url)}/shards/${shard.shardId}`;
|
|
289
306
|
const response = await axios.get(getUrl, {
|
|
290
307
|
responseType: 'arraybuffer',
|
|
291
|
-
timeout:
|
|
308
|
+
timeout: directTimeoutMs
|
|
292
309
|
});
|
|
293
310
|
const candidate = Buffer.from(response.data);
|
|
294
311
|
if (sha256Hex(candidate) !== shard.checksum) {
|
|
295
|
-
|
|
312
|
+
throw new Error('checksum mismatch');
|
|
296
313
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
return candidate;
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const successfulDirectFetch = directFetchResults.find((result) => result.status === 'fulfilled');
|
|
319
|
+
if (successfulDirectFetch) {
|
|
320
|
+
encryptedBuffer = successfulDirectFetch.value;
|
|
302
321
|
}
|
|
303
322
|
|
|
304
323
|
if (!encryptedBuffer && Array.isArray(shard.replicas) && shard.replicas.length > 0) {
|
package/src/user/index.js
CHANGED
|
@@ -14,6 +14,7 @@ program
|
|
|
14
14
|
.option('--file-id <id>', 'custom file ID (optional)')
|
|
15
15
|
.option('--shard-size-mb <mb>', 'plaintext shard size in MB', '1')
|
|
16
16
|
.option('--replicas <count>', 'replicas per shard (minimum 5)', '5')
|
|
17
|
+
.option('--direct-timeout-ms <ms>', 'timeout for each direct donor attempt before relay fallback', '4000')
|
|
17
18
|
.option('--key-base64 <key>', '32-byte AES key in base64 (optional)')
|
|
18
19
|
.action(async (options) => {
|
|
19
20
|
await uploadFile(options);
|
|
@@ -26,6 +27,7 @@ program
|
|
|
26
27
|
.requiredOption('--key-base64 <key>', '32-byte AES key in base64 from upload output')
|
|
27
28
|
.option('--server <url>', 'central server URL', 'https://api.nodio.me')
|
|
28
29
|
.option('--output <path>', 'output file path')
|
|
30
|
+
.option('--direct-timeout-ms <ms>', 'timeout for each direct donor attempt before relay fallback', '4000')
|
|
29
31
|
.action(async (options) => {
|
|
30
32
|
await downloadFile(options);
|
|
31
33
|
});
|