@protontech/drive-sdk 0.1.1 → 0.2.0
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/crypto/driveCrypto.d.ts +11 -0
- package/dist/crypto/driveCrypto.js +20 -7
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/interface.d.ts +10 -1
- package/dist/crypto/openPGPCrypto.d.ts +18 -2
- package/dist/crypto/openPGPCrypto.js +25 -6
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/diagnostic/telemetry.d.ts +1 -1
- package/dist/diagnostic/telemetry.js +1 -1
- package/dist/diagnostic/telemetry.js.map +1 -1
- package/dist/interface/download.d.ts +46 -0
- package/dist/interface/index.d.ts +2 -2
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +26 -1
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/telemetry.d.ts +5 -2
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/internal/apiService/apiService.js +1 -1
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +78 -165
- package/dist/internal/apiService/index.d.ts +1 -1
- package/dist/internal/apiService/index.js +2 -2
- package/dist/internal/apiService/index.js.map +1 -1
- package/dist/internal/apiService/transformers.d.ts +1 -1
- package/dist/internal/apiService/transformers.js +2 -2
- package/dist/internal/apiService/transformers.js.map +1 -1
- package/dist/internal/download/blockIndex.d.ts +11 -0
- package/dist/internal/download/blockIndex.js +35 -0
- package/dist/internal/download/blockIndex.js.map +1 -0
- package/dist/internal/download/blockIndex.test.d.ts +1 -0
- package/dist/internal/download/blockIndex.test.js +147 -0
- package/dist/internal/download/blockIndex.test.js.map +1 -0
- package/dist/internal/download/fileDownloader.d.ts +6 -2
- package/dist/internal/download/fileDownloader.js +83 -6
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +69 -4
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/download/interface.d.ts +4 -4
- package/dist/internal/download/seekableStream.d.ts +80 -0
- package/dist/internal/download/seekableStream.js +163 -0
- package/dist/internal/download/seekableStream.js.map +1 -0
- package/dist/internal/download/seekableStream.test.d.ts +1 -0
- package/dist/internal/download/seekableStream.test.js +149 -0
- package/dist/internal/download/seekableStream.test.js.map +1 -0
- package/dist/internal/download/telemetry.js +1 -1
- package/dist/internal/download/telemetry.js.map +1 -1
- package/dist/internal/download/telemetry.test.js +7 -7
- package/dist/internal/download/telemetry.test.js.map +1 -1
- package/dist/internal/errors.d.ts +1 -1
- package/dist/internal/errors.js +7 -1
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/errors.test.js +44 -10
- package/dist/internal/errors.test.js.map +1 -1
- package/dist/internal/events/eventManager.d.ts +1 -0
- package/dist/internal/events/eventManager.js +9 -0
- package/dist/internal/events/eventManager.js.map +1 -1
- package/dist/internal/events/eventManager.test.js +53 -38
- package/dist/internal/events/eventManager.test.js.map +1 -1
- package/dist/internal/events/index.d.ts +4 -3
- package/dist/internal/events/index.js +38 -32
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/nodes/apiService.js +16 -3
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +43 -7
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +9 -2
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +6 -1
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +4 -1
- package/dist/internal/nodes/cryptoService.js +66 -16
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +129 -46
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/events.js +7 -7
- package/dist/internal/nodes/events.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.d.ts +2 -1
- package/dist/internal/nodes/extendedAttributes.js +27 -1
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +59 -6
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +18 -2
- package/dist/internal/nodes/nodesAccess.js +11 -1
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +1 -1
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesRevisions.d.ts +4 -3
- package/dist/internal/nodes/nodesRevisions.js +2 -2
- package/dist/internal/nodes/nodesRevisions.js.map +1 -1
- package/dist/internal/shares/cryptoService.js +7 -4
- package/dist/internal/shares/cryptoService.js.map +1 -1
- package/dist/internal/shares/cryptoService.test.js +5 -3
- package/dist/internal/shares/cryptoService.test.js.map +1 -1
- package/dist/internal/sharing/apiService.js +5 -5
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/cache.d.ts +1 -0
- package/dist/internal/sharing/cache.js +9 -0
- package/dist/internal/sharing/cache.js.map +1 -1
- package/dist/internal/sharing/cryptoService.js +8 -5
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/cryptoService.test.js +7 -4
- package/dist/internal/sharing/cryptoService.test.js.map +1 -1
- package/dist/internal/sharing/events.d.ts +1 -0
- package/dist/internal/sharing/events.js +28 -18
- package/dist/internal/sharing/events.js.map +1 -1
- package/dist/internal/sharing/events.test.js +98 -84
- package/dist/internal/sharing/events.test.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +1 -0
- package/dist/internal/upload/manager.d.ts +1 -1
- package/dist/internal/upload/manager.js +8 -4
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +7 -10
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +1 -1
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +1 -1
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/internal/upload/telemetry.js +2 -2
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/internal/upload/telemetry.test.js +7 -7
- package/dist/internal/upload/telemetry.test.js.map +1 -1
- package/dist/protonDriveClient.js +2 -2
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/telemetry.d.ts +2 -2
- package/dist/telemetry.js +2 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/tests/telemetry.js +1 -1
- package/dist/tests/telemetry.js.map +1 -1
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +3 -1
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +70 -25
- package/src/crypto/interface.ts +15 -0
- package/src/crypto/openPGPCrypto.ts +37 -5
- package/src/diagnostic/telemetry.ts +1 -1
- package/src/interface/download.ts +46 -0
- package/src/interface/index.ts +2 -1
- package/src/interface/nodes.ts +28 -1
- package/src/interface/telemetry.ts +6 -1
- package/src/internal/apiService/apiService.ts +1 -1
- package/src/internal/apiService/driveTypes.ts +78 -165
- package/src/internal/apiService/index.ts +1 -1
- package/src/internal/apiService/transformers.ts +1 -1
- package/src/internal/download/blockIndex.test.ts +158 -0
- package/src/internal/download/blockIndex.ts +36 -0
- package/src/internal/download/fileDownloader.test.ts +100 -7
- package/src/internal/download/fileDownloader.ts +109 -9
- package/src/internal/download/interface.ts +4 -4
- package/src/internal/download/seekableStream.test.ts +187 -0
- package/src/internal/download/seekableStream.ts +182 -0
- package/src/internal/download/telemetry.test.ts +7 -7
- package/src/internal/download/telemetry.ts +1 -1
- package/src/internal/errors.test.ts +45 -11
- package/src/internal/errors.ts +8 -0
- package/src/internal/events/eventManager.test.ts +61 -40
- package/src/internal/events/eventManager.ts +10 -0
- package/src/internal/events/index.ts +53 -35
- package/src/internal/nodes/apiService.test.ts +59 -15
- package/src/internal/nodes/apiService.ts +21 -4
- package/src/internal/nodes/cache.test.ts +6 -1
- package/src/internal/nodes/cache.ts +9 -2
- package/src/internal/nodes/cryptoService.test.ts +139 -47
- package/src/internal/nodes/cryptoService.ts +94 -9
- package/src/internal/nodes/events.ts +6 -7
- package/src/internal/nodes/extendedAttributes.test.ts +60 -7
- package/src/internal/nodes/extendedAttributes.ts +37 -1
- package/src/internal/nodes/index.test.ts +1 -1
- package/src/internal/nodes/interface.ts +19 -2
- package/src/internal/nodes/nodesAccess.ts +15 -1
- package/src/internal/nodes/nodesManagement.ts +1 -1
- package/src/internal/nodes/nodesRevisions.ts +14 -5
- package/src/internal/shares/cryptoService.test.ts +5 -3
- package/src/internal/shares/cryptoService.ts +7 -4
- package/src/internal/sharing/apiService.ts +6 -6
- package/src/internal/sharing/cache.ts +9 -0
- package/src/internal/sharing/cryptoService.test.ts +7 -4
- package/src/internal/sharing/cryptoService.ts +8 -5
- package/src/internal/sharing/events.test.ts +104 -89
- package/src/internal/sharing/events.ts +33 -18
- package/src/internal/upload/interface.ts +1 -0
- package/src/internal/upload/manager.test.ts +7 -10
- package/src/internal/upload/manager.ts +7 -4
- package/src/internal/upload/streamUploader.test.ts +1 -1
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/internal/upload/telemetry.test.ts +7 -7
- package/src/internal/upload/telemetry.ts +2 -2
- package/src/protonDriveClient.ts +2 -2
- package/src/telemetry.ts +2 -2
- package/src/tests/telemetry.ts +1 -1
- package/src/transformers.ts +6 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { APIHTTPError, HTTPErrorCode } from '../apiService';
|
|
2
|
+
import { DecryptedRevision } from '../nodes';
|
|
2
3
|
import { FileDownloader } from './fileDownloader';
|
|
3
4
|
import { DownloadTelemetry } from './telemetry';
|
|
4
5
|
import { DownloadAPIService } from './apiService';
|
|
5
6
|
import { DownloadCryptoService } from './cryptoService';
|
|
6
|
-
import { APIHTTPError, HTTPErrorCode } from '../apiService';
|
|
7
7
|
|
|
8
8
|
function mockBlockDownload(_: string, token: string, onProgress: (downloadedBytes: number) => void) {
|
|
9
9
|
const index = parseInt(token.slice(5, 6));
|
|
@@ -21,7 +21,7 @@ describe('FileDownloader', () => {
|
|
|
21
21
|
let apiService: DownloadAPIService;
|
|
22
22
|
let cryptoService: DownloadCryptoService;
|
|
23
23
|
let nodeKey: { key: object; contentKeyPacketSessionKey: string };
|
|
24
|
-
let revision:
|
|
24
|
+
let revision: DecryptedRevision;
|
|
25
25
|
|
|
26
26
|
beforeEach(() => {
|
|
27
27
|
// @ts-expect-error No need to implement all methods for mocking
|
|
@@ -74,7 +74,8 @@ describe('FileDownloader', () => {
|
|
|
74
74
|
revision = {
|
|
75
75
|
uid: 'revisionUid',
|
|
76
76
|
claimedSize: 1024,
|
|
77
|
-
|
|
77
|
+
claimedBlockSizes: [16, 16, 16, 16],
|
|
78
|
+
} as DecryptedRevision;
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
describe('writeToStream', () => {
|
|
@@ -262,7 +263,7 @@ describe('FileDownloader', () => {
|
|
|
262
263
|
await verifyOnProgress([1, 2, 3]);
|
|
263
264
|
});
|
|
264
265
|
|
|
265
|
-
it('should handle failure when
|
|
266
|
+
it('should handle failure when verifying block', async () => {
|
|
266
267
|
cryptoService.verifyBlockIntegrity = jest.fn().mockImplementation(async function () {
|
|
267
268
|
throw new Error('Failed to verify block');
|
|
268
269
|
});
|
|
@@ -270,7 +271,7 @@ describe('FileDownloader', () => {
|
|
|
270
271
|
await verifyFailure('Failed to verify block', undefined);
|
|
271
272
|
});
|
|
272
273
|
|
|
273
|
-
it('should handle one time-off failure when
|
|
274
|
+
it('should handle one time-off failure when verifying block', async () => {
|
|
274
275
|
let count = 0;
|
|
275
276
|
cryptoService.verifyBlockIntegrity = jest.fn().mockImplementation(async function () {
|
|
276
277
|
if (count === 0) {
|
|
@@ -335,7 +336,7 @@ describe('FileDownloader', () => {
|
|
|
335
336
|
await verifyOnProgress([1, 2, 3]);
|
|
336
337
|
});
|
|
337
338
|
|
|
338
|
-
it('should handle failure when
|
|
339
|
+
it('should handle failure when verifying manifest', async () => {
|
|
339
340
|
cryptoService.verifyManifest = jest.fn().mockImplementation(async function () {
|
|
340
341
|
throw new Error('Failed to verify manifest');
|
|
341
342
|
});
|
|
@@ -394,4 +395,96 @@ describe('FileDownloader', () => {
|
|
|
394
395
|
expect(onFinish).toHaveBeenCalledTimes(1);
|
|
395
396
|
});
|
|
396
397
|
});
|
|
398
|
+
|
|
399
|
+
describe('getSeekableStream', () => {
|
|
400
|
+
let onFinish: () => void;
|
|
401
|
+
let downloader: FileDownloader;
|
|
402
|
+
|
|
403
|
+
beforeEach(() => {
|
|
404
|
+
apiService.downloadBlock = jest.fn().mockImplementation(async function (_, token) {
|
|
405
|
+
const index = parseInt(token.slice(5, 6)) - 1;
|
|
406
|
+
const data = new Uint8Array(16);
|
|
407
|
+
for (let i = 0; i < data.length; i++) {
|
|
408
|
+
data[i] = index * 16 + i;
|
|
409
|
+
}
|
|
410
|
+
return data;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
onFinish = jest.fn();
|
|
414
|
+
|
|
415
|
+
downloader = new FileDownloader(
|
|
416
|
+
telemetry,
|
|
417
|
+
apiService,
|
|
418
|
+
cryptoService,
|
|
419
|
+
nodeKey as any,
|
|
420
|
+
revision,
|
|
421
|
+
undefined,
|
|
422
|
+
onFinish,
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should read the stream', async () => {
|
|
427
|
+
const stream = downloader.getSeekableStream();
|
|
428
|
+
|
|
429
|
+
const data = await stream.read(32);
|
|
430
|
+
expect(data.value).toEqual(
|
|
431
|
+
new Uint8Array([
|
|
432
|
+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
|
433
|
+
27, 28, 29, 30, 31,
|
|
434
|
+
]),
|
|
435
|
+
);
|
|
436
|
+
expect(data.done).toEqual(false);
|
|
437
|
+
|
|
438
|
+
const data2 = await stream.read(32);
|
|
439
|
+
expect(data2.value).toEqual(
|
|
440
|
+
new Uint8Array([
|
|
441
|
+
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
|
|
442
|
+
57, 58, 59, 60, 61, 62, 63,
|
|
443
|
+
]),
|
|
444
|
+
);
|
|
445
|
+
expect(data2.done).toEqual(false);
|
|
446
|
+
|
|
447
|
+
const data3 = await stream.read(32);
|
|
448
|
+
expect(data3.value).toEqual(new Uint8Array([]));
|
|
449
|
+
expect(data3.done).toEqual(true);
|
|
450
|
+
|
|
451
|
+
expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(4);
|
|
452
|
+
expect(cryptoService.decryptBlock).toHaveBeenCalledWith(expect.anything(), {
|
|
453
|
+
key: 'privateKey',
|
|
454
|
+
contentKeyPacketSessionKey: 'contentSessionKey',
|
|
455
|
+
verificationKeys: 'verificationKeys',
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should read the stream with seeking', async () => {
|
|
460
|
+
const stream = downloader.getSeekableStream();
|
|
461
|
+
|
|
462
|
+
const data1 = await stream.read(5);
|
|
463
|
+
expect(data1.value).toEqual(new Uint8Array([0, 1, 2, 3, 4]));
|
|
464
|
+
expect(data1.done).toEqual(false);
|
|
465
|
+
expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(1);
|
|
466
|
+
|
|
467
|
+
await stream.seek(10);
|
|
468
|
+
|
|
469
|
+
// Seek withing first block, so no new block is downloaded.
|
|
470
|
+
const data2 = await stream.read(5);
|
|
471
|
+
expect(data2.value).toEqual(new Uint8Array([10, 11, 12, 13, 14]));
|
|
472
|
+
expect(data2.done).toEqual(false);
|
|
473
|
+
expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(1);
|
|
474
|
+
|
|
475
|
+
// Seek and read from second and third blocks.
|
|
476
|
+
await stream.seek(30);
|
|
477
|
+
|
|
478
|
+
const data3 = await stream.read(5);
|
|
479
|
+
expect(data3.value).toEqual(new Uint8Array([30, 31, 32, 33, 34]));
|
|
480
|
+
expect(data3.done).toEqual(false);
|
|
481
|
+
expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(3);
|
|
482
|
+
|
|
483
|
+
expect(cryptoService.decryptBlock).toHaveBeenCalledWith(expect.anything(), {
|
|
484
|
+
key: 'privateKey',
|
|
485
|
+
contentKeyPacketSessionKey: 'contentSessionKey',
|
|
486
|
+
verificationKeys: 'verificationKeys',
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
});
|
|
397
490
|
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { PrivateKey, SessionKey, base64StringToUint8Array } from '../../crypto';
|
|
2
|
-
import { Logger
|
|
2
|
+
import { Logger } from '../../interface';
|
|
3
3
|
import { LoggerWithPrefix } from '../../telemetry';
|
|
4
4
|
import { APIHTTPError, HTTPErrorCode } from '../apiService';
|
|
5
|
+
import { DecryptedRevision } from '../nodes';
|
|
5
6
|
import { DownloadAPIService } from './apiService';
|
|
7
|
+
import { getBlockIndex } from './blockIndex';
|
|
6
8
|
import { DownloadController } from './controller';
|
|
7
9
|
import { DownloadCryptoService } from './cryptoService';
|
|
8
10
|
import { BlockMetadata, RevisionKeys } from './interface';
|
|
11
|
+
import { BufferedSeekableStream } from './seekableStream';
|
|
9
12
|
import { DownloadTelemetry } from './telemetry';
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -33,7 +36,7 @@ export class FileDownloader {
|
|
|
33
36
|
private apiService: DownloadAPIService,
|
|
34
37
|
private cryptoService: DownloadCryptoService,
|
|
35
38
|
private nodeKey: { key: PrivateKey; contentKeyPacketSessionKey: SessionKey },
|
|
36
|
-
private revision:
|
|
39
|
+
private revision: DecryptedRevision,
|
|
37
40
|
private signal?: AbortSignal,
|
|
38
41
|
private onFinish?: () => void,
|
|
39
42
|
) {
|
|
@@ -52,6 +55,89 @@ export class FileDownloader {
|
|
|
52
55
|
return this.revision.claimedSize;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
getSeekableStream(): BufferedSeekableStream {
|
|
59
|
+
let position = 0;
|
|
60
|
+
let cryptoKeys: RevisionKeys;
|
|
61
|
+
|
|
62
|
+
const logger = new LoggerWithPrefix(this.logger, `seekable stream`);
|
|
63
|
+
|
|
64
|
+
const claimedBlockSizes = this.revision.claimedBlockSizes;
|
|
65
|
+
if (!claimedBlockSizes) {
|
|
66
|
+
// Old nodes will not have claimed block sizes. One option is to
|
|
67
|
+
// use default block size, but old clients didn't use the same
|
|
68
|
+
// size (4 MiB vs 4 MB, for example).
|
|
69
|
+
// Ideally, we should throw error that client can easily handle,
|
|
70
|
+
// at the same time, new nodes shouldn't have this issue.
|
|
71
|
+
// For now, we throw general error that client must handle as any
|
|
72
|
+
// error from download - do not support seeking and ask user to
|
|
73
|
+
// download the whole file instead.
|
|
74
|
+
// In the future, we might either change this error, or have some
|
|
75
|
+
// clever way to detect block sizes from the first block and work
|
|
76
|
+
// around this issue.
|
|
77
|
+
throw new Error('Revision does not have defined claimed block sizes');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stream = new BufferedSeekableStream({
|
|
81
|
+
start: async () => {
|
|
82
|
+
logger.debug(`Starting`);
|
|
83
|
+
cryptoKeys = await this.cryptoService.getRevisionKeys(this.nodeKey, this.revision);
|
|
84
|
+
},
|
|
85
|
+
pull: async (controller) => {
|
|
86
|
+
logger.debug(`Pulling at position ${position}`);
|
|
87
|
+
|
|
88
|
+
const result = await this.downloadDataFromPosition(claimedBlockSizes, position, cryptoKeys);
|
|
89
|
+
if (result instanceof Error) {
|
|
90
|
+
logger.error('Download failed', result);
|
|
91
|
+
controller.error(result);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!result) {
|
|
95
|
+
logger.debug(`Download finished at position ${position}`);
|
|
96
|
+
controller.close();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
controller.enqueue(result);
|
|
100
|
+
position += result.length;
|
|
101
|
+
},
|
|
102
|
+
cancel: (reason?: unknown) => {
|
|
103
|
+
logger.info(`Cancelled: ${reason}`);
|
|
104
|
+
this.onFinish?.();
|
|
105
|
+
},
|
|
106
|
+
seek: async (newPosition) => {
|
|
107
|
+
logger.info(`Seeking to position ${newPosition}`);
|
|
108
|
+
position = newPosition;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return stream;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async downloadDataFromPosition(
|
|
115
|
+
claimedBlockSizes: number[],
|
|
116
|
+
position: number,
|
|
117
|
+
cryptoKeys: RevisionKeys,
|
|
118
|
+
): Promise<Uint8Array | Error | undefined> {
|
|
119
|
+
const { value, done } = getBlockIndex(claimedBlockSizes, position);
|
|
120
|
+
if (done) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.logger.info(`Downloading data from block ${value.blockIndex} at offset ${value.blockOffset}`);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const { blockIndex, blockOffset } = value;
|
|
128
|
+
const blockMetadata = await this.apiService.getRevisionBlockToken(
|
|
129
|
+
this.revision.uid,
|
|
130
|
+
blockIndex,
|
|
131
|
+
this.signal,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const blockData = await this.downloadBlockData(blockMetadata, true, cryptoKeys);
|
|
135
|
+
return blockData.slice(blockOffset);
|
|
136
|
+
} catch (error: unknown) {
|
|
137
|
+
return error instanceof Error ? error : new Error(`Unknown error: ${error}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
55
141
|
writeToStream(stream: WritableStream, onProgress?: (downloadedBytes: number) => void): DownloadController {
|
|
56
142
|
if (this.controller.promise) {
|
|
57
143
|
throw new Error(`Download already started`);
|
|
@@ -71,7 +157,7 @@ export class FileDownloader {
|
|
|
71
157
|
|
|
72
158
|
private async downloadToStream(
|
|
73
159
|
stream: WritableStream,
|
|
74
|
-
onProgress?: (
|
|
160
|
+
onProgress?: (downloadedBytes: number) => void,
|
|
75
161
|
ignoreIntegrityErrors = false,
|
|
76
162
|
): Promise<void> {
|
|
77
163
|
const writer = stream.getWriter();
|
|
@@ -113,12 +199,16 @@ export class FileDownloader {
|
|
|
113
199
|
this.ongoingDownloads.set(blockMetadata.index, { downloadPromise });
|
|
114
200
|
|
|
115
201
|
await this.waitForDownloadCapacity();
|
|
116
|
-
await this.flushCompletedBlocks(
|
|
202
|
+
await this.flushCompletedBlocks(async (chunk) => {
|
|
203
|
+
await writer.write(chunk);
|
|
204
|
+
});
|
|
117
205
|
}
|
|
118
206
|
|
|
119
207
|
this.logger.debug(`All blocks downloading, waiting for them to finish`);
|
|
120
208
|
await Promise.all(this.downloadPromises);
|
|
121
|
-
await this.flushCompletedBlocks(
|
|
209
|
+
await this.flushCompletedBlocks(async (chunk) => {
|
|
210
|
+
await writer.write(chunk);
|
|
211
|
+
});
|
|
122
212
|
|
|
123
213
|
if (this.ongoingDownloads.size > 0) {
|
|
124
214
|
this.logger.error(`Some blocks were not downloaded: ${this.ongoingDownloads.keys()}`);
|
|
@@ -158,6 +248,16 @@ export class FileDownloader {
|
|
|
158
248
|
cryptoKeys: RevisionKeys,
|
|
159
249
|
onProgress: (downloadedBytes: number) => void,
|
|
160
250
|
) {
|
|
251
|
+
const blockData = await this.downloadBlockData(blockMetadata, ignoreIntegrityErrors, cryptoKeys, onProgress);
|
|
252
|
+
this.ongoingDownloads.get(blockMetadata.index)!.decryptedBufferedBlock = blockData;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async downloadBlockData(
|
|
256
|
+
blockMetadata: BlockMetadata,
|
|
257
|
+
ignoreIntegrityErrors: boolean,
|
|
258
|
+
cryptoKeys: RevisionKeys,
|
|
259
|
+
onProgress?: (downloadedBytes: number) => void,
|
|
260
|
+
): Promise<Uint8Array> {
|
|
161
261
|
const logger = new LoggerWithPrefix(this.logger, `block ${blockMetadata.index}`);
|
|
162
262
|
logger.info(`Download started`);
|
|
163
263
|
|
|
@@ -219,8 +319,8 @@ export class FileDownloader {
|
|
|
219
319
|
}
|
|
220
320
|
}
|
|
221
321
|
|
|
222
|
-
this.ongoingDownloads.get(blockMetadata.index)!.decryptedBufferedBlock = decryptedBlock;
|
|
223
322
|
logger.info(`Downloaded`);
|
|
323
|
+
return decryptedBlock;
|
|
224
324
|
}
|
|
225
325
|
|
|
226
326
|
private async waitForDownloadCapacity() {
|
|
@@ -251,16 +351,16 @@ export class FileDownloader {
|
|
|
251
351
|
}
|
|
252
352
|
}
|
|
253
353
|
|
|
254
|
-
private async flushCompletedBlocks(
|
|
354
|
+
private async flushCompletedBlocks(write: (chunk: Uint8Array) => void | Promise<void>) {
|
|
255
355
|
this.logger.debug(`Flushing completed blocks`);
|
|
256
356
|
while (this.isNextBlockDownloaded) {
|
|
257
357
|
const decryptedBlock = this.ongoingDownloads.get(this.nextBlockIndex)!.decryptedBufferedBlock!;
|
|
258
358
|
this.logger.info(`Flushing completed block ${this.nextBlockIndex}`);
|
|
259
359
|
try {
|
|
260
|
-
await
|
|
360
|
+
await write(decryptedBlock);
|
|
261
361
|
} catch (error) {
|
|
262
362
|
this.logger.error(`Failed to write block, retrying once`, error);
|
|
263
|
-
await
|
|
363
|
+
await write(decryptedBlock);
|
|
264
364
|
}
|
|
265
365
|
this.ongoingDownloads.delete(this.nextBlockIndex);
|
|
266
366
|
this.nextBlockIndex++;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PrivateKey, PublicKey, SessionKey } from '../../crypto';
|
|
2
|
-
import { NodeType, Result,
|
|
3
|
-
import { DecryptedNode } from '../nodes';
|
|
2
|
+
import { NodeType, Result, MissingNode, MetricVolumeType } from '../../interface';
|
|
3
|
+
import { DecryptedNode, DecryptedRevision } from '../nodes';
|
|
4
4
|
|
|
5
5
|
export type BlockMetadata = {
|
|
6
6
|
index: number;
|
|
@@ -29,9 +29,9 @@ export interface NodesService {
|
|
|
29
29
|
export interface NodesServiceNode {
|
|
30
30
|
uid: string;
|
|
31
31
|
type: NodeType;
|
|
32
|
-
activeRevision?: Result<
|
|
32
|
+
activeRevision?: Result<DecryptedRevision, Error>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface RevisionsService {
|
|
36
|
-
getRevision(nodeRevisionUid: string): Promise<
|
|
36
|
+
getRevision(nodeRevisionUid: string): Promise<DecryptedRevision>;
|
|
37
37
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { SeekableReadableStream, BufferedSeekableStream } from './seekableStream';
|
|
2
|
+
|
|
3
|
+
describe('SeekableReadableStream', () => {
|
|
4
|
+
it('should call the seek callback when seek is called', async () => {
|
|
5
|
+
const mockSeek = jest.fn().mockResolvedValue(undefined);
|
|
6
|
+
const mockStart = jest.fn();
|
|
7
|
+
|
|
8
|
+
const stream = new SeekableReadableStream({
|
|
9
|
+
start: mockStart,
|
|
10
|
+
seek: mockSeek,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
await stream.seek(100);
|
|
14
|
+
|
|
15
|
+
expect(mockSeek).toHaveBeenCalledWith(100);
|
|
16
|
+
expect(mockSeek).toHaveBeenCalledTimes(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle synchronous seek callback', async () => {
|
|
20
|
+
const mockSeek = jest.fn().mockReturnValue(undefined);
|
|
21
|
+
|
|
22
|
+
const stream = new SeekableReadableStream({
|
|
23
|
+
seek: mockSeek,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await stream.seek(250);
|
|
27
|
+
|
|
28
|
+
expect(mockSeek).toHaveBeenCalledWith(250);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('BufferedSeekableStream', () => {
|
|
33
|
+
let startWithCloseMock: jest.Mock;
|
|
34
|
+
let pullMock: jest.Mock;
|
|
35
|
+
|
|
36
|
+
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
|
|
37
|
+
const data2 = new Uint8Array([6, 7, 8, 9, 10]);
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
startWithCloseMock = jest.fn().mockImplementation((controller) => {
|
|
41
|
+
controller.enqueue(data1);
|
|
42
|
+
controller.close();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
let readIndex = 0;
|
|
46
|
+
pullMock = jest.fn().mockImplementation((controller) => {
|
|
47
|
+
if (readIndex === 0) {
|
|
48
|
+
controller.enqueue(data1);
|
|
49
|
+
} else if (readIndex === 1) {
|
|
50
|
+
controller.enqueue(data2);
|
|
51
|
+
} else {
|
|
52
|
+
controller.close();
|
|
53
|
+
}
|
|
54
|
+
readIndex++;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw error if highWaterMark is not 0', () => {
|
|
59
|
+
expect(() => {
|
|
60
|
+
new BufferedSeekableStream({ seek: jest.fn() }, { highWaterMark: 1 });
|
|
61
|
+
}).toThrow('highWaterMark must be 0');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should throw error when reading invalid number of bytes', async () => {
|
|
65
|
+
const stream = new BufferedSeekableStream({
|
|
66
|
+
seek: jest.fn(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await expect(stream.read(0)).rejects.toThrow('Invalid number of bytes to read');
|
|
70
|
+
await expect(stream.read(-1)).rejects.toThrow('Invalid number of bytes to read');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should read exact number of bytes when underlying source provides exact amount', async () => {
|
|
74
|
+
const stream = new BufferedSeekableStream({
|
|
75
|
+
start: startWithCloseMock,
|
|
76
|
+
seek: jest.fn(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await stream.read(5);
|
|
80
|
+
|
|
81
|
+
expect(result).toEqual({ value: data1, done: false });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should buffer extra bytes when underlying source provides more than requested', async () => {
|
|
85
|
+
const stream = new BufferedSeekableStream({
|
|
86
|
+
pull: pullMock,
|
|
87
|
+
seek: jest.fn(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result1 = await stream.read(3);
|
|
91
|
+
expect(result1).toEqual({ value: new Uint8Array([1, 2, 3]), done: false });
|
|
92
|
+
expect(pullMock).toHaveBeenCalledTimes(1);
|
|
93
|
+
|
|
94
|
+
const result2 = await stream.read(2);
|
|
95
|
+
expect(result2).toEqual({ value: new Uint8Array([4, 5]), done: false });
|
|
96
|
+
expect(pullMock).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should use buffered data and read more when buffer is not enough for next read', async () => {
|
|
100
|
+
const stream = new BufferedSeekableStream({
|
|
101
|
+
pull: pullMock,
|
|
102
|
+
seek: jest.fn(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result1 = await stream.read(3);
|
|
106
|
+
expect(result1).toEqual({ value: new Uint8Array([1, 2, 3]), done: false });
|
|
107
|
+
expect(pullMock).toHaveBeenCalledTimes(1);
|
|
108
|
+
|
|
109
|
+
const result2 = await stream.read(5);
|
|
110
|
+
expect(result2).toEqual({ value: new Uint8Array([4, 5, 6, 7, 8]), done: false });
|
|
111
|
+
expect(pullMock).toHaveBeenCalledTimes(2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle end of file gracefully when not enough data available', async () => {
|
|
115
|
+
const stream = new BufferedSeekableStream({
|
|
116
|
+
start: startWithCloseMock,
|
|
117
|
+
seek: jest.fn(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await stream.read(10);
|
|
121
|
+
expect(result).toEqual({ value: data1, done: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should clear buffer when seeking back', async () => {
|
|
125
|
+
const stream = new BufferedSeekableStream({
|
|
126
|
+
pull: pullMock,
|
|
127
|
+
seek: jest.fn(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result1 = await stream.read(2);
|
|
131
|
+
expect(result1).toEqual({ value: new Uint8Array([1, 2]), done: false });
|
|
132
|
+
|
|
133
|
+
await stream.seek(0);
|
|
134
|
+
|
|
135
|
+
const result2 = await stream.read(3);
|
|
136
|
+
expect(result2).toEqual({ value: new Uint8Array([6, 7, 8]), done: false });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should clear buffer when seeking past buffer end', async () => {
|
|
140
|
+
const stream = new BufferedSeekableStream({
|
|
141
|
+
pull: pullMock,
|
|
142
|
+
seek: jest.fn(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result1 = await stream.read(2);
|
|
146
|
+
expect(result1).toEqual({ value: new Uint8Array([1, 2]), done: false });
|
|
147
|
+
|
|
148
|
+
await stream.seek(100);
|
|
149
|
+
|
|
150
|
+
const result2 = await stream.read(3);
|
|
151
|
+
expect(result2).toEqual({ value: new Uint8Array([6, 7, 8]), done: false });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should update buffer correctly when seeking within buffer range', async () => {
|
|
155
|
+
const stream = new BufferedSeekableStream({
|
|
156
|
+
pull: pullMock,
|
|
157
|
+
seek: jest.fn(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result1 = await stream.read(1);
|
|
161
|
+
expect(result1).toEqual({ value: new Uint8Array([1]), done: false });
|
|
162
|
+
|
|
163
|
+
await stream.seek(3);
|
|
164
|
+
|
|
165
|
+
const result2 = await stream.read(3);
|
|
166
|
+
expect(result2).toEqual({ value: new Uint8Array([4, 5, 6]), done: false });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle multiple read operations correctly', async () => {
|
|
170
|
+
const stream = new BufferedSeekableStream({
|
|
171
|
+
pull: pullMock,
|
|
172
|
+
seek: jest.fn(),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result1 = await stream.read(2);
|
|
176
|
+
expect(result1).toEqual({ value: new Uint8Array([1, 2]), done: false });
|
|
177
|
+
|
|
178
|
+
const result2 = await stream.read(4);
|
|
179
|
+
expect(result2).toEqual({ value: new Uint8Array([3, 4, 5, 6]), done: false });
|
|
180
|
+
|
|
181
|
+
const result3 = await stream.read(3);
|
|
182
|
+
expect(result3).toEqual({ value: new Uint8Array([7, 8, 9]), done: false });
|
|
183
|
+
|
|
184
|
+
const result4 = await stream.read(2);
|
|
185
|
+
expect(result4).toEqual({ value: new Uint8Array([10]), done: true });
|
|
186
|
+
});
|
|
187
|
+
});
|