@protontech/drive-sdk 0.8.0 → 0.9.1
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/errors.d.ts +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/interface/download.d.ts +14 -0
- package/dist/internal/apiService/apiService.js +6 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +9 -1
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/download/controller.d.ts +3 -0
- package/dist/internal/download/controller.js +7 -0
- package/dist/internal/download/controller.js.map +1 -1
- package/dist/internal/download/cryptoService.js +9 -2
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.js +9 -3
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +14 -11
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/download/interface.d.ts +14 -0
- package/dist/internal/download/interface.js +16 -0
- package/dist/internal/download/interface.js.map +1 -1
- package/dist/internal/download/seekableStream.d.ts +1 -2
- package/dist/internal/download/seekableStream.js +28 -11
- package/dist/internal/download/seekableStream.js.map +1 -1
- package/dist/internal/download/seekableStream.test.js +100 -0
- package/dist/internal/download/seekableStream.test.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +1 -0
- package/dist/internal/nodes/apiService.js +3 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +1 -0
- package/dist/internal/nodes/nodesManagement.js +5 -0
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/index.js +2 -2
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/upload/apiService.d.ts +1 -0
- package/dist/internal/upload/apiService.js +12 -0
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/index.js +4 -4
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/manager.js +19 -1
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +23 -0
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/queue.d.ts +18 -3
- package/dist/internal/upload/queue.js +27 -8
- package/dist/internal/upload/queue.js.map +1 -1
- package/dist/internal/upload/queue.test.d.ts +1 -0
- package/dist/internal/upload/queue.test.js +103 -0
- package/dist/internal/upload/queue.test.js.map +1 -0
- package/dist/internal/upload/streamUploader.d.ts +3 -0
- package/dist/internal/upload/streamUploader.js +30 -2
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +65 -6
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/internal/upload/telemetry.js +2 -3
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/protonDriveClient.js +1 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +2 -2
- package/src/interface/download.ts +16 -0
- package/src/internal/apiService/apiService.test.ts +12 -2
- package/src/internal/apiService/apiService.ts +7 -2
- package/src/internal/download/controller.ts +9 -0
- package/src/internal/download/cryptoService.ts +13 -3
- package/src/internal/download/fileDownloader.test.ts +17 -11
- package/src/internal/download/fileDownloader.ts +9 -5
- package/src/internal/download/interface.ts +15 -0
- package/src/internal/download/seekableStream.test.ts +126 -1
- package/src/internal/download/seekableStream.ts +34 -13
- package/src/internal/nodes/apiService.ts +7 -0
- package/src/internal/nodes/nodesManagement.ts +6 -0
- package/src/internal/photos/index.ts +2 -2
- package/src/internal/upload/apiService.ts +25 -0
- package/src/internal/upload/index.ts +4 -4
- package/src/internal/upload/manager.test.ts +37 -0
- package/src/internal/upload/manager.ts +17 -1
- package/src/internal/upload/queue.test.ts +130 -0
- package/src/internal/upload/queue.ts +34 -9
- package/src/internal/upload/streamUploader.test.ts +80 -7
- package/src/internal/upload/streamUploader.ts +34 -2
- package/src/internal/upload/telemetry.ts +2 -3
- package/src/protonDriveClient.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Thumbnail, ThumbnailType, UploadMetadata } from '../../interface';
|
|
1
|
+
import { Logger, Thumbnail, ThumbnailType, UploadMetadata } from '../../interface';
|
|
2
2
|
import { IntegrityError } from '../../errors';
|
|
3
|
+
import { getMockLogger } from '../../tests/logger';
|
|
3
4
|
import { APIHTTPError, HTTPErrorCode } from '../apiService';
|
|
4
5
|
import { FILE_CHUNK_SIZE, StreamUploader } from './streamUploader';
|
|
5
6
|
import { UploadTelemetry } from './telemetry';
|
|
@@ -40,6 +41,7 @@ function mockUploadBlock(
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
describe('StreamUploader', () => {
|
|
44
|
+
let logger: Logger;
|
|
43
45
|
let telemetry: UploadTelemetry;
|
|
44
46
|
let apiService: jest.Mocked<UploadAPIService>;
|
|
45
47
|
let cryptoService: UploadCryptoService;
|
|
@@ -54,14 +56,11 @@ describe('StreamUploader', () => {
|
|
|
54
56
|
let uploader: StreamUploader;
|
|
55
57
|
|
|
56
58
|
beforeEach(() => {
|
|
59
|
+
logger = getMockLogger();
|
|
60
|
+
|
|
57
61
|
// @ts-expect-error No need to implement all methods for mocking
|
|
58
62
|
telemetry = {
|
|
59
|
-
getLoggerForRevision: jest.fn().mockReturnValue(
|
|
60
|
-
debug: jest.fn(),
|
|
61
|
-
info: jest.fn(),
|
|
62
|
-
warn: jest.fn(),
|
|
63
|
-
error: jest.fn(),
|
|
64
|
-
}),
|
|
63
|
+
getLoggerForRevision: jest.fn().mockReturnValue(logger),
|
|
65
64
|
logBlockVerificationError: jest.fn(),
|
|
66
65
|
uploadFailed: jest.fn(),
|
|
67
66
|
uploadFinished: jest.fn(),
|
|
@@ -403,6 +402,80 @@ describe('StreamUploader', () => {
|
|
|
403
402
|
await verifyOnProgress([1024, 4 * 1024 * 1024, 2 * 1024 * 1024, 4 * 1024 * 1024]);
|
|
404
403
|
});
|
|
405
404
|
|
|
405
|
+
it('should handle timeout when uploading block', async () => {
|
|
406
|
+
const error = new Error('TimeoutError');
|
|
407
|
+
error.name = 'TimeoutError';
|
|
408
|
+
|
|
409
|
+
let count = 0;
|
|
410
|
+
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
411
|
+
if (token === 'token/block:1' && count === 0) {
|
|
412
|
+
count++;
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect((uploader as any).maxUploadingBlocks).toEqual(5);
|
|
419
|
+
await verifySuccess();
|
|
420
|
+
expect(apiService.requestBlockUpload).toHaveBeenCalledTimes(1);
|
|
421
|
+
// 3 blocks + 1 timeout retry + 1 thumbnail
|
|
422
|
+
expect(apiService.uploadBlock).toHaveBeenCalledTimes(5);
|
|
423
|
+
expect(logger.warn).toHaveBeenCalledTimes(2);
|
|
424
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
425
|
+
'block 1:token/block:1: Upload timeout, limiting upload capacity to 1 block',
|
|
426
|
+
);
|
|
427
|
+
expect(logger.warn).toHaveBeenCalledWith('block 1:token/block:1: Upload timeout, retrying');
|
|
428
|
+
expect((uploader as any).maxUploadingBlocks).toEqual(1);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('limitUploadCapacity should wait for the previous blocks to finish', async () => {
|
|
432
|
+
const error = new Error('TimeoutError');
|
|
433
|
+
error.name = 'TimeoutError';
|
|
434
|
+
|
|
435
|
+
const events: string[] = [];
|
|
436
|
+
let block1Resolver: (() => void) | undefined;
|
|
437
|
+
let block2FirstAttempt = true;
|
|
438
|
+
|
|
439
|
+
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
440
|
+
if (token === 'token/block:1') {
|
|
441
|
+
events.push('block1:upload:start');
|
|
442
|
+
await new Promise<void>((resolve) => {
|
|
443
|
+
block1Resolver = resolve;
|
|
444
|
+
});
|
|
445
|
+
events.push('block1:upload:end');
|
|
446
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
447
|
+
}
|
|
448
|
+
if (token === 'token/block:2') {
|
|
449
|
+
if (block2FirstAttempt) {
|
|
450
|
+
block2FirstAttempt = false;
|
|
451
|
+
events.push('block2:timeout');
|
|
452
|
+
// Resolve block 1 after a small delay to simulate real-world conditions
|
|
453
|
+
setTimeout(() => block1Resolver?.(), 100);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
events.push('block2:retry');
|
|
457
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
458
|
+
}
|
|
459
|
+
// Block 3 and thumbnails proceed normally
|
|
460
|
+
return mockUploadBlock(bareUrl, token, block, onProgress);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await verifySuccess();
|
|
464
|
+
|
|
465
|
+
expect(events).toMatchObject([
|
|
466
|
+
'block1:upload:start',
|
|
467
|
+
'block2:timeout',
|
|
468
|
+
'block1:upload:end',
|
|
469
|
+
'block2:retry',
|
|
470
|
+
]);
|
|
471
|
+
|
|
472
|
+
// Also verify the warning messages were logged
|
|
473
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
474
|
+
'block 2:token/block:2: Upload timeout, limiting upload capacity to 1 block',
|
|
475
|
+
);
|
|
476
|
+
expect(logger.warn).toHaveBeenCalledWith('block 2:token/block:2: Upload timeout, retrying');
|
|
477
|
+
});
|
|
478
|
+
|
|
406
479
|
it('should handle expired token when uploading block', async () => {
|
|
407
480
|
let count = 0;
|
|
408
481
|
apiService.uploadBlock = jest.fn().mockImplementation(async function (bareUrl, token, block, onProgress) {
|
|
@@ -55,6 +55,8 @@ const MAX_BLOCK_UPLOAD_RETRIES = 3;
|
|
|
55
55
|
* that the upload process is efficient and does not overload the server.
|
|
56
56
|
*/
|
|
57
57
|
export class StreamUploader {
|
|
58
|
+
protected maxUploadingBlocks = MAX_UPLOADING_BLOCKS;
|
|
59
|
+
|
|
58
60
|
protected logger: Logger;
|
|
59
61
|
|
|
60
62
|
protected digests: UploadDigests;
|
|
@@ -67,6 +69,7 @@ export class StreamUploader {
|
|
|
67
69
|
protected ongoingUploads = new Map<
|
|
68
70
|
string,
|
|
69
71
|
{
|
|
72
|
+
index?: number;
|
|
70
73
|
uploadPromise: Promise<void>;
|
|
71
74
|
encryptedBlock: EncryptedBlock | EncryptedThumbnail;
|
|
72
75
|
}
|
|
@@ -181,7 +184,7 @@ export class StreamUploader {
|
|
|
181
184
|
await this.controller.waitWhilePaused();
|
|
182
185
|
await this.waitForUploadCapacityAndBufferedBlocks();
|
|
183
186
|
|
|
184
|
-
if (this.isEncryptionFullyFinished) {
|
|
187
|
+
if (this.isEncryptionFullyFinished || this.isUploadAborted) {
|
|
185
188
|
break;
|
|
186
189
|
}
|
|
187
190
|
|
|
@@ -376,6 +379,7 @@ export class StreamUploader {
|
|
|
376
379
|
|
|
377
380
|
const uploadKey = `block:${blockToken.index}`;
|
|
378
381
|
this.ongoingUploads.set(uploadKey, {
|
|
382
|
+
index: blockToken.index,
|
|
379
383
|
uploadPromise: this.uploadBlock(blockToken, encryptedBlock, onProgress).finally(() => {
|
|
380
384
|
this.ongoingUploads.delete(uploadKey);
|
|
381
385
|
|
|
@@ -500,6 +504,13 @@ export class StreamUploader {
|
|
|
500
504
|
blockProgress = 0;
|
|
501
505
|
}
|
|
502
506
|
|
|
507
|
+
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
508
|
+
logger.warn(`Upload timeout, limiting upload capacity to 1 block`);
|
|
509
|
+
await this.limitUploadCapacity(uploadToken.index);
|
|
510
|
+
logger.warn(`Upload timeout, retrying`);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
503
514
|
if (
|
|
504
515
|
(error instanceof APIHTTPError && error.statusCode === HTTPErrorCode.NOT_FOUND) ||
|
|
505
516
|
error instanceof NotFoundAPIError
|
|
@@ -542,6 +553,27 @@ export class StreamUploader {
|
|
|
542
553
|
logger.info(`Uploaded`);
|
|
543
554
|
}
|
|
544
555
|
|
|
556
|
+
private async limitUploadCapacity(index: number) {
|
|
557
|
+
this.maxUploadingBlocks = 1;
|
|
558
|
+
|
|
559
|
+
// This ensures that when the upload is downscaled, all ongoing block
|
|
560
|
+
// uploads are waiting for their turn one by one.
|
|
561
|
+
try {
|
|
562
|
+
await waitForCondition(() => {
|
|
563
|
+
const ongoingIndexes = Array.from(this.ongoingUploads.values())
|
|
564
|
+
.map(({ index: ongoingIndex }) => ongoingIndex)
|
|
565
|
+
.filter((ongoingIndex) => ongoingIndex !== undefined);
|
|
566
|
+
ongoingIndexes.sort((a, b) => a - b);
|
|
567
|
+
return ongoingIndexes[0] === index;
|
|
568
|
+
}, this.abortController.signal);
|
|
569
|
+
} catch (error: unknown) {
|
|
570
|
+
if (error instanceof AbortError) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
545
577
|
private async waitForBufferCapacity() {
|
|
546
578
|
if (this.encryptedBlocks.size >= MAX_BUFFERED_BLOCKS) {
|
|
547
579
|
try {
|
|
@@ -559,7 +591,7 @@ export class StreamUploader {
|
|
|
559
591
|
}
|
|
560
592
|
|
|
561
593
|
private async waitForUploadCapacityAndBufferedBlocks() {
|
|
562
|
-
while (this.ongoingUploads.size >=
|
|
594
|
+
while (this.ongoingUploads.size >= this.maxUploadingBlocks) {
|
|
563
595
|
await Promise.race(this.ongoingUploads.values().map(({ uploadPromise }) => uploadPromise));
|
|
564
596
|
}
|
|
565
597
|
try {
|
|
@@ -13,13 +13,12 @@ export class UploadTelemetry {
|
|
|
13
13
|
private sharesService: SharesService,
|
|
14
14
|
) {
|
|
15
15
|
this.telemetry = telemetry;
|
|
16
|
-
this.logger = this.telemetry.getLogger('
|
|
16
|
+
this.logger = this.telemetry.getLogger('upload');
|
|
17
17
|
this.sharesService = sharesService;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
getLoggerForRevision(revisionUid: string) {
|
|
21
|
-
|
|
22
|
-
return new LoggerWithPrefix(logger, `revision ${revisionUid}`);
|
|
21
|
+
return new LoggerWithPrefix(this.logger, `revision ${revisionUid}`);
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
logBlockVerificationError(retryHelped: boolean) {
|
package/src/protonDriveClient.ts
CHANGED