@protontech/drive-sdk 0.5.0 → 0.6.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.
Files changed (206) 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 +2 -2
  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/interface/upload.d.ts +1 -12
  16. package/dist/internal/apiService/driveTypes.d.ts +2571 -2356
  17. package/dist/internal/download/controller.d.ts +2 -0
  18. package/dist/internal/download/controller.js +15 -1
  19. package/dist/internal/download/controller.js.map +1 -1
  20. package/dist/internal/download/fileDownloader.js +6 -1
  21. package/dist/internal/download/fileDownloader.js.map +1 -1
  22. package/dist/internal/nodes/apiService.d.ts +11 -1
  23. package/dist/internal/nodes/apiService.js +47 -13
  24. package/dist/internal/nodes/apiService.js.map +1 -1
  25. package/dist/internal/nodes/apiService.test.js +61 -3
  26. package/dist/internal/nodes/apiService.test.js.map +1 -1
  27. package/dist/internal/nodes/cryptoService.d.ts +4 -0
  28. package/dist/internal/nodes/cryptoService.js +6 -0
  29. package/dist/internal/nodes/cryptoService.js.map +1 -1
  30. package/dist/internal/nodes/debouncer.d.ts +3 -2
  31. package/dist/internal/nodes/debouncer.js +16 -4
  32. package/dist/internal/nodes/debouncer.js.map +1 -1
  33. package/dist/internal/nodes/debouncer.test.js +20 -12
  34. package/dist/internal/nodes/debouncer.test.js.map +1 -1
  35. package/dist/internal/nodes/extendedAttributes.js +2 -2
  36. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  37. package/dist/internal/nodes/index.d.ts +1 -1
  38. package/dist/internal/nodes/index.js +3 -3
  39. package/dist/internal/nodes/index.js.map +1 -1
  40. package/dist/internal/nodes/index.test.js +1 -1
  41. package/dist/internal/nodes/index.test.js.map +1 -1
  42. package/dist/internal/nodes/nodeName.d.ts +8 -0
  43. package/dist/internal/nodes/nodeName.js +30 -0
  44. package/dist/internal/nodes/nodeName.js.map +1 -0
  45. package/dist/internal/nodes/nodeName.test.d.ts +1 -0
  46. package/dist/internal/nodes/nodeName.test.js +50 -0
  47. package/dist/internal/nodes/nodeName.test.js.map +1 -0
  48. package/dist/internal/nodes/nodesAccess.d.ts +5 -4
  49. package/dist/internal/nodes/nodesAccess.js +6 -5
  50. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  51. package/dist/internal/nodes/nodesAccess.test.js +17 -5
  52. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  53. package/dist/internal/nodes/nodesManagement.d.ts +3 -2
  54. package/dist/internal/nodes/nodesManagement.js +35 -4
  55. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  56. package/dist/internal/nodes/nodesManagement.test.js +64 -1
  57. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  58. package/dist/internal/photos/apiService.js +9 -20
  59. package/dist/internal/photos/apiService.js.map +1 -1
  60. package/dist/internal/photos/index.d.ts +2 -0
  61. package/dist/internal/photos/index.js +4 -0
  62. package/dist/internal/photos/index.js.map +1 -1
  63. package/dist/internal/photos/upload.js +7 -1
  64. package/dist/internal/photos/upload.js.map +1 -1
  65. package/dist/internal/shares/index.d.ts +1 -0
  66. package/dist/internal/shares/index.js +3 -0
  67. package/dist/internal/shares/index.js.map +1 -1
  68. package/dist/internal/shares/interface.d.ts +8 -0
  69. package/dist/internal/shares/interface.js +10 -1
  70. package/dist/internal/shares/interface.js.map +1 -1
  71. package/dist/internal/sharing/apiService.d.ts +4 -2
  72. package/dist/internal/sharing/apiService.js +18 -14
  73. package/dist/internal/sharing/apiService.js.map +1 -1
  74. package/dist/internal/sharing/index.d.ts +2 -1
  75. package/dist/internal/sharing/index.js +6 -2
  76. package/dist/internal/sharing/index.js.map +1 -1
  77. package/dist/internal/sharing/sharingManagement.d.ts +4 -1
  78. package/dist/internal/sharing/sharingManagement.js +7 -4
  79. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  80. package/dist/internal/sharingPublic/apiService.d.ts +8 -10
  81. package/dist/internal/sharingPublic/apiService.js +9 -63
  82. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  83. package/dist/internal/sharingPublic/index.d.ts +3 -3
  84. package/dist/internal/sharingPublic/index.js +7 -12
  85. package/dist/internal/sharingPublic/index.js.map +1 -1
  86. package/dist/internal/sharingPublic/nodes.d.ts +13 -8
  87. package/dist/internal/sharingPublic/nodes.js +20 -2
  88. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  89. package/dist/internal/sharingPublic/session/apiService.d.ts +7 -5
  90. package/dist/internal/sharingPublic/session/apiService.js +25 -4
  91. package/dist/internal/sharingPublic/session/apiService.js.map +1 -1
  92. package/dist/internal/sharingPublic/session/interface.d.ts +17 -0
  93. package/dist/internal/sharingPublic/session/manager.d.ts +12 -4
  94. package/dist/internal/sharingPublic/session/manager.js +14 -4
  95. package/dist/internal/sharingPublic/session/manager.js.map +1 -1
  96. package/dist/internal/sharingPublic/session/session.d.ts +5 -2
  97. package/dist/internal/sharingPublic/session/session.js +7 -3
  98. package/dist/internal/sharingPublic/session/session.js.map +1 -1
  99. package/dist/internal/sharingPublic/shares.d.ts +3 -10
  100. package/dist/internal/sharingPublic/shares.js +10 -33
  101. package/dist/internal/sharingPublic/shares.js.map +1 -1
  102. package/dist/internal/upload/apiService.d.ts +0 -9
  103. package/dist/internal/upload/apiService.js +0 -16
  104. package/dist/internal/upload/apiService.js.map +1 -1
  105. package/dist/internal/upload/controller.d.ts +3 -1
  106. package/dist/internal/upload/controller.js +16 -2
  107. package/dist/internal/upload/controller.js.map +1 -1
  108. package/dist/internal/upload/cryptoService.d.ts +0 -4
  109. package/dist/internal/upload/cryptoService.js +0 -6
  110. package/dist/internal/upload/cryptoService.js.map +1 -1
  111. package/dist/internal/upload/fileUploader.d.ts +0 -1
  112. package/dist/internal/upload/fileUploader.js +2 -6
  113. package/dist/internal/upload/fileUploader.js.map +1 -1
  114. package/dist/internal/upload/manager.d.ts +0 -1
  115. package/dist/internal/upload/manager.js +0 -51
  116. package/dist/internal/upload/manager.js.map +1 -1
  117. package/dist/internal/upload/manager.test.js +0 -61
  118. package/dist/internal/upload/manager.test.js.map +1 -1
  119. package/dist/internal/upload/streamUploader.d.ts +4 -3
  120. package/dist/internal/upload/streamUploader.js +61 -18
  121. package/dist/internal/upload/streamUploader.js.map +1 -1
  122. package/dist/internal/upload/streamUploader.test.js +38 -12
  123. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  124. package/dist/protonDriveClient.d.ts +24 -4
  125. package/dist/protonDriveClient.js +26 -7
  126. package/dist/protonDriveClient.js.map +1 -1
  127. package/dist/protonDrivePhotosClient.d.ts +100 -4
  128. package/dist/protonDrivePhotosClient.js +160 -9
  129. package/dist/protonDrivePhotosClient.js.map +1 -1
  130. package/dist/protonDrivePublicLinkClient.d.ts +4 -3
  131. package/dist/protonDrivePublicLinkClient.js +2 -2
  132. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  133. package/dist/tests/telemetry.d.ts +4 -2
  134. package/dist/tests/telemetry.js +3 -1
  135. package/dist/tests/telemetry.js.map +1 -1
  136. package/dist/transformers.d.ts +3 -2
  137. package/dist/transformers.js +6 -0
  138. package/dist/transformers.js.map +1 -1
  139. package/package.json +1 -1
  140. package/src/diagnostic/{sdkDiagnosticFull.ts → diagnostic.ts} +10 -6
  141. package/src/diagnostic/index.ts +3 -5
  142. package/src/diagnostic/interface.ts +39 -0
  143. package/src/diagnostic/sdkDiagnostic.ts +110 -9
  144. package/src/interface/index.ts +2 -1
  145. package/src/interface/nodes.ts +3 -0
  146. package/src/interface/telemetry.ts +5 -0
  147. package/src/interface/upload.ts +1 -13
  148. package/src/internal/apiService/driveTypes.ts +2698 -2529
  149. package/src/internal/download/controller.ts +13 -1
  150. package/src/internal/download/fileDownloader.ts +8 -1
  151. package/src/internal/nodes/apiService.test.ts +65 -1
  152. package/src/internal/nodes/apiService.ts +80 -17
  153. package/src/internal/nodes/cryptoService.ts +9 -0
  154. package/src/internal/nodes/debouncer.test.ts +25 -13
  155. package/src/internal/nodes/debouncer.ts +20 -4
  156. package/src/internal/nodes/extendedAttributes.ts +2 -2
  157. package/src/internal/nodes/index.test.ts +1 -0
  158. package/src/internal/nodes/index.ts +3 -9
  159. package/src/internal/nodes/nodeName.test.ts +57 -0
  160. package/src/internal/nodes/nodeName.ts +26 -0
  161. package/src/internal/nodes/nodesAccess.test.ts +17 -5
  162. package/src/internal/nodes/nodesAccess.ts +15 -5
  163. package/src/internal/nodes/nodesManagement.test.ts +68 -1
  164. package/src/internal/nodes/nodesManagement.ts +54 -6
  165. package/src/internal/photos/apiService.ts +12 -29
  166. package/src/internal/photos/index.ts +4 -0
  167. package/src/internal/photos/upload.ts +19 -1
  168. package/src/internal/shares/index.ts +1 -0
  169. package/src/internal/shares/interface.ts +9 -0
  170. package/src/internal/sharing/apiService.ts +17 -14
  171. package/src/internal/sharing/index.ts +7 -1
  172. package/src/internal/sharing/sharingManagement.ts +7 -4
  173. package/src/internal/sharingPublic/apiService.ts +23 -77
  174. package/src/internal/sharingPublic/index.ts +13 -11
  175. package/src/internal/sharingPublic/nodes.ts +33 -11
  176. package/src/internal/sharingPublic/session/apiService.ts +31 -9
  177. package/src/internal/sharingPublic/session/interface.ts +20 -0
  178. package/src/internal/sharingPublic/session/manager.ts +31 -8
  179. package/src/internal/sharingPublic/session/session.ts +10 -5
  180. package/src/internal/sharingPublic/shares.ts +7 -43
  181. package/src/internal/upload/apiService.ts +0 -39
  182. package/src/internal/upload/controller.ts +16 -4
  183. package/src/internal/upload/cryptoService.ts +0 -9
  184. package/src/internal/upload/fileUploader.ts +2 -7
  185. package/src/internal/upload/manager.test.ts +0 -65
  186. package/src/internal/upload/manager.ts +0 -64
  187. package/src/internal/upload/streamUploader.test.ts +46 -14
  188. package/src/internal/upload/streamUploader.ts +74 -21
  189. package/src/protonDriveClient.ts +46 -9
  190. package/src/protonDrivePhotosClient.ts +193 -8
  191. package/src/protonDrivePublicLinkClient.ts +7 -4
  192. package/src/tests/telemetry.ts +6 -3
  193. package/src/transformers.ts +8 -0
  194. package/dist/diagnostic/sdkDiagnosticFull.js.map +0 -1
  195. package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -15
  196. package/dist/internal/sharingPublic/cryptoCache.js +0 -44
  197. package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
  198. package/dist/internal/sharingPublic/cryptoService.d.ts +0 -8
  199. package/dist/internal/sharingPublic/cryptoService.js +0 -19
  200. package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
  201. package/dist/internal/sharingPublic/interface.d.ts +0 -5
  202. package/dist/internal/sharingPublic/interface.js +0 -3
  203. package/dist/internal/sharingPublic/interface.js.map +0 -1
  204. package/src/internal/sharingPublic/cryptoCache.ts +0 -45
  205. package/src/internal/sharingPublic/cryptoService.ts +0 -22
  206. 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';
@@ -172,7 +173,7 @@ describe('nodeAPIService', () => {
172
173
  put: jest.fn(),
173
174
  };
174
175
 
175
- api = new NodeAPIService(getMockLogger(), apiMock);
176
+ api = new NodeAPIService(getMockLogger(), apiMock, 'clientUid');
176
177
  });
177
178
 
178
179
  describe('getNode', () => {
@@ -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,
@@ -101,6 +102,14 @@ type PostRestoreRevisionResponse =
101
102
  type DeleteRevisionResponse =
102
103
  drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['delete']['responses']['200']['content']['application/json'];
103
104
 
105
+
106
+ type PostCheckAvailableHashesRequest = Extract<
107
+ drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/checkAvailableHashes']['post']['requestBody'],
108
+ { content: object }
109
+ >['content']['application/json'];
110
+ type PostCheckAvailableHashesResponse =
111
+ drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/checkAvailableHashes']['post']['responses']['200']['content']['application/json'];
112
+
104
113
  /**
105
114
  * Provides API communication for fetching and manipulating nodes metadata.
106
115
  *
@@ -111,9 +120,11 @@ export class NodeAPIService {
111
120
  constructor(
112
121
  private logger: Logger,
113
122
  private apiService: DriveAPIService,
123
+ private clientUid: string | undefined,
114
124
  ) {
115
125
  this.logger = logger;
116
126
  this.apiService = apiService;
127
+ this.clientUid = clientUid;
117
128
  }
118
129
 
119
130
  async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
@@ -400,7 +411,7 @@ export class NodeAPIService {
400
411
  );
401
412
 
402
413
  // TODO: remove `as` when backend fixes OpenAPI schema.
403
- yield * handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
414
+ yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
404
415
  }
405
416
  }
406
417
 
@@ -449,21 +460,41 @@ export class NodeAPIService {
449
460
  ): Promise<string> {
450
461
  const { volumeId, nodeId: parentId } = splitNodeUid(parentUid);
451
462
 
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
- );
463
+ let response: PostCreateFolderResponse;
464
+ try {
465
+ response = await this.apiService.post<PostCreateFolderRequest, PostCreateFolderResponse>(
466
+ `drive/v2/volumes/${volumeId}/folders`,
467
+ {
468
+ ParentLinkID: parentId,
469
+ NodeKey: newNode.armoredKey,
470
+ NodeHashKey: newNode.armoredHashKey,
471
+ NodePassphrase: newNode.armoredNodePassphrase,
472
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
473
+ SignatureEmail: newNode.signatureEmail,
474
+ Name: newNode.encryptedName,
475
+ Hash: newNode.hash,
476
+ // @ts-expect-error: XAttr is optional as undefined.
477
+ XAttr: newNode.armoredExtendedAttributes,
478
+ },
479
+ );
480
+ } catch (error: unknown) {
481
+ if (error instanceof ValidationError) {
482
+ if (error.code === ErrorCode.ALREADY_EXISTS) {
483
+ const typedDetails = error.details as
484
+ | {
485
+ ConflictLinkID: string;
486
+ }
487
+ | undefined;
488
+
489
+ const existingNodeUid = typedDetails?.ConflictLinkID
490
+ ? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
491
+ : undefined;
492
+
493
+ throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
494
+ }
495
+ }
496
+ throw error;
497
+ }
467
498
 
468
499
  return makeNodeUid(volumeId, response.Folder.ID);
469
500
  }
@@ -505,6 +536,38 @@ export class NodeAPIService {
505
536
  `drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/${revisionId}`,
506
537
  );
507
538
  }
539
+
540
+ async checkAvailableHashes(
541
+ parentNodeUid: string,
542
+ hashes: string[],
543
+ ): Promise<{
544
+ availableHashes: string[];
545
+ pendingHashes: {
546
+ hash: string;
547
+ nodeUid: string;
548
+ revisionUid: string;
549
+ clientUid?: string;
550
+ }[];
551
+ }> {
552
+ const { volumeId, nodeId: parentNodeId } = splitNodeUid(parentNodeUid);
553
+ const result = await this.apiService.post<PostCheckAvailableHashesRequest, PostCheckAvailableHashesResponse>(
554
+ `drive/v2/volumes/${volumeId}/links/${parentNodeId}/checkAvailableHashes`,
555
+ {
556
+ Hashes: hashes,
557
+ ClientUID: this.clientUid ? [this.clientUid] : null,
558
+ },
559
+ );
560
+
561
+ return {
562
+ availableHashes: result.AvailableHashes,
563
+ pendingHashes: result.PendingHashes.map((hash) => ({
564
+ hash: hash.Hash,
565
+ nodeUid: makeNodeUid(volumeId, hash.LinkID),
566
+ revisionUid: makeNodeRevisionUid(volumeId, hash.LinkID, hash.RevisionID),
567
+ clientUid: hash.ClientUID || undefined,
568
+ })),
569
+ };
570
+ }
508
571
  }
509
572
 
510
573
  type LinkResponse = {
@@ -645,6 +645,15 @@ export class NodesCryptoService {
645
645
  nameSignatureEmail: email,
646
646
  };
647
647
  }
648
+
649
+ async generateNameHashes(parentHashKey: Uint8Array, names: string[]): Promise<{ name: string; hash: string }[]> {
650
+ return Promise.all(
651
+ names.map(async (name) => ({
652
+ name,
653
+ hash: await this.driveCrypto.generateLookupHash(name, parentHashKey),
654
+ })),
655
+ );
656
+ }
648
657
  }
649
658
 
650
659
  function getClaimedAuthor(
@@ -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) {
@@ -64,6 +64,7 @@ describe('nodesModules integration tests', () => {
64
64
  account,
65
65
  driveCrypto,
66
66
  sharesService,
67
+ 'clientUid',
67
68
  );
68
69
 
69
70
  nodesCache = new NodesCache(getMockLogger(), driveEntitiesCache);
@@ -37,20 +37,14 @@ export function initNodesModule(
37
37
  account: ProtonDriveAccount,
38
38
  driveCrypto: DriveCrypto,
39
39
  sharesService: SharesService,
40
+ clientUid: string | undefined,
40
41
  ) {
41
- const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
42
+ const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService, clientUid);
42
43
  const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
43
44
  const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
44
45
  const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
45
46
  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
- );
47
+ const nodesAccess = new NodesAccess(telemetry, api, cache, cryptoCache, cryptoService, sharesService);
54
48
  const nodesEventHandler = new NodesEventsHandler(telemetry.getLogger('nodes-events'), cache);
55
49
  const nodesManagement = new NodesManagement(api, cryptoCache, cryptoService, nodesAccess);
56
50
  const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
@@ -0,0 +1,57 @@
1
+ import { splitExtension, joinNameAndExtension } from './nodeName';
2
+
3
+ describe('nodeName', () => {
4
+ describe('splitExtension', () => {
5
+ it('should handle empty string', () => {
6
+ const result = splitExtension('');
7
+ expect(result).toEqual(['', '']);
8
+ });
9
+
10
+ it('should split filename with extension correctly', () => {
11
+ const result = splitExtension('document.pdf');
12
+ expect(result).toEqual(['document', 'pdf']);
13
+ });
14
+
15
+ it('should handle filename without extension', () => {
16
+ const result = splitExtension('folder');
17
+ expect(result).toEqual(['folder', '']);
18
+ });
19
+
20
+ it('should split filename with multiple dots correctly', () => {
21
+ const result = splitExtension('my.file.name.txt');
22
+ expect(result).toEqual(['my.file.name', 'txt']);
23
+ });
24
+
25
+ it('should handle filename ending with dot', () => {
26
+ const result = splitExtension('dot.');
27
+ expect(result).toEqual(['dot.', '']);
28
+ });
29
+
30
+ it('should handle filename with only extension', () => {
31
+ const result = splitExtension('.gitignore');
32
+ expect(result).toEqual(['.gitignore', '']);
33
+ });
34
+ });
35
+
36
+ describe('joinNameAndExtension', () => {
37
+ it('should join name, index, and extension correctly', () => {
38
+ const result = joinNameAndExtension('document', 1, 'pdf');
39
+ expect(result).toBe('document (1).pdf');
40
+ });
41
+
42
+ it('should handle empty name with extension', () => {
43
+ const result = joinNameAndExtension('', 2, 'txt');
44
+ expect(result).toBe('(2).txt');
45
+ });
46
+
47
+ it('should handle name with empty extension', () => {
48
+ const result = joinNameAndExtension('document', 3, '');
49
+ expect(result).toBe('document (3)');
50
+ });
51
+
52
+ it('should handle both name and extension empty', () => {
53
+ const result = joinNameAndExtension('', 4, '');
54
+ expect(result).toBe('(4)');
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Split a filename into `[name, extension]`
3
+ */
4
+ export function splitExtension(filename = ''): [string, string] {
5
+ const endIdx = filename.lastIndexOf('.');
6
+ if (endIdx === -1 || endIdx === 0 || endIdx === filename.length - 1) {
7
+ return [filename, ''];
8
+ }
9
+ return [filename.slice(0, endIdx), filename.slice(endIdx + 1)];
10
+ }
11
+
12
+ /**
13
+ * Join a filename into `name (index).extension`
14
+ */
15
+ export function joinNameAndExtension(name: string, index: number, extension: string): string {
16
+ if (!name && !extension) {
17
+ return `(${index})`;
18
+ }
19
+ if (!name) {
20
+ return `(${index}).${extension}`;
21
+ }
22
+ if (!extension) {
23
+ return `${name} (${index})`;
24
+ }
25
+ return `${name} (${index}).${extension}`;
26
+ }
@@ -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 {