bytifi 0.2.5 → 0.2.7
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/LICENSE +21 -0
- package/README.md +19 -5
- package/bin/bytifi.js +168 -66
- package/lib/api.js +203 -0
- package/lib/base64url.js +10 -2
- package/lib/crypto.js +0 -1
- package/lib/decrypt.js +184 -136
- package/lib/progress.js +50 -0
- package/lib/upload.js +94 -254
- package/lib/url.js +26 -0
- package/package.json +5 -2
package/lib/decrypt.js
CHANGED
|
@@ -9,15 +9,24 @@ import {
|
|
|
9
9
|
usesChunkCompression,
|
|
10
10
|
} from './crypto.js'
|
|
11
11
|
import { fromBase64Url } from './base64url.js'
|
|
12
|
-
import {
|
|
12
|
+
import { fetchPublicBinaryWithRetry, publicFetchWithRetry } from './api.js'
|
|
13
|
+
import { normalizeBaseUrl } from './url.js'
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
+
const DEFAULT_DECRYPT_CONCURRENCY = 2
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
-
return
|
|
17
|
+
function decryptProgress(overrides) {
|
|
18
|
+
return { stage: 'decrypting', percent: 0, ...overrides }
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
function resolveDecryptConcurrency(requested) {
|
|
22
|
+
const value = Number(requested)
|
|
23
|
+
if (Number.isFinite(value) && value > 0) {
|
|
24
|
+
return Math.min(8, Math.max(1, Math.floor(value)))
|
|
25
|
+
}
|
|
26
|
+
return DEFAULT_DECRYPT_CONCURRENCY
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseDecryptInput(input, { encryptionToken = '', baseUrl = undefined } = {}) {
|
|
21
30
|
const trimmed = String(input || '').trim()
|
|
22
31
|
if (!trimmed) {
|
|
23
32
|
throw new Error('Missing share URL or link token.')
|
|
@@ -67,7 +76,7 @@ export function parseDecryptInput(input, { encryptionToken = '', baseUrl = DEFAU
|
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
export function parseShareReference(input, { encryptionToken = '', baseUrl =
|
|
79
|
+
export function parseShareReference(input, { encryptionToken = '', baseUrl = undefined } = {}) {
|
|
71
80
|
const trimmed = String(input || '').trim()
|
|
72
81
|
if (!trimmed) {
|
|
73
82
|
return {
|
|
@@ -127,16 +136,16 @@ async function resolveEncryptionMeta({
|
|
|
127
136
|
metaPath = '',
|
|
128
137
|
inlineMeta = null,
|
|
129
138
|
linkToken = '',
|
|
130
|
-
baseUrl
|
|
139
|
+
baseUrl,
|
|
131
140
|
signal,
|
|
132
141
|
onStatus,
|
|
133
142
|
}) {
|
|
134
143
|
if (inlineMeta) {
|
|
135
|
-
return { meta: inlineMeta }
|
|
144
|
+
return { meta: inlineMeta, skipApiMetaFetch: true }
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
if (metaPath) {
|
|
139
|
-
return { meta: await readMetaFile(metaPath) }
|
|
148
|
+
return { meta: await readMetaFile(metaPath), skipApiMetaFetch: true }
|
|
140
149
|
}
|
|
141
150
|
|
|
142
151
|
if (!linkToken) {
|
|
@@ -169,6 +178,10 @@ function sanitizeOutputName(filename) {
|
|
|
169
178
|
return base || 'download'
|
|
170
179
|
}
|
|
171
180
|
|
|
181
|
+
async function cleanupPartialOutput(outputPath) {
|
|
182
|
+
await fs.unlink(outputPath).catch(() => {})
|
|
183
|
+
}
|
|
184
|
+
|
|
172
185
|
async function fetchLinkInfo(baseUrl, linkToken, signal, onStatus) {
|
|
173
186
|
return publicFetchWithRetry(baseUrl, `/api/link/${encodeURIComponent(linkToken)}`, { signal, onStatus })
|
|
174
187
|
}
|
|
@@ -181,6 +194,14 @@ async function fetchEncryptedPart(baseUrl, linkToken, partNumber, signal, onStat
|
|
|
181
194
|
)
|
|
182
195
|
}
|
|
183
196
|
|
|
197
|
+
function splitDownloadUrl(encryptedFileUrl, fallbackBaseUrl) {
|
|
198
|
+
const url = new URL(encryptedFileUrl, `${normalizeBaseUrl(fallbackBaseUrl)}/`)
|
|
199
|
+
return {
|
|
200
|
+
baseUrl: `${url.protocol}//${url.host}`,
|
|
201
|
+
path: `${url.pathname}${url.search}`,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
184
205
|
async function decryptFromParts({
|
|
185
206
|
baseUrl,
|
|
186
207
|
linkToken,
|
|
@@ -191,38 +212,48 @@ async function decryptFromParts({
|
|
|
191
212
|
onProgress,
|
|
192
213
|
onStatus,
|
|
193
214
|
signal,
|
|
215
|
+
concurrency = DEFAULT_DECRYPT_CONCURRENCY,
|
|
194
216
|
}) {
|
|
195
217
|
const { chunks } = buildEncryptedChunkPlan(meta)
|
|
196
218
|
const fileHandle = await fs.open(outputPath, 'w')
|
|
197
219
|
let decryptedBytes = 0
|
|
220
|
+
const workerCount = Math.min(resolveDecryptConcurrency(concurrency), chunks.length)
|
|
221
|
+
let nextIndex = 0
|
|
222
|
+
let completedParts = 0
|
|
223
|
+
let inFlightParts = 0
|
|
198
224
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
225
|
+
function partPercent() {
|
|
226
|
+
const weighted = completedParts + inFlightParts * 0.5
|
|
227
|
+
return Math.min(100, Math.round((weighted / chunks.length) * 100))
|
|
228
|
+
}
|
|
204
229
|
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
async function processPart(index) {
|
|
231
|
+
if (signal?.aborted) {
|
|
232
|
+
throw new Error('Decrypt aborted.')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const chunk = chunks[index]
|
|
236
|
+
const partNumber = chunk.chunkIndex + 1
|
|
237
|
+
inFlightParts += 1
|
|
207
238
|
|
|
208
|
-
|
|
239
|
+
try {
|
|
240
|
+
onProgress?.(decryptProgress({
|
|
209
241
|
stage: 'downloading',
|
|
210
242
|
part: partNumber,
|
|
211
243
|
totalParts: chunks.length,
|
|
212
|
-
downloadedBytes: decryptedBytes,
|
|
213
244
|
totalBytes: meta.originalSize,
|
|
214
|
-
percent:
|
|
215
|
-
})
|
|
245
|
+
percent: partPercent(),
|
|
246
|
+
}))
|
|
216
247
|
|
|
217
248
|
const encryptedPart = await fetchEncryptedPart(baseUrl, linkToken, partNumber, signal, onStatus)
|
|
218
249
|
|
|
219
|
-
onProgress?.({
|
|
250
|
+
onProgress?.(decryptProgress({
|
|
220
251
|
stage: 'decrypting',
|
|
221
252
|
part: partNumber,
|
|
222
253
|
totalParts: chunks.length,
|
|
223
254
|
partBytes: encryptedPart.length,
|
|
224
|
-
percent:
|
|
225
|
-
})
|
|
255
|
+
percent: partPercent(),
|
|
256
|
+
}))
|
|
226
257
|
|
|
227
258
|
const plainPart = await decryptPlainChunkFromEncrypted(
|
|
228
259
|
encryptedPart,
|
|
@@ -232,19 +263,45 @@ async function decryptFromParts({
|
|
|
232
263
|
meta,
|
|
233
264
|
)
|
|
234
265
|
|
|
235
|
-
|
|
236
|
-
|
|
266
|
+
return { index, partNumber, plainPart }
|
|
267
|
+
} finally {
|
|
268
|
+
inFlightParts -= 1
|
|
269
|
+
}
|
|
270
|
+
}
|
|
237
271
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
272
|
+
try {
|
|
273
|
+
const results = new Array(chunks.length)
|
|
274
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
275
|
+
while (true) {
|
|
276
|
+
const index = nextIndex
|
|
277
|
+
nextIndex += 1
|
|
278
|
+
if (index >= chunks.length) return
|
|
279
|
+
|
|
280
|
+
const result = await processPart(index)
|
|
281
|
+
results[result.index] = result.plainPart
|
|
282
|
+
completedParts += 1
|
|
283
|
+
decryptedBytes += result.plainPart.length
|
|
284
|
+
|
|
285
|
+
onProgress?.(decryptProgress({
|
|
286
|
+
stage: 'writing',
|
|
287
|
+
part: result.partNumber,
|
|
288
|
+
totalParts: chunks.length,
|
|
289
|
+
downloadedBytes: decryptedBytes,
|
|
290
|
+
totalBytes: meta.originalSize,
|
|
291
|
+
percent: Math.round((decryptedBytes / meta.originalSize) * 100),
|
|
292
|
+
detail: `${completedParts}/${chunks.length} parts`,
|
|
293
|
+
}))
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await Promise.all(workers)
|
|
298
|
+
|
|
299
|
+
for (const plainPart of results) {
|
|
300
|
+
await fileHandle.write(plainPart)
|
|
247
301
|
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
await cleanupPartialOutput(outputPath)
|
|
304
|
+
throw error
|
|
248
305
|
} finally {
|
|
249
306
|
await fileHandle.close()
|
|
250
307
|
}
|
|
@@ -259,17 +316,11 @@ async function decryptFromSingleFile({
|
|
|
259
316
|
noncePrefix,
|
|
260
317
|
outputPath,
|
|
261
318
|
onProgress,
|
|
319
|
+
onStatus,
|
|
262
320
|
signal,
|
|
321
|
+
baseUrl,
|
|
263
322
|
}) {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
if (!response.ok) {
|
|
267
|
-
const text = await response.text()
|
|
268
|
-
throw new BytifiApiError(text || 'Failed to download encrypted file.', {
|
|
269
|
-
status: response.status,
|
|
270
|
-
body: text,
|
|
271
|
-
})
|
|
272
|
-
}
|
|
323
|
+
const { baseUrl: downloadBaseUrl, path: downloadPath } = splitDownloadUrl(encryptedFileUrl, baseUrl)
|
|
273
324
|
|
|
274
325
|
if (usesChunkCompression(meta)) {
|
|
275
326
|
if (meta.chunkCount !== 1) {
|
|
@@ -278,7 +329,7 @@ async function decryptFromSingleFile({
|
|
|
278
329
|
)
|
|
279
330
|
}
|
|
280
331
|
|
|
281
|
-
const encryptedBuffer =
|
|
332
|
+
const encryptedBuffer = await fetchPublicBinaryWithRetry(downloadBaseUrl, downloadPath, { signal, onStatus })
|
|
282
333
|
const plainPart = await decryptPlainChunkFromEncrypted(
|
|
283
334
|
encryptedBuffer,
|
|
284
335
|
tokenBytes,
|
|
@@ -286,93 +337,27 @@ async function decryptFromSingleFile({
|
|
|
286
337
|
0,
|
|
287
338
|
meta,
|
|
288
339
|
)
|
|
289
|
-
await fs.writeFile(outputPath, plainPart)
|
|
290
|
-
onProgress?.(100)
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
340
|
|
|
294
|
-
|
|
341
|
+
try {
|
|
342
|
+
await fs.writeFile(outputPath, plainPart)
|
|
343
|
+
} catch (error) {
|
|
344
|
+
await cleanupPartialOutput(outputPath)
|
|
345
|
+
throw error
|
|
346
|
+
}
|
|
295
347
|
|
|
296
|
-
|
|
297
|
-
const encryptedBuffer = Buffer.from(await response.arrayBuffer())
|
|
298
|
-
await writeDecryptedBuffer(encryptedBuffer, chunks, meta, tokenBytes, noncePrefix, outputPath, onProgress, signal)
|
|
348
|
+
onProgress?.({ stage: 'complete', percent: 100 })
|
|
299
349
|
return
|
|
300
350
|
}
|
|
301
351
|
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
const pending = []
|
|
305
|
-
let pendingLength = 0
|
|
306
|
-
let downloadedBytes = 0
|
|
307
|
-
let nextChunkIndex = 0
|
|
308
|
-
let decryptedBytes = 0
|
|
309
|
-
|
|
310
|
-
const takeBytes = (length) => {
|
|
311
|
-
while (pendingLength < length) {
|
|
312
|
-
return null
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const out = Buffer.alloc(length)
|
|
316
|
-
let offset = 0
|
|
317
|
-
|
|
318
|
-
while (offset < length) {
|
|
319
|
-
const head = pending[0]
|
|
320
|
-
const take = Math.min(length - offset, head.length)
|
|
321
|
-
head.copy(out, offset, 0, take)
|
|
322
|
-
offset += take
|
|
323
|
-
|
|
324
|
-
if (take === head.length) {
|
|
325
|
-
pending.shift()
|
|
326
|
-
} else {
|
|
327
|
-
pending[0] = head.subarray(take)
|
|
328
|
-
}
|
|
329
|
-
pendingLength -= take
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return out
|
|
333
|
-
}
|
|
352
|
+
const { chunks, totalEncryptedSize } = buildEncryptedChunkPlan(meta)
|
|
353
|
+
const encryptedBuffer = await fetchPublicBinaryWithRetry(downloadBaseUrl, downloadPath, { signal, onStatus })
|
|
334
354
|
|
|
335
355
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const chunk = chunks[nextChunkIndex]
|
|
342
|
-
|
|
343
|
-
while (pendingLength < chunk.encryptedSize) {
|
|
344
|
-
const { done, value } = await reader.read()
|
|
345
|
-
if (done) break
|
|
346
|
-
const buffer = Buffer.from(value)
|
|
347
|
-
pending.push(buffer)
|
|
348
|
-
pendingLength += buffer.length
|
|
349
|
-
downloadedBytes += buffer.length
|
|
350
|
-
onProgress?.(Math.min(99, Math.round((downloadedBytes / totalEncryptedSize) * 100)))
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const encryptedChunk = takeBytes(chunk.encryptedSize)
|
|
354
|
-
if (!encryptedChunk) {
|
|
355
|
-
throw new Error('Encrypted file ended before all parts were downloaded.')
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const plainPart = await decryptPlainChunkFromEncrypted(
|
|
359
|
-
encryptedChunk,
|
|
360
|
-
tokenBytes,
|
|
361
|
-
noncePrefix,
|
|
362
|
-
chunk.chunkIndex,
|
|
363
|
-
meta,
|
|
364
|
-
)
|
|
365
|
-
await fileHandle.write(plainPart)
|
|
366
|
-
decryptedBytes += plainPart.length
|
|
367
|
-
nextChunkIndex += 1
|
|
368
|
-
onProgress?.(Math.min(99, Math.round((decryptedBytes / meta.originalSize) * 100)))
|
|
369
|
-
}
|
|
370
|
-
} finally {
|
|
371
|
-
await reader.cancel().catch(() => {})
|
|
372
|
-
await fileHandle.close()
|
|
356
|
+
await writeDecryptedBuffer(encryptedBuffer, chunks, meta, tokenBytes, noncePrefix, outputPath, onProgress, signal)
|
|
357
|
+
} catch (error) {
|
|
358
|
+
await cleanupPartialOutput(outputPath)
|
|
359
|
+
throw error
|
|
373
360
|
}
|
|
374
|
-
|
|
375
|
-
onProgress?.(100)
|
|
376
361
|
}
|
|
377
362
|
|
|
378
363
|
async function writeDecryptedBuffer(encryptedBuffer, chunks, meta, tokenBytes, noncePrefix, outputPath, onProgress, signal) {
|
|
@@ -389,7 +374,7 @@ async function writeDecryptedBuffer(encryptedBuffer, chunks, meta, tokenBytes, n
|
|
|
389
374
|
meta,
|
|
390
375
|
)
|
|
391
376
|
await fs.writeFile(outputPath, plainPart)
|
|
392
|
-
onProgress?.(100)
|
|
377
|
+
onProgress?.({ stage: 'complete', percent: 100 })
|
|
393
378
|
return
|
|
394
379
|
}
|
|
395
380
|
|
|
@@ -418,13 +403,21 @@ async function writeDecryptedBuffer(encryptedBuffer, chunks, meta, tokenBytes, n
|
|
|
418
403
|
await fileHandle.write(plainPart)
|
|
419
404
|
offset += chunk.encryptedSize
|
|
420
405
|
decryptedBytes += plainPart.length
|
|
421
|
-
onProgress?.(
|
|
406
|
+
onProgress?.(decryptProgress({
|
|
407
|
+
stage: 'decrypting',
|
|
408
|
+
part: chunk.chunkIndex + 1,
|
|
409
|
+
totalParts: chunks.length,
|
|
410
|
+
percent: Math.round((decryptedBytes / meta.originalSize) * 100),
|
|
411
|
+
}))
|
|
422
412
|
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
await cleanupPartialOutput(outputPath)
|
|
415
|
+
throw error
|
|
423
416
|
} finally {
|
|
424
417
|
await fileHandle.close()
|
|
425
418
|
}
|
|
426
419
|
|
|
427
|
-
onProgress?.(100)
|
|
420
|
+
onProgress?.({ stage: 'complete', percent: 100 })
|
|
428
421
|
}
|
|
429
422
|
|
|
430
423
|
async function decryptFromLocalSingleFile({
|
|
@@ -444,7 +437,12 @@ async function decryptFromLocalSingleFile({
|
|
|
444
437
|
}
|
|
445
438
|
|
|
446
439
|
const encryptedBuffer = await fs.readFile(encryptedFilePath)
|
|
447
|
-
|
|
440
|
+
try {
|
|
441
|
+
await writeDecryptedBuffer(encryptedBuffer, chunks, meta, tokenBytes, noncePrefix, outputPath, onProgress, signal)
|
|
442
|
+
} catch (error) {
|
|
443
|
+
await cleanupPartialOutput(outputPath)
|
|
444
|
+
throw error
|
|
445
|
+
}
|
|
448
446
|
return
|
|
449
447
|
}
|
|
450
448
|
|
|
@@ -452,7 +450,12 @@ async function decryptFromLocalSingleFile({
|
|
|
452
450
|
|
|
453
451
|
if (stat.size <= 64 * 1024 * 1024) {
|
|
454
452
|
const encryptedBuffer = await fs.readFile(encryptedFilePath)
|
|
455
|
-
|
|
453
|
+
try {
|
|
454
|
+
await writeDecryptedBuffer(encryptedBuffer, chunks, meta, tokenBytes, noncePrefix, outputPath, onProgress, signal)
|
|
455
|
+
} catch (error) {
|
|
456
|
+
await cleanupPartialOutput(outputPath)
|
|
457
|
+
throw error
|
|
458
|
+
}
|
|
456
459
|
return
|
|
457
460
|
}
|
|
458
461
|
|
|
@@ -484,14 +487,22 @@ async function decryptFromLocalSingleFile({
|
|
|
484
487
|
await outputHandle.write(plainPart)
|
|
485
488
|
offset += chunk.encryptedSize
|
|
486
489
|
decryptedBytes += plainPart.length
|
|
487
|
-
onProgress?.(
|
|
490
|
+
onProgress?.(decryptProgress({
|
|
491
|
+
stage: 'decrypting',
|
|
492
|
+
part: chunk.chunkIndex + 1,
|
|
493
|
+
totalParts: chunks.length,
|
|
494
|
+
percent: Math.round((decryptedBytes / meta.originalSize) * 100),
|
|
495
|
+
}))
|
|
488
496
|
}
|
|
497
|
+
} catch (error) {
|
|
498
|
+
await cleanupPartialOutput(outputPath)
|
|
499
|
+
throw error
|
|
489
500
|
} finally {
|
|
490
501
|
await fileHandle.close()
|
|
491
502
|
await outputHandle.close()
|
|
492
503
|
}
|
|
493
504
|
|
|
494
|
-
onProgress?.(100)
|
|
505
|
+
onProgress?.({ stage: 'complete', percent: 100 })
|
|
495
506
|
}
|
|
496
507
|
|
|
497
508
|
async function listLocalPartFiles(dirPath) {
|
|
@@ -554,13 +565,21 @@ async function decryptFromLocalParts({
|
|
|
554
565
|
|
|
555
566
|
await fileHandle.write(plainPart)
|
|
556
567
|
decryptedBytes += plainPart.length
|
|
557
|
-
onProgress?.(
|
|
568
|
+
onProgress?.(decryptProgress({
|
|
569
|
+
stage: 'decrypting',
|
|
570
|
+
part: partNumber,
|
|
571
|
+
totalParts: chunks.length,
|
|
572
|
+
percent: Math.round((decryptedBytes / meta.originalSize) * 100),
|
|
573
|
+
}))
|
|
558
574
|
}
|
|
575
|
+
} catch (error) {
|
|
576
|
+
await cleanupPartialOutput(outputPath)
|
|
577
|
+
throw error
|
|
559
578
|
} finally {
|
|
560
579
|
await fileHandle.close()
|
|
561
580
|
}
|
|
562
581
|
|
|
563
|
-
onProgress?.(100)
|
|
582
|
+
onProgress?.({ stage: 'complete', percent: 100 })
|
|
564
583
|
}
|
|
565
584
|
|
|
566
585
|
function hintTokenConfusion(encryptionToken, linkToken) {
|
|
@@ -662,6 +681,20 @@ function buildDecryptResult({
|
|
|
662
681
|
}
|
|
663
682
|
}
|
|
664
683
|
|
|
684
|
+
async function ensureOutputPath(outputPath, force) {
|
|
685
|
+
try {
|
|
686
|
+
await fs.access(outputPath)
|
|
687
|
+
if (!force) {
|
|
688
|
+
throw new Error(`Output file already exists: ${outputPath} (use --force to overwrite)`)
|
|
689
|
+
}
|
|
690
|
+
await fs.unlink(outputPath)
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (error.code !== 'ENOENT') {
|
|
693
|
+
throw error
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
665
698
|
async function decryptLocalFile(inputPath, options = {}) {
|
|
666
699
|
const resolvedOptions = await applyUploadJsonDefaults(options)
|
|
667
700
|
const absolutePath = path.resolve(inputPath)
|
|
@@ -681,15 +714,17 @@ async function decryptLocalFile(inputPath, options = {}) {
|
|
|
681
714
|
hintTokenConfusion(resolvedOptions.encryptionToken, '')
|
|
682
715
|
}
|
|
683
716
|
|
|
717
|
+
const hasCompleteLocalMeta = Boolean(resolvedOptions.inlineMeta || resolvedOptions.metaPath)
|
|
718
|
+
|
|
684
719
|
const resolved = await resolveEncryptionMeta({
|
|
685
720
|
metaPath: resolvedOptions.metaPath,
|
|
686
721
|
inlineMeta: resolvedOptions.inlineMeta,
|
|
687
|
-
linkToken,
|
|
722
|
+
linkToken: hasCompleteLocalMeta ? '' : linkToken,
|
|
688
723
|
baseUrl: resolvedOptions.baseUrl || shareReference.baseUrl,
|
|
689
724
|
signal: resolvedOptions.signal,
|
|
690
725
|
onStatus: resolvedOptions.onStatus,
|
|
691
726
|
})
|
|
692
|
-
const meta = resolved.meta
|
|
727
|
+
const meta = resolved.meta
|
|
693
728
|
const linkInfo = resolved.linkInfo || null
|
|
694
729
|
const encryptionToken = resolveEncryptionToken(
|
|
695
730
|
resolvedOptions.encryptionToken,
|
|
@@ -705,6 +740,7 @@ async function decryptLocalFile(inputPath, options = {}) {
|
|
|
705
740
|
)
|
|
706
741
|
|
|
707
742
|
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
|
743
|
+
await ensureOutputPath(outputPath, resolvedOptions.force)
|
|
708
744
|
|
|
709
745
|
if (stat.isDirectory()) {
|
|
710
746
|
await decryptFromLocalParts({
|
|
@@ -762,7 +798,10 @@ export async function decryptFile(input, options = {}) {
|
|
|
762
798
|
throw new Error('Missing share URL, link token, or encrypted file path.')
|
|
763
799
|
}
|
|
764
800
|
|
|
765
|
-
|
|
801
|
+
const treatAsLocal = resolvedOptions.localFile
|
|
802
|
+
|| (!looksLikeRemoteInput(trimmedInput) && await pathExists(trimmedInput))
|
|
803
|
+
|
|
804
|
+
if (treatAsLocal) {
|
|
766
805
|
return decryptLocalFile(trimmedInput, {
|
|
767
806
|
...resolvedOptions,
|
|
768
807
|
onProgress: reportStatus,
|
|
@@ -785,7 +824,13 @@ export async function decryptFile(input, options = {}) {
|
|
|
785
824
|
throw new Error('This link is not an encrypted file.')
|
|
786
825
|
}
|
|
787
826
|
|
|
788
|
-
|
|
827
|
+
let meta = resolvedOptions.inlineMeta
|
|
828
|
+
if (!meta && resolvedOptions.metaPath) {
|
|
829
|
+
meta = await readMetaFile(resolvedOptions.metaPath)
|
|
830
|
+
}
|
|
831
|
+
if (!meta) {
|
|
832
|
+
meta = normalizeClientEncryptionMeta(linkInfo.clientEncryptionMeta)
|
|
833
|
+
}
|
|
789
834
|
if (!meta) {
|
|
790
835
|
throw new Error('Invalid encryption metadata for this file.')
|
|
791
836
|
}
|
|
@@ -800,6 +845,7 @@ export async function decryptFile(input, options = {}) {
|
|
|
800
845
|
)
|
|
801
846
|
|
|
802
847
|
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
|
848
|
+
await ensureOutputPath(outputPath, resolvedOptions.force)
|
|
803
849
|
|
|
804
850
|
if (linkInfo.storageMode === 'parts') {
|
|
805
851
|
await decryptFromParts({
|
|
@@ -812,6 +858,7 @@ export async function decryptFile(input, options = {}) {
|
|
|
812
858
|
onProgress: reportStatus,
|
|
813
859
|
onStatus: reportStatus,
|
|
814
860
|
signal: resolvedOptions.signal,
|
|
861
|
+
concurrency: resolvedOptions.concurrency,
|
|
815
862
|
})
|
|
816
863
|
} else {
|
|
817
864
|
const encryptedFileUrl = linkInfo.downloadUrl
|
|
@@ -826,6 +873,7 @@ export async function decryptFile(input, options = {}) {
|
|
|
826
873
|
onProgress: reportStatus,
|
|
827
874
|
onStatus: reportStatus,
|
|
828
875
|
signal: resolvedOptions.signal,
|
|
876
|
+
baseUrl: parsed.baseUrl,
|
|
829
877
|
})
|
|
830
878
|
}
|
|
831
879
|
|
package/lib/progress.js
CHANGED
|
@@ -24,6 +24,48 @@ export function formatDuration(ms) {
|
|
|
24
24
|
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function createProgressTracker({ totalBytes = null } = {}) {
|
|
28
|
+
let lastBytes = 0
|
|
29
|
+
let lastTime = Date.now()
|
|
30
|
+
let smoothedBytesPerSec = 0
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
update(info = {}) {
|
|
34
|
+
const now = Date.now()
|
|
35
|
+
const bytes = Number(info.downloadedBytes ?? info.bytes ?? 0)
|
|
36
|
+
const elapsedMs = now - lastTime
|
|
37
|
+
|
|
38
|
+
if (elapsedMs >= 250 && bytes >= lastBytes) {
|
|
39
|
+
const instantRate = ((bytes - lastBytes) / elapsedMs) * 1000
|
|
40
|
+
smoothedBytesPerSec = smoothedBytesPerSec === 0
|
|
41
|
+
? instantRate
|
|
42
|
+
: smoothedBytesPerSec * 0.7 + instantRate * 0.3
|
|
43
|
+
lastBytes = bytes
|
|
44
|
+
lastTime = now
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const total = Number(info.totalBytes ?? totalBytes)
|
|
48
|
+
let etaMs = null
|
|
49
|
+
|
|
50
|
+
if (Number.isFinite(total) && total > bytes && smoothedBytesPerSec > 0) {
|
|
51
|
+
etaMs = Math.round(((total - bytes) / smoothedBytesPerSec) * 1000)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...info,
|
|
56
|
+
bytesPerSec: smoothedBytesPerSec > 0 ? smoothedBytesPerSec : undefined,
|
|
57
|
+
etaMs: etaMs ?? undefined,
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
reset() {
|
|
62
|
+
lastBytes = 0
|
|
63
|
+
lastTime = Date.now()
|
|
64
|
+
smoothedBytesPerSec = 0
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
export function formatProgressLine(info) {
|
|
28
70
|
if (typeof info === 'number') {
|
|
29
71
|
return `${info}%`
|
|
@@ -55,6 +97,14 @@ export function formatProgressLine(info) {
|
|
|
55
97
|
segments.push(`${formatBytes(info.downloadedBytes)}/${formatBytes(info.totalBytes)}`)
|
|
56
98
|
}
|
|
57
99
|
|
|
100
|
+
if (Number.isFinite(info.bytesPerSec) && info.bytesPerSec > 0) {
|
|
101
|
+
segments.push(`${formatBytes(info.bytesPerSec)}/s`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Number.isFinite(info.etaMs) && info.etaMs > 0) {
|
|
105
|
+
segments.push(`ETA ${formatDuration(info.etaMs)}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
58
108
|
if (Number.isFinite(info.percent)) {
|
|
59
109
|
segments.push(`${info.percent}%`)
|
|
60
110
|
}
|