@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.
Files changed (82) hide show
  1. package/dist/errors.d.ts +1 -1
  2. package/dist/errors.js +2 -2
  3. package/dist/errors.js.map +1 -1
  4. package/dist/interface/download.d.ts +14 -0
  5. package/dist/internal/apiService/apiService.js +6 -2
  6. package/dist/internal/apiService/apiService.js.map +1 -1
  7. package/dist/internal/apiService/apiService.test.js +9 -1
  8. package/dist/internal/apiService/apiService.test.js.map +1 -1
  9. package/dist/internal/download/controller.d.ts +3 -0
  10. package/dist/internal/download/controller.js +7 -0
  11. package/dist/internal/download/controller.js.map +1 -1
  12. package/dist/internal/download/cryptoService.js +9 -2
  13. package/dist/internal/download/cryptoService.js.map +1 -1
  14. package/dist/internal/download/fileDownloader.js +9 -3
  15. package/dist/internal/download/fileDownloader.js.map +1 -1
  16. package/dist/internal/download/fileDownloader.test.js +14 -11
  17. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  18. package/dist/internal/download/interface.d.ts +14 -0
  19. package/dist/internal/download/interface.js +16 -0
  20. package/dist/internal/download/interface.js.map +1 -1
  21. package/dist/internal/download/seekableStream.d.ts +1 -2
  22. package/dist/internal/download/seekableStream.js +28 -11
  23. package/dist/internal/download/seekableStream.js.map +1 -1
  24. package/dist/internal/download/seekableStream.test.js +100 -0
  25. package/dist/internal/download/seekableStream.test.js.map +1 -1
  26. package/dist/internal/nodes/apiService.d.ts +1 -0
  27. package/dist/internal/nodes/apiService.js +3 -0
  28. package/dist/internal/nodes/apiService.js.map +1 -1
  29. package/dist/internal/nodes/nodesManagement.d.ts +1 -0
  30. package/dist/internal/nodes/nodesManagement.js +5 -0
  31. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  32. package/dist/internal/photos/index.js +2 -2
  33. package/dist/internal/photos/index.js.map +1 -1
  34. package/dist/internal/upload/apiService.d.ts +1 -0
  35. package/dist/internal/upload/apiService.js +12 -0
  36. package/dist/internal/upload/apiService.js.map +1 -1
  37. package/dist/internal/upload/index.js +4 -4
  38. package/dist/internal/upload/index.js.map +1 -1
  39. package/dist/internal/upload/manager.js +19 -1
  40. package/dist/internal/upload/manager.js.map +1 -1
  41. package/dist/internal/upload/manager.test.js +23 -0
  42. package/dist/internal/upload/manager.test.js.map +1 -1
  43. package/dist/internal/upload/queue.d.ts +18 -3
  44. package/dist/internal/upload/queue.js +27 -8
  45. package/dist/internal/upload/queue.js.map +1 -1
  46. package/dist/internal/upload/queue.test.d.ts +1 -0
  47. package/dist/internal/upload/queue.test.js +103 -0
  48. package/dist/internal/upload/queue.test.js.map +1 -0
  49. package/dist/internal/upload/streamUploader.d.ts +3 -0
  50. package/dist/internal/upload/streamUploader.js +30 -2
  51. package/dist/internal/upload/streamUploader.js.map +1 -1
  52. package/dist/internal/upload/streamUploader.test.js +65 -6
  53. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  54. package/dist/internal/upload/telemetry.js +2 -3
  55. package/dist/internal/upload/telemetry.js.map +1 -1
  56. package/dist/protonDriveClient.js +1 -1
  57. package/dist/protonDriveClient.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/errors.ts +2 -2
  60. package/src/interface/download.ts +16 -0
  61. package/src/internal/apiService/apiService.test.ts +12 -2
  62. package/src/internal/apiService/apiService.ts +7 -2
  63. package/src/internal/download/controller.ts +9 -0
  64. package/src/internal/download/cryptoService.ts +13 -3
  65. package/src/internal/download/fileDownloader.test.ts +17 -11
  66. package/src/internal/download/fileDownloader.ts +9 -5
  67. package/src/internal/download/interface.ts +15 -0
  68. package/src/internal/download/seekableStream.test.ts +126 -1
  69. package/src/internal/download/seekableStream.ts +34 -13
  70. package/src/internal/nodes/apiService.ts +7 -0
  71. package/src/internal/nodes/nodesManagement.ts +6 -0
  72. package/src/internal/photos/index.ts +2 -2
  73. package/src/internal/upload/apiService.ts +25 -0
  74. package/src/internal/upload/index.ts +4 -4
  75. package/src/internal/upload/manager.test.ts +37 -0
  76. package/src/internal/upload/manager.ts +17 -1
  77. package/src/internal/upload/queue.test.ts +130 -0
  78. package/src/internal/upload/queue.ts +34 -9
  79. package/src/internal/upload/streamUploader.test.ts +80 -7
  80. package/src/internal/upload/streamUploader.ts +34 -2
  81. package/src/internal/upload/telemetry.ts +2 -3
  82. 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 >= MAX_UPLOADING_BLOCKS) {
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('download');
16
+ this.logger = this.telemetry.getLogger('upload');
17
17
  this.sharesService = sharesService;
18
18
  }
19
19
 
20
20
  getLoggerForRevision(revisionUid: string) {
21
- const logger = this.telemetry.getLogger('upload');
22
- return new LoggerWithPrefix(logger, `revision ${revisionUid}`);
21
+ return new LoggerWithPrefix(this.logger, `revision ${revisionUid}`);
23
22
  }
24
23
 
25
24
  logBlockVerificationError(retryHelped: boolean) {
@@ -514,7 +514,7 @@ export class ProtonDriveClient {
514
514
 
515
515
  async emptyTrash(): Promise<void> {
516
516
  this.logger.info('Emptying trash');
517
- throw new Error('Method not implemented');
517
+ return this.nodes.management.emptyTrash();
518
518
  }
519
519
 
520
520
  /**