@protontech/drive-sdk 0.5.0 → 0.5.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 (151) hide show
  1. package/dist/diagnostic/{sdkDiagnosticFull.d.ts → diagnostic.d.ts} +5 -4
  2. package/dist/diagnostic/{sdkDiagnosticFull.js → diagnostic.js} +13 -10
  3. package/dist/diagnostic/diagnostic.js.map +1 -0
  4. package/dist/diagnostic/index.js +2 -4
  5. package/dist/diagnostic/index.js.map +1 -1
  6. package/dist/diagnostic/interface.d.ts +22 -1
  7. package/dist/diagnostic/sdkDiagnostic.d.ts +3 -2
  8. package/dist/diagnostic/sdkDiagnostic.js +79 -7
  9. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  10. package/dist/interface/index.d.ts +1 -1
  11. package/dist/interface/index.js.map +1 -1
  12. package/dist/interface/nodes.d.ts +9 -0
  13. package/dist/interface/telemetry.d.ts +4 -1
  14. package/dist/interface/telemetry.js.map +1 -1
  15. package/dist/internal/apiService/driveTypes.d.ts +2571 -2356
  16. package/dist/internal/download/controller.d.ts +2 -0
  17. package/dist/internal/download/controller.js +15 -1
  18. package/dist/internal/download/controller.js.map +1 -1
  19. package/dist/internal/download/fileDownloader.js +6 -1
  20. package/dist/internal/download/fileDownloader.js.map +1 -1
  21. package/dist/internal/nodes/apiService.js +27 -12
  22. package/dist/internal/nodes/apiService.js.map +1 -1
  23. package/dist/internal/nodes/apiService.test.js +60 -2
  24. package/dist/internal/nodes/apiService.test.js.map +1 -1
  25. package/dist/internal/nodes/debouncer.d.ts +3 -2
  26. package/dist/internal/nodes/debouncer.js +16 -4
  27. package/dist/internal/nodes/debouncer.js.map +1 -1
  28. package/dist/internal/nodes/debouncer.test.js +20 -12
  29. package/dist/internal/nodes/debouncer.test.js.map +1 -1
  30. package/dist/internal/nodes/extendedAttributes.js +2 -2
  31. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  32. package/dist/internal/nodes/index.js +1 -1
  33. package/dist/internal/nodes/index.js.map +1 -1
  34. package/dist/internal/nodes/nodesAccess.d.ts +5 -4
  35. package/dist/internal/nodes/nodesAccess.js +6 -5
  36. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  37. package/dist/internal/nodes/nodesAccess.test.js +17 -5
  38. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  39. package/dist/internal/nodes/nodesManagement.d.ts +2 -2
  40. package/dist/internal/nodes/nodesManagement.js +5 -3
  41. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  42. package/dist/internal/nodes/nodesManagement.test.js +3 -1
  43. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  44. package/dist/internal/photos/apiService.js +9 -20
  45. package/dist/internal/photos/apiService.js.map +1 -1
  46. package/dist/internal/photos/upload.js +7 -1
  47. package/dist/internal/photos/upload.js.map +1 -1
  48. package/dist/internal/sharing/apiService.d.ts +1 -1
  49. package/dist/internal/sharing/apiService.js +2 -2
  50. package/dist/internal/sharing/apiService.js.map +1 -1
  51. package/dist/internal/sharing/sharingManagement.d.ts +4 -1
  52. package/dist/internal/sharing/sharingManagement.js +7 -4
  53. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  54. package/dist/internal/sharingPublic/apiService.d.ts +8 -10
  55. package/dist/internal/sharingPublic/apiService.js +9 -63
  56. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  57. package/dist/internal/sharingPublic/index.d.ts +3 -3
  58. package/dist/internal/sharingPublic/index.js +5 -11
  59. package/dist/internal/sharingPublic/index.js.map +1 -1
  60. package/dist/internal/sharingPublic/nodes.d.ts +13 -8
  61. package/dist/internal/sharingPublic/nodes.js +20 -2
  62. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  63. package/dist/internal/sharingPublic/session/apiService.d.ts +7 -5
  64. package/dist/internal/sharingPublic/session/apiService.js +25 -4
  65. package/dist/internal/sharingPublic/session/apiService.js.map +1 -1
  66. package/dist/internal/sharingPublic/session/interface.d.ts +17 -0
  67. package/dist/internal/sharingPublic/session/manager.d.ts +12 -4
  68. package/dist/internal/sharingPublic/session/manager.js +14 -4
  69. package/dist/internal/sharingPublic/session/manager.js.map +1 -1
  70. package/dist/internal/sharingPublic/session/session.d.ts +5 -2
  71. package/dist/internal/sharingPublic/session/session.js +7 -3
  72. package/dist/internal/sharingPublic/session/session.js.map +1 -1
  73. package/dist/internal/sharingPublic/shares.d.ts +3 -10
  74. package/dist/internal/sharingPublic/shares.js +10 -33
  75. package/dist/internal/sharingPublic/shares.js.map +1 -1
  76. package/dist/internal/upload/controller.d.ts +3 -1
  77. package/dist/internal/upload/controller.js +16 -2
  78. package/dist/internal/upload/controller.js.map +1 -1
  79. package/dist/internal/upload/fileUploader.js +2 -2
  80. package/dist/internal/upload/fileUploader.js.map +1 -1
  81. package/dist/internal/upload/streamUploader.d.ts +4 -3
  82. package/dist/internal/upload/streamUploader.js +61 -18
  83. package/dist/internal/upload/streamUploader.js.map +1 -1
  84. package/dist/internal/upload/streamUploader.test.js +38 -12
  85. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  86. package/dist/protonDriveClient.d.ts +8 -3
  87. package/dist/protonDriveClient.js +7 -6
  88. package/dist/protonDriveClient.js.map +1 -1
  89. package/dist/protonDrivePublicLinkClient.d.ts +4 -3
  90. package/dist/protonDrivePublicLinkClient.js +2 -2
  91. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  92. package/dist/tests/telemetry.d.ts +4 -2
  93. package/dist/tests/telemetry.js +3 -1
  94. package/dist/tests/telemetry.js.map +1 -1
  95. package/dist/transformers.d.ts +3 -2
  96. package/dist/transformers.js +6 -0
  97. package/dist/transformers.js.map +1 -1
  98. package/package.json +1 -1
  99. package/src/diagnostic/{sdkDiagnosticFull.ts → diagnostic.ts} +10 -6
  100. package/src/diagnostic/index.ts +3 -5
  101. package/src/diagnostic/interface.ts +39 -0
  102. package/src/diagnostic/sdkDiagnostic.ts +110 -9
  103. package/src/interface/index.ts +1 -0
  104. package/src/interface/nodes.ts +3 -0
  105. package/src/interface/telemetry.ts +5 -0
  106. package/src/internal/apiService/driveTypes.ts +2698 -2529
  107. package/src/internal/download/controller.ts +13 -1
  108. package/src/internal/download/fileDownloader.ts +8 -1
  109. package/src/internal/nodes/apiService.test.ts +64 -0
  110. package/src/internal/nodes/apiService.ts +38 -17
  111. package/src/internal/nodes/debouncer.test.ts +25 -13
  112. package/src/internal/nodes/debouncer.ts +20 -4
  113. package/src/internal/nodes/extendedAttributes.ts +2 -2
  114. package/src/internal/nodes/index.ts +1 -8
  115. package/src/internal/nodes/nodesAccess.test.ts +17 -5
  116. package/src/internal/nodes/nodesAccess.ts +15 -5
  117. package/src/internal/nodes/nodesManagement.test.ts +3 -1
  118. package/src/internal/nodes/nodesManagement.ts +11 -5
  119. package/src/internal/photos/apiService.ts +12 -29
  120. package/src/internal/photos/upload.ts +19 -1
  121. package/src/internal/sharing/apiService.ts +2 -2
  122. package/src/internal/sharing/sharingManagement.ts +7 -4
  123. package/src/internal/sharingPublic/apiService.ts +23 -77
  124. package/src/internal/sharingPublic/index.ts +11 -10
  125. package/src/internal/sharingPublic/nodes.ts +33 -11
  126. package/src/internal/sharingPublic/session/apiService.ts +31 -9
  127. package/src/internal/sharingPublic/session/interface.ts +20 -0
  128. package/src/internal/sharingPublic/session/manager.ts +31 -8
  129. package/src/internal/sharingPublic/session/session.ts +10 -5
  130. package/src/internal/sharingPublic/shares.ts +7 -43
  131. package/src/internal/upload/controller.ts +16 -4
  132. package/src/internal/upload/fileUploader.ts +2 -2
  133. package/src/internal/upload/streamUploader.test.ts +46 -14
  134. package/src/internal/upload/streamUploader.ts +74 -21
  135. package/src/protonDriveClient.ts +25 -7
  136. package/src/protonDrivePublicLinkClient.ts +7 -4
  137. package/src/tests/telemetry.ts +6 -3
  138. package/src/transformers.ts +8 -0
  139. package/dist/diagnostic/sdkDiagnosticFull.js.map +0 -1
  140. package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -15
  141. package/dist/internal/sharingPublic/cryptoCache.js +0 -44
  142. package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
  143. package/dist/internal/sharingPublic/cryptoService.d.ts +0 -8
  144. package/dist/internal/sharingPublic/cryptoService.js +0 -19
  145. package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
  146. package/dist/internal/sharingPublic/interface.d.ts +0 -5
  147. package/dist/internal/sharingPublic/interface.js +0 -3
  148. package/dist/internal/sharingPublic/interface.js.map +0 -1
  149. package/src/internal/sharingPublic/cryptoCache.ts +0 -45
  150. package/src/internal/sharingPublic/cryptoService.ts +0 -22
  151. package/src/internal/sharingPublic/interface.ts +0 -5
@@ -1,11 +1,23 @@
1
+ import { AbortError } from '../../errors';
1
2
  import { waitForCondition } from '../wait';
2
3
 
3
4
  export class DownloadController {
4
5
  private paused = false;
5
6
  public promise?: Promise<void>;
6
7
 
8
+ constructor(private signal?: AbortSignal) {
9
+ this.signal = signal;
10
+ }
11
+
7
12
  async waitWhilePaused(): Promise<void> {
8
- await waitForCondition(() => !this.paused);
13
+ try {
14
+ await waitForCondition(() => !this.paused, this.signal);
15
+ } catch (error) {
16
+ if (error instanceof AbortError) {
17
+ return;
18
+ }
19
+ throw error;
20
+ }
9
21
  }
10
22
 
11
23
  pause(): void {
@@ -1,4 +1,7 @@
1
+ import { c } from 'ttag';
2
+
1
3
  import { PrivateKey, SessionKey, base64StringToUint8Array } from '../../crypto';
4
+ import { AbortError } from '../../errors';
2
5
  import { Logger } from '../../interface';
3
6
  import { LoggerWithPrefix } from '../../telemetry';
4
7
  import { APIHTTPError, HTTPErrorCode } from '../apiService';
@@ -48,7 +51,7 @@ export class FileDownloader {
48
51
  this.revision = revision;
49
52
  this.signal = signal;
50
53
  this.onFinish = onFinish;
51
- this.controller = new DownloadController();
54
+ this.controller = new DownloadController(this.signal);
52
55
  }
53
56
 
54
57
  getClaimedSizeInBytes(): number | undefined {
@@ -289,6 +292,10 @@ export class FileDownloader {
289
292
  logger.debug(`Decrypting`);
290
293
  decryptedBlock = await this.cryptoService.decryptBlock(encryptedBlock, cryptoKeys);
291
294
  } catch (error) {
295
+ if (this.signal?.aborted) {
296
+ throw new AbortError(c('Error').t`Operation aborted`);
297
+ }
298
+
292
299
  if (blockProgress !== 0) {
293
300
  onProgress?.(-blockProgress);
294
301
  blockProgress = 0;
@@ -1,3 +1,4 @@
1
+ import { NodeWithSameNameExistsValidationError, ValidationError } from '../../errors';
1
2
  import { MemberRole, NodeType } from '../../interface';
2
3
  import { getMockLogger } from '../../tests/logger';
3
4
  import { DriveAPIService, ErrorCode, InvalidRequirementsAPIError } from '../apiService';
@@ -627,6 +628,69 @@ describe('nodeAPIService', () => {
627
628
  });
628
629
  });
629
630
 
631
+ describe('createFolder', () => {
632
+ it('should create folder', async () => {
633
+ apiMock.post = jest.fn().mockResolvedValue({
634
+ Code: ErrorCode.OK,
635
+ Folder: {
636
+ ID: 'newNodeId',
637
+ },
638
+ });
639
+
640
+ const result = await api.createFolder('volumeId~parentNodeId', {
641
+ armoredKey: 'armoredKey',
642
+ armoredHashKey: 'armoredHashKey',
643
+ armoredNodePassphrase: 'armoredNodePassphrase',
644
+ armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
645
+ signatureEmail: 'signatureEmail',
646
+ encryptedName: 'encryptedName',
647
+ hash: 'hash',
648
+ armoredExtendedAttributes: 'armoredExtendedAttributes',
649
+ });
650
+
651
+ expect(result).toEqual('volumeId~newNodeId');
652
+ expect(apiMock.post).toHaveBeenCalledWith('drive/v2/volumes/volumeId/folders', {
653
+ ParentLinkID: 'parentNodeId',
654
+ NodeKey: 'armoredKey',
655
+ NodeHashKey: 'armoredHashKey',
656
+ NodePassphrase: 'armoredNodePassphrase',
657
+ NodePassphraseSignature: 'armoredNodePassphraseSignature',
658
+ SignatureEmail: 'signatureEmail',
659
+ Name: 'encryptedName',
660
+ Hash: 'hash',
661
+ XAttr: 'armoredExtendedAttributes',
662
+ });
663
+ });
664
+
665
+ it('should throw NodeWithSameNameExistsValidationError if node already exists', async () => {
666
+ apiMock.post = jest.fn().mockRejectedValue(
667
+ new ValidationError('Node already exists', ErrorCode.ALREADY_EXISTS, {
668
+ ConflictLinkID: 'existingNodeId',
669
+ }),
670
+ );
671
+
672
+ try {
673
+ await api.createFolder('volumeId~parentNodeId', {
674
+ armoredKey: 'armoredKey',
675
+ armoredHashKey: 'armoredHashKey',
676
+ armoredNodePassphrase: 'armoredNodePassphrase',
677
+ armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
678
+ signatureEmail: 'signatureEmail',
679
+ encryptedName: 'encryptedName',
680
+ hash: 'hash',
681
+ armoredExtendedAttributes: 'armoredExtendedAttributes',
682
+ });
683
+ expect(false).toBeTruthy();
684
+ } catch (error: unknown) {
685
+ expect(error).toBeInstanceOf(NodeWithSameNameExistsValidationError);
686
+ if (error instanceof NodeWithSameNameExistsValidationError) {
687
+ expect(error.code).toEqual(ErrorCode.ALREADY_EXISTS);
688
+ expect(error.existingNodeUid).toEqual('volumeId~existingNodeId');
689
+ }
690
+ }
691
+ });
692
+ });
693
+
630
694
  describe('renameNode', () => {
631
695
  it('should rename node', async () => {
632
696
  await api.renameNode(
@@ -1,11 +1,12 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { ProtonDriveError, ValidationError } from '../../errors';
3
+ import { NodeWithSameNameExistsValidationError, ProtonDriveError, ValidationError } from '../../errors';
4
4
  import { Logger, NodeResult } from '../../interface';
5
5
  import { MemberRole, RevisionState } from '../../interface/nodes';
6
6
  import {
7
7
  DriveAPIService,
8
8
  drivePaths,
9
+ ErrorCode,
9
10
  InvalidRequirementsAPIError,
10
11
  isCodeOk,
11
12
  nodeTypeNumberToNodeType,
@@ -400,7 +401,7 @@ export class NodeAPIService {
400
401
  );
401
402
 
402
403
  // TODO: remove `as` when backend fixes OpenAPI schema.
403
- yield * handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
404
+ yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
404
405
  }
405
406
  }
406
407
 
@@ -449,21 +450,41 @@ export class NodeAPIService {
449
450
  ): Promise<string> {
450
451
  const { volumeId, nodeId: parentId } = splitNodeUid(parentUid);
451
452
 
452
- const response = await this.apiService.post<PostCreateFolderRequest, PostCreateFolderResponse>(
453
- `drive/v2/volumes/${volumeId}/folders`,
454
- {
455
- ParentLinkID: parentId,
456
- NodeKey: newNode.armoredKey,
457
- NodeHashKey: newNode.armoredHashKey,
458
- NodePassphrase: newNode.armoredNodePassphrase,
459
- NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
460
- SignatureEmail: newNode.signatureEmail,
461
- Name: newNode.encryptedName,
462
- Hash: newNode.hash,
463
- // @ts-expect-error: XAttr is optional as undefined.
464
- XAttr: newNode.armoredExtendedAttributes,
465
- },
466
- );
453
+ let response: PostCreateFolderResponse;
454
+ try {
455
+ response = await this.apiService.post<PostCreateFolderRequest, PostCreateFolderResponse>(
456
+ `drive/v2/volumes/${volumeId}/folders`,
457
+ {
458
+ ParentLinkID: parentId,
459
+ NodeKey: newNode.armoredKey,
460
+ NodeHashKey: newNode.armoredHashKey,
461
+ NodePassphrase: newNode.armoredNodePassphrase,
462
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
463
+ SignatureEmail: newNode.signatureEmail,
464
+ Name: newNode.encryptedName,
465
+ Hash: newNode.hash,
466
+ // @ts-expect-error: XAttr is optional as undefined.
467
+ XAttr: newNode.armoredExtendedAttributes,
468
+ },
469
+ );
470
+ } catch (error: unknown) {
471
+ if (error instanceof ValidationError) {
472
+ if (error.code === ErrorCode.ALREADY_EXISTS) {
473
+ const typedDetails = error.details as
474
+ | {
475
+ ConflictLinkID: string;
476
+ }
477
+ | undefined;
478
+
479
+ const existingNodeUid = typedDetails?.ConflictLinkID
480
+ ? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
481
+ : undefined;
482
+
483
+ throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
484
+ }
485
+ }
486
+ throw error;
487
+ }
467
488
 
468
489
  return makeNodeUid(volumeId, response.Folder.ID);
469
490
  }
@@ -1,18 +1,14 @@
1
+ import { ProtonDriveTelemetry } from '../../interface';
2
+ import { getMockTelemetry } from '../../tests/telemetry';
1
3
  import { NodesDebouncer } from './debouncer';
2
- import { Logger } from '../../interface';
3
4
 
4
5
  describe('NodesDebouncer', () => {
5
6
  let debouncer: NodesDebouncer;
6
- let mockLogger: jest.Mocked<Logger>;
7
+ let mockTelemetry: ReturnType<typeof getMockTelemetry>;
7
8
 
8
9
  beforeEach(() => {
9
- mockLogger = {
10
- debug: jest.fn(),
11
- info: jest.fn(),
12
- warn: jest.fn(),
13
- error: jest.fn(),
14
- };
15
- debouncer = new NodesDebouncer(mockLogger);
10
+ mockTelemetry = getMockTelemetry();
11
+ debouncer = new NodesDebouncer(mockTelemetry);
16
12
 
17
13
  jest.useFakeTimers();
18
14
  });
@@ -77,7 +73,22 @@ describe('NodesDebouncer', () => {
77
73
  debouncer.loadingNode(nodeUid);
78
74
  debouncer.loadingNode(nodeUid);
79
75
 
80
- expect(mockLogger.warn).toHaveBeenCalledWith(`debouncer: Loading twice for: ${nodeUid}`);
76
+ expect(mockTelemetry.mockLogger.warn).toHaveBeenCalledWith(`Loading twice for: ${nodeUid}`);
77
+ });
78
+
79
+ it('should send metric when waiting for a long time', async () => {
80
+ const nodeUid = 'test-node-1';
81
+ debouncer.loadingNode(nodeUid);
82
+
83
+ const waitPromise = debouncer.waitForLoadingNode(nodeUid);
84
+ expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
85
+ jest.advanceTimersByTime(1500);
86
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
87
+ eventName: 'debounceLongWait',
88
+ });
89
+
90
+ debouncer.finishedLoadingNode(nodeUid);
91
+ await waitPromise;
81
92
  });
82
93
 
83
94
  it('should timeout', async () => {
@@ -85,7 +96,7 @@ describe('NodesDebouncer', () => {
85
96
  debouncer.loadingNode(nodeUid);
86
97
 
87
98
  jest.advanceTimersByTime(6000);
88
- expect(mockLogger.warn).toHaveBeenCalledWith(`debouncer: Timeout for: ${nodeUid}`);
99
+ expect(mockTelemetry.mockLogger.warn).toHaveBeenCalledWith(`Timeout for: ${nodeUid}`);
89
100
  await expect(debouncer.waitForLoadingNode(nodeUid)).resolves.toBeUndefined();
90
101
  });
91
102
 
@@ -112,7 +123,7 @@ describe('NodesDebouncer', () => {
112
123
 
113
124
  const result = await debouncer.waitForLoadingNode(nodeUid);
114
125
  expect(result).toBeUndefined();
115
- expect(mockLogger.debug).not.toHaveBeenCalled();
126
+ expect(mockTelemetry.mockLogger.debug).not.toHaveBeenCalled();
116
127
  });
117
128
 
118
129
  it('should wait for registered node and log debug message', async () => {
@@ -121,7 +132,8 @@ describe('NodesDebouncer', () => {
121
132
 
122
133
  const waitPromise = debouncer.waitForLoadingNode(nodeUid);
123
134
 
124
- expect(mockLogger.debug).toHaveBeenCalledWith(`debouncer: Wait for: ${nodeUid}`);
135
+ expect(mockTelemetry.mockLogger.debug).toHaveBeenCalledWith(`Wait for: ${nodeUid}`);
136
+
125
137
  debouncer.finishedLoadingNode(nodeUid);
126
138
  await waitPromise;
127
139
  });
@@ -1,5 +1,4 @@
1
- import { Logger } from "../../interface";
2
- import { LoggerWithPrefix } from '../../telemetry';
1
+ import { Logger, ProtonDriveTelemetry } from '../../interface';
3
2
 
4
3
  /**
5
4
  * The timeout for which the node is considered to be loading.
@@ -11,6 +10,12 @@ import { LoggerWithPrefix } from '../../telemetry';
11
10
  */
12
11
  const DEBOUNCE_TIMEOUT = 5000;
13
12
 
13
+ /**
14
+ * The timeout for which the node is considered to be waiting for a long time.
15
+ * After this timeout the metric is sent.
16
+ */
17
+ const DEBOUNCE_LONG_WAIT_TIMEOUT = 1000;
18
+
14
19
  /**
15
20
  * Helper to avoid loading the same node twice.
16
21
  *
@@ -23,6 +28,8 @@ const DEBOUNCE_TIMEOUT = 5000;
23
28
  * the node to be loaded if that is the case.
24
29
  */
25
30
  export class NodesDebouncer {
31
+ private logger: Logger;
32
+
26
33
  private promises: Map<
27
34
  string,
28
35
  {
@@ -32,8 +39,9 @@ export class NodesDebouncer {
32
39
  }
33
40
  > = new Map();
34
41
 
35
- constructor(private logger: Logger) {
36
- this.logger = new LoggerWithPrefix(logger, 'debouncer');
42
+ constructor(private telemetry: ProtonDriveTelemetry) {
43
+ this.logger = telemetry.getLogger('nodes-debouncer');
44
+ this.telemetry = telemetry;
37
45
  }
38
46
 
39
47
  loadingNodes(nodeUids: string[]) {
@@ -79,8 +87,16 @@ export class NodesDebouncer {
79
87
  return;
80
88
  }
81
89
 
90
+ const metricTimeout = setTimeout(() => {
91
+ this.telemetry.recordMetric({
92
+ eventName: 'debounceLongWait',
93
+ });
94
+ }, DEBOUNCE_LONG_WAIT_TIMEOUT);
95
+
82
96
  this.logger.debug(`Wait for: ${nodeUid}`);
83
97
  await result.promise;
98
+
99
+ clearTimeout(metricTimeout);
84
100
  }
85
101
 
86
102
  clear() {
@@ -208,11 +208,11 @@ function parseBlockSizes(
208
208
  return undefined;
209
209
  }
210
210
  if (!Array.isArray(blockSizes)) {
211
- logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
211
+ logger.warn(`XAttr block sizes "${JSON.stringify(blockSizes)}" is not valid`);
212
212
  return undefined;
213
213
  }
214
214
  if (blockSizes.some((size) => typeof size !== 'number' || size <= 0)) {
215
- logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
215
+ logger.warn(`XAttr block sizes "${JSON.stringify(blockSizes)}" is not valid`);
216
216
  return undefined;
217
217
  }
218
218
  if (blockSizes.length === 0) {
@@ -43,14 +43,7 @@ export function initNodesModule(
43
43
  const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
44
44
  const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
45
45
  const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
46
- const nodesAccess = new NodesAccess(
47
- telemetry.getLogger('nodes'),
48
- api,
49
- cache,
50
- cryptoCache,
51
- cryptoService,
52
- sharesService,
53
- );
46
+ const nodesAccess = new NodesAccess(telemetry, api, cache, cryptoCache, cryptoService, sharesService);
54
47
  const nodesEventHandler = new NodesEventsHandler(telemetry.getLogger('nodes-events'), cache);
55
48
  const nodesManagement = new NodesManagement(api, cryptoCache, cryptoService, nodesAccess);
56
49
  const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
@@ -1,4 +1,4 @@
1
- import { getMockLogger } from '../../tests/logger';
1
+ import { getMockTelemetry } from '../../tests/telemetry';
2
2
  import { PrivateKey } from '../../crypto';
3
3
  import { DecryptionError } from '../../errors';
4
4
  import { NodeAPIService } from './apiService';
@@ -50,7 +50,7 @@ describe('nodesAccess', () => {
50
50
  getSharePrivateKey: jest.fn(),
51
51
  };
52
52
 
53
- access = new NodesAccess(getMockLogger(), apiService, cache, cryptoCache, cryptoService, shareService);
53
+ access = new NodesAccess(getMockTelemetry(), apiService, cache, cryptoCache, cryptoService, shareService);
54
54
  });
55
55
 
56
56
  describe('getNode', () => {
@@ -569,7 +569,11 @@ describe('nodesAccess', () => {
569
569
  it('should get share parent keys', async () => {
570
570
  shareService.getSharePrivateKey = jest.fn(() => Promise.resolve('shareKey' as any as PrivateKey));
571
571
 
572
- const result = await access.getParentKeys({ shareId: 'shareId', parentUid: undefined });
572
+ const result = await access.getParentKeys({
573
+ uid: 'volumeId~nodeId',
574
+ shareId: 'shareId',
575
+ parentUid: undefined,
576
+ });
573
577
  expect(result).toEqual({ key: 'shareKey' });
574
578
  expect(cryptoCache.getNodeKeys).not.toHaveBeenCalled();
575
579
  });
@@ -577,7 +581,11 @@ describe('nodesAccess', () => {
577
581
  it('should get node parent keys', async () => {
578
582
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
579
583
 
580
- const result = await access.getParentKeys({ shareId: undefined, parentUid: 'volumeId~parentNodeid' });
584
+ const result = await access.getParentKeys({
585
+ uid: 'volumeId~nodeId',
586
+ shareId: undefined,
587
+ parentUid: 'volumeId~parentNodeid',
588
+ });
581
589
  expect(result).toEqual({ key: 'parentKey' });
582
590
  expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
583
591
  });
@@ -585,7 +593,11 @@ describe('nodesAccess', () => {
585
593
  it('should get node parent keys even if share is set', async () => {
586
594
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
587
595
 
588
- const result = await access.getParentKeys({ shareId: 'shareId', parentUid: 'volume1~parentNodeid' });
596
+ const result = await access.getParentKeys({
597
+ uid: 'volume1~nodeId',
598
+ shareId: 'shareId',
599
+ parentUid: 'volume1~parentNodeid',
600
+ });
589
601
  expect(result).toEqual({ key: 'parentKey' });
590
602
  expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
591
603
  });
@@ -1,7 +1,16 @@
1
1
  import { c } from 'ttag';
2
2
 
3
3
  import { PrivateKey, SessionKey } from '../../crypto';
4
- import { InvalidNameError, Logger, MissingNode, NodeType, Result, resultError, resultOk } from '../../interface';
4
+ import {
5
+ InvalidNameError,
6
+ Logger,
7
+ MissingNode,
8
+ NodeType,
9
+ ProtonDriveTelemetry,
10
+ Result,
11
+ resultError,
12
+ resultOk,
13
+ } from '../../interface';
5
14
  import { DecryptionError, ProtonDriveError } from '../../errors';
6
15
  import { asyncIteratorMap } from '../asyncIteratorMap';
7
16
  import { getErrorMessage } from '../errors';
@@ -41,10 +50,11 @@ const DECRYPTION_CONCURRENCY = 30;
41
50
  * nodes metadata.
42
51
  */
43
52
  export class NodesAccess {
53
+ private logger: Logger;
44
54
  private debouncer: NodesDebouncer;
45
55
 
46
56
  constructor(
47
- private logger: Logger,
57
+ private telemetry: ProtonDriveTelemetry,
48
58
  private apiService: NodeAPIService,
49
59
  private cache: NodesCache,
50
60
  private cryptoCache: NodesCryptoCache,
@@ -54,13 +64,13 @@ export class NodesAccess {
54
64
  'getOwnVolumeIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
55
65
  >,
56
66
  ) {
57
- this.logger = logger;
67
+ this.logger = telemetry.getLogger('nodes');
58
68
  this.apiService = apiService;
59
69
  this.cache = cache;
60
70
  this.cryptoCache = cryptoCache;
61
71
  this.cryptoService = cryptoService;
62
72
  this.shareService = shareService;
63
- this.debouncer = new NodesDebouncer(this.logger);
73
+ this.debouncer = new NodesDebouncer(this.telemetry);
64
74
  }
65
75
 
66
76
  async getVolumeRootFolder() {
@@ -357,7 +367,7 @@ export class NodesAccess {
357
367
  }
358
368
 
359
369
  async getParentKeys(
360
- node: Pick<DecryptedNode, 'parentUid' | 'shareId'>,
370
+ node: Pick<DecryptedNode, 'uid' | 'parentUid' | 'shareId'>,
361
371
  ): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
362
372
  if (node.parentUid) {
363
373
  try {
@@ -50,7 +50,7 @@ describe('NodesManagement', () => {
50
50
  apiService = {
51
51
  renameNode: jest.fn(),
52
52
  moveNode: jest.fn(),
53
- copyNode: jest.fn(),
53
+ copyNode: jest.fn().mockResolvedValue('newCopiedNodeUid'),
54
54
  trashNodes: jest.fn(async function* (uids) {
55
55
  yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
56
56
  }),
@@ -251,6 +251,7 @@ describe('NodesManagement', () => {
251
251
 
252
252
  expect(newNode).toEqual({
253
253
  ...nodes.nodeUid,
254
+ uid: 'newCopiedNodeUid',
254
255
  parentUid: 'newParentNodeUid',
255
256
  encryptedName: 'copiedArmoredNodeName',
256
257
  hash: 'copiedHash',
@@ -307,6 +308,7 @@ describe('NodesManagement', () => {
307
308
  );
308
309
  expect(newNode).toEqual({
309
310
  ...nodes.anonymousNodeUid,
311
+ uid: 'newCopiedNodeUid',
310
312
  parentUid: 'newParentNodeUid',
311
313
  encryptedName: 'copiedArmoredNodeName',
312
314
  hash: 'copiedHash',
@@ -1,6 +1,6 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { MemberRole, NodeType, NodeResult, resultOk } from '../../interface';
3
+ import { MemberRole, NodeType, NodeResult, NodeResultWithNewUid, resultOk } from '../../interface';
4
4
  import { AbortError, ValidationError } from '../../errors';
5
5
  import { getErrorMessage } from '../errors';
6
6
  import { splitNodeUid } from '../uids';
@@ -182,16 +182,21 @@ export class NodesManagement {
182
182
  return newNode;
183
183
  }
184
184
 
185
- // Improvement requested: copy nodes in parallel
186
- async *copyNodes(nodeUids: string[], newParentNodeUid: string, signal?: AbortSignal): AsyncGenerator<NodeResult> {
185
+ // Improvement requested: copy nodes in parallel using copy_multiple endpoint
186
+ async *copyNodes(
187
+ nodeUids: string[],
188
+ newParentNodeUid: string,
189
+ signal?: AbortSignal,
190
+ ): AsyncGenerator<NodeResultWithNewUid> {
187
191
  for (const nodeUid of nodeUids) {
188
192
  if (signal?.aborted) {
189
193
  throw new AbortError(c('Error').t`Copy operation aborted`);
190
194
  }
191
195
  try {
192
- await this.copyNode(nodeUid, newParentNodeUid);
196
+ const { uid: newNodeUid } = await this.copyNode(nodeUid, newParentNodeUid);
193
197
  yield {
194
198
  uid: nodeUid,
199
+ newUid: newNodeUid,
195
200
  ok: true,
196
201
  };
197
202
  } catch (error: unknown) {
@@ -237,7 +242,7 @@ export class NodesManagement {
237
242
  signatureEmail: encryptedCrypto.signatureEmail,
238
243
  armoredNodePassphraseSignature: encryptedCrypto.armoredNodePassphraseSignature,
239
244
  };
240
- await this.apiService.copyNode(nodeUid, {
245
+ const newNodeUid = await this.apiService.copyNode(nodeUid, {
241
246
  ...keySignatureProperties,
242
247
  parentUid: newParentUid,
243
248
  armoredNodePassphrase: encryptedCrypto.armoredNodePassphrase,
@@ -247,6 +252,7 @@ export class NodesManagement {
247
252
  });
248
253
  const newNode: DecryptedNode = {
249
254
  ...node,
255
+ uid: newNodeUid,
250
256
  encryptedName: encryptedCrypto.encryptedName,
251
257
  parentUid: newParentUid,
252
258
  hash: encryptedCrypto.hash,
@@ -1,12 +1,9 @@
1
- import { c } from 'ttag';
2
-
3
- import { DriveAPIService, drivePaths, NotFoundAPIError } from '../apiService';
1
+ import { DriveAPIService, drivePaths } from '../apiService';
4
2
  import { EncryptedRootShare, EncryptedShareCrypto, ShareType } from '../shares/interface';
5
3
  import { makeNodeUid } from '../uids';
6
4
 
7
- type GetVolumesResponse = drivePaths['/drive/volumes']['get']['responses']['200']['content']['application/json'];
8
-
9
- type GetShareResponse = drivePaths['/drive/shares/{shareID}']['get']['responses']['200']['content']['application/json'];
5
+ type GetPhotoShareResponse =
6
+ drivePaths['/drive/v2/shares/photos']['get']['responses']['200']['content']['application/json'];
10
7
 
11
8
  type PostCreateVolumeRequest = Extract<
12
9
  drivePaths['/drive/photos/volumes']['post']['requestBody'],
@@ -34,33 +31,19 @@ export class PhotosAPIService {
34
31
  }
35
32
 
36
33
  async getPhotoShare(): Promise<EncryptedRootShare> {
37
- // TODO: Switch to drive/v2/shares/photos once available.
38
-
39
- const volumesResponse = await this.apiService.get<GetVolumesResponse>('drive/volumes');
40
-
41
- const photoVolume = volumesResponse.Volumes.find((volume) => volume.Type === 2);
42
-
43
- if (!photoVolume) {
44
- throw new NotFoundAPIError(c('Error').t`Photo volume not found`);
45
- }
46
-
47
- const response = await this.apiService.get<GetShareResponse>(`drive/shares/${photoVolume.Share.ShareID}`);
48
-
49
- if (!response.AddressID) {
50
- throw new Error('Photo root share has not address ID set');
51
- }
34
+ const response = await this.apiService.get<GetPhotoShareResponse>('drive/v2/shares/photos');
52
35
 
53
36
  return {
54
- volumeId: response.VolumeID,
55
- shareId: response.ShareID,
56
- rootNodeId: response.LinkID,
57
- creatorEmail: response.Creator,
37
+ volumeId: response.Volume.VolumeID,
38
+ shareId: response.Share.ShareID,
39
+ rootNodeId: response.Link.Link.LinkID,
40
+ creatorEmail: response.Share.CreatorEmail,
58
41
  encryptedCrypto: {
59
- armoredKey: response.Key,
60
- armoredPassphrase: response.Passphrase,
61
- armoredPassphraseSignature: response.PassphraseSignature,
42
+ armoredKey: response.Share.Key,
43
+ armoredPassphrase: response.Share.Passphrase,
44
+ armoredPassphraseSignature: response.Share.PassphraseSignature,
62
45
  },
63
- addressId: response.AddressID,
46
+ addressId: response.Share.AddressID,
64
47
  type: ShareType.Photo,
65
48
  };
66
49
  }
@@ -85,7 +85,25 @@ export class PhotoStreamUploader extends StreamUploader {
85
85
  controller: UploadController,
86
86
  signal?: AbortSignal,
87
87
  ) {
88
- super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, controller, signal);
88
+ const abortController = new AbortController();
89
+ if (signal) {
90
+ signal.addEventListener('abort', () => {
91
+ abortController.abort();
92
+ });
93
+ }
94
+
95
+ super(
96
+ telemetry,
97
+ apiService,
98
+ cryptoService,
99
+ uploadManager,
100
+ blockVerifier,
101
+ revisionDraft,
102
+ metadata,
103
+ onFinish,
104
+ controller,
105
+ abortController,
106
+ );
89
107
  this.photoUploadManager = uploadManager;
90
108
  this.photoMetadata = metadata;
91
109
  }
@@ -347,8 +347,8 @@ export class SharingAPIService {
347
347
  return response.Share.ID;
348
348
  }
349
349
 
350
- async deleteShare(shareId: string): Promise<void> {
351
- await this.apiService.delete(`drive/shares/${shareId}?Force=1`);
350
+ async deleteShare(shareId: string, force: boolean = false): Promise<void> {
351
+ await this.apiService.delete(`drive/shares/${shareId}?Force=${force ? 1 : 0}`);
352
352
  }
353
353
 
354
354
  async inviteProtonUser(