@protontech/drive-sdk 0.6.2 → 0.7.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 (158) hide show
  1. package/dist/interface/index.d.ts +1 -0
  2. package/dist/interface/index.js.map +1 -1
  3. package/dist/interface/nodes.d.ts +14 -10
  4. package/dist/interface/nodes.js +5 -8
  5. package/dist/interface/nodes.js.map +1 -1
  6. package/dist/interface/photos.d.ts +62 -0
  7. package/dist/interface/photos.js +3 -0
  8. package/dist/interface/photos.js.map +1 -0
  9. package/dist/internal/apiService/apiService.d.ts +2 -2
  10. package/dist/internal/apiService/apiService.js.map +1 -1
  11. package/dist/internal/apiService/driveTypes.d.ts +1294 -517
  12. package/dist/internal/apiService/errors.js +4 -3
  13. package/dist/internal/apiService/errors.js.map +1 -1
  14. package/dist/internal/download/cryptoService.js +8 -6
  15. package/dist/internal/download/cryptoService.js.map +1 -1
  16. package/dist/internal/download/fileDownloader.d.ts +2 -1
  17. package/dist/internal/download/fileDownloader.js +6 -3
  18. package/dist/internal/download/fileDownloader.js.map +1 -1
  19. package/dist/internal/download/index.d.ts +1 -1
  20. package/dist/internal/download/index.js +3 -3
  21. package/dist/internal/download/index.js.map +1 -1
  22. package/dist/internal/errors.d.ts +1 -0
  23. package/dist/internal/errors.js +4 -0
  24. package/dist/internal/errors.js.map +1 -1
  25. package/dist/internal/nodes/apiService.d.ts +68 -16
  26. package/dist/internal/nodes/apiService.js +138 -85
  27. package/dist/internal/nodes/apiService.js.map +1 -1
  28. package/dist/internal/nodes/apiService.test.js +7 -5
  29. package/dist/internal/nodes/apiService.test.js.map +1 -1
  30. package/dist/internal/nodes/cache.d.ts +16 -8
  31. package/dist/internal/nodes/cache.js +19 -5
  32. package/dist/internal/nodes/cache.js.map +1 -1
  33. package/dist/internal/nodes/cache.test.js +1 -0
  34. package/dist/internal/nodes/cache.test.js.map +1 -1
  35. package/dist/internal/nodes/cryptoReporter.d.ts +3 -3
  36. package/dist/internal/nodes/cryptoReporter.js.map +1 -1
  37. package/dist/internal/nodes/cryptoService.d.ts +13 -22
  38. package/dist/internal/nodes/cryptoService.js +47 -16
  39. package/dist/internal/nodes/cryptoService.js.map +1 -1
  40. package/dist/internal/nodes/cryptoService.test.js +262 -17
  41. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  42. package/dist/internal/nodes/events.d.ts +2 -2
  43. package/dist/internal/nodes/events.js.map +1 -1
  44. package/dist/internal/nodes/index.test.js +1 -0
  45. package/dist/internal/nodes/index.test.js.map +1 -1
  46. package/dist/internal/nodes/interface.d.ts +14 -3
  47. package/dist/internal/nodes/nodesAccess.d.ts +36 -20
  48. package/dist/internal/nodes/nodesAccess.js +54 -29
  49. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  50. package/dist/internal/nodes/nodesManagement.d.ts +34 -14
  51. package/dist/internal/nodes/nodesManagement.js +44 -31
  52. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  53. package/dist/internal/nodes/nodesManagement.test.js +60 -14
  54. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  55. package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
  56. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  57. package/dist/internal/photos/albums.d.ts +2 -2
  58. package/dist/internal/photos/albums.js.map +1 -1
  59. package/dist/internal/photos/index.d.ts +19 -3
  60. package/dist/internal/photos/index.js +38 -8
  61. package/dist/internal/photos/index.js.map +1 -1
  62. package/dist/internal/photos/interface.d.ts +18 -9
  63. package/dist/internal/photos/nodes.d.ts +57 -0
  64. package/dist/internal/photos/nodes.js +165 -0
  65. package/dist/internal/photos/nodes.js.map +1 -0
  66. package/dist/internal/photos/timeline.d.ts +2 -2
  67. package/dist/internal/photos/timeline.js.map +1 -1
  68. package/dist/internal/photos/timeline.test.js.map +1 -1
  69. package/dist/internal/photos/upload.d.ts +2 -2
  70. package/dist/internal/photos/upload.js.map +1 -1
  71. package/dist/internal/sharingPublic/index.d.ts +6 -6
  72. package/dist/internal/sharingPublic/index.js +8 -7
  73. package/dist/internal/sharingPublic/index.js.map +1 -1
  74. package/dist/internal/sharingPublic/nodes.d.ts +16 -3
  75. package/dist/internal/sharingPublic/nodes.js +34 -2
  76. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  77. package/dist/internal/sharingPublic/unauthApiService.d.ts +17 -0
  78. package/dist/internal/sharingPublic/unauthApiService.js +31 -0
  79. package/dist/internal/sharingPublic/unauthApiService.js.map +1 -0
  80. package/dist/internal/sharingPublic/unauthApiService.test.d.ts +1 -0
  81. package/dist/internal/sharingPublic/unauthApiService.test.js +27 -0
  82. package/dist/internal/sharingPublic/unauthApiService.test.js.map +1 -0
  83. package/dist/internal/upload/apiService.d.ts +4 -3
  84. package/dist/internal/upload/apiService.js.map +1 -1
  85. package/dist/internal/upload/cryptoService.d.ts +8 -3
  86. package/dist/internal/upload/cryptoService.js +45 -9
  87. package/dist/internal/upload/cryptoService.js.map +1 -1
  88. package/dist/internal/upload/fileUploader.test.js +1 -1
  89. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  90. package/dist/internal/upload/interface.d.ts +25 -13
  91. package/dist/internal/upload/manager.js +7 -4
  92. package/dist/internal/upload/manager.js.map +1 -1
  93. package/dist/internal/upload/manager.test.js +5 -4
  94. package/dist/internal/upload/manager.test.js.map +1 -1
  95. package/dist/internal/upload/streamUploader.js +9 -4
  96. package/dist/internal/upload/streamUploader.js.map +1 -1
  97. package/dist/internal/upload/streamUploader.test.js +8 -5
  98. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  99. package/dist/protonDriveClient.d.ts +11 -2
  100. package/dist/protonDriveClient.js +20 -4
  101. package/dist/protonDriveClient.js.map +1 -1
  102. package/dist/protonDrivePhotosClient.d.ts +8 -8
  103. package/dist/protonDrivePhotosClient.js +8 -9
  104. package/dist/protonDrivePhotosClient.js.map +1 -1
  105. package/dist/protonDrivePublicLinkClient.d.ts +9 -2
  106. package/dist/protonDrivePublicLinkClient.js +16 -5
  107. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  108. package/dist/transformers.d.ts +7 -2
  109. package/dist/transformers.js +37 -0
  110. package/dist/transformers.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/interface/index.ts +1 -0
  113. package/src/interface/nodes.ts +14 -11
  114. package/src/interface/photos.ts +67 -0
  115. package/src/internal/apiService/apiService.ts +2 -2
  116. package/src/internal/apiService/driveTypes.ts +1294 -517
  117. package/src/internal/apiService/errors.ts +5 -4
  118. package/src/internal/download/cryptoService.ts +13 -6
  119. package/src/internal/download/fileDownloader.ts +4 -2
  120. package/src/internal/download/index.ts +3 -0
  121. package/src/internal/errors.ts +4 -0
  122. package/src/internal/nodes/apiService.test.ts +7 -5
  123. package/src/internal/nodes/apiService.ts +210 -124
  124. package/src/internal/nodes/cache.test.ts +1 -0
  125. package/src/internal/nodes/cache.ts +32 -13
  126. package/src/internal/nodes/cryptoReporter.ts +3 -3
  127. package/src/internal/nodes/cryptoService.test.ts +380 -18
  128. package/src/internal/nodes/cryptoService.ts +77 -36
  129. package/src/internal/nodes/events.ts +2 -2
  130. package/src/internal/nodes/index.test.ts +1 -0
  131. package/src/internal/nodes/interface.ts +17 -2
  132. package/src/internal/nodes/nodesAccess.ts +99 -54
  133. package/src/internal/nodes/nodesManagement.test.ts +69 -14
  134. package/src/internal/nodes/nodesManagement.ts +94 -48
  135. package/src/internal/nodes/nodesRevisions.ts +3 -3
  136. package/src/internal/photos/albums.ts +2 -2
  137. package/src/internal/photos/index.ts +45 -3
  138. package/src/internal/photos/interface.ts +21 -9
  139. package/src/internal/photos/nodes.ts +233 -0
  140. package/src/internal/photos/timeline.test.ts +2 -2
  141. package/src/internal/photos/timeline.ts +2 -2
  142. package/src/internal/photos/upload.ts +3 -3
  143. package/src/internal/sharingPublic/index.ts +7 -3
  144. package/src/internal/sharingPublic/nodes.ts +43 -2
  145. package/src/internal/sharingPublic/unauthApiService.test.ts +29 -0
  146. package/src/internal/sharingPublic/unauthApiService.ts +32 -0
  147. package/src/internal/upload/apiService.ts +4 -3
  148. package/src/internal/upload/cryptoService.ts +73 -12
  149. package/src/internal/upload/fileUploader.test.ts +1 -1
  150. package/src/internal/upload/interface.ts +24 -13
  151. package/src/internal/upload/manager.test.ts +5 -4
  152. package/src/internal/upload/manager.ts +7 -4
  153. package/src/internal/upload/streamUploader.test.ts +8 -5
  154. package/src/internal/upload/streamUploader.ts +10 -4
  155. package/src/protonDriveClient.ts +27 -5
  156. package/src/protonDrivePhotosClient.ts +23 -23
  157. package/src/protonDrivePublicLinkClient.ts +19 -3
  158. package/src/transformers.ts +49 -2
@@ -1,8 +1,7 @@
1
1
  import { c } from 'ttag';
2
2
 
3
3
  import { NodeWithSameNameExistsValidationError, ProtonDriveError, ValidationError } from '../../errors';
4
- import { Logger, NodeResult } from '../../interface';
5
- import { MemberRole, RevisionState } from '../../interface/nodes';
4
+ import { Logger, NodeResult, MemberRole, RevisionState, AnonymousUser } from '../../interface';
6
5
  import {
7
6
  DriveAPIService,
8
7
  drivePaths,
@@ -102,7 +101,6 @@ type PostRestoreRevisionResponse =
102
101
  type DeleteRevisionResponse =
103
102
  drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['delete']['responses']['200']['content']['application/json'];
104
103
 
105
-
106
104
  type PostCheckAvailableHashesRequest = Extract<
107
105
  drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/checkAvailableHashes']['post']['requestBody'],
108
106
  { content: object }
@@ -116,18 +114,21 @@ type PostCheckAvailableHashesResponse =
116
114
  * The service is responsible for transforming local objects to API payloads
117
115
  * and vice versa. It should not contain any business logic.
118
116
  */
119
- export class NodeAPIService {
117
+ export abstract class NodeAPIServiceBase<
118
+ T extends EncryptedNode = EncryptedNode,
119
+ TMetadataResponseLink extends { Link: { LinkID: string } } = { Link: { LinkID: string } },
120
+ > {
120
121
  constructor(
121
- private logger: Logger,
122
- private apiService: DriveAPIService,
123
- private clientUid: string | undefined,
122
+ protected logger: Logger,
123
+ protected apiService: DriveAPIService,
124
+ protected clientUid: string | undefined,
124
125
  ) {
125
126
  this.logger = logger;
126
127
  this.apiService = apiService;
127
128
  this.clientUid = clientUid;
128
129
  }
129
130
 
130
- async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
131
+ async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<T> {
131
132
  const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, undefined, signal);
132
133
  const result = await nodesGenerator.next();
133
134
  if (!result.value) {
@@ -142,7 +143,7 @@ export class NodeAPIService {
142
143
  ownVolumeId: string | undefined,
143
144
  filterOptions?: FilterOptions,
144
145
  signal?: AbortSignal,
145
- ): AsyncGenerator<EncryptedNode> {
146
+ ): AsyncGenerator<T> {
146
147
  const allNodeIds = nodeUids.map(splitNodeUid);
147
148
 
148
149
  const nodeIdsByVolumeId = new Map<string, string[]>();
@@ -186,27 +187,21 @@ export class NodeAPIService {
186
187
  }
187
188
  }
188
189
 
189
- private async *iterateNodesPerVolume(
190
+ protected async *iterateNodesPerVolume(
190
191
  volumeId: string,
191
192
  nodeIds: string[],
192
193
  isOwnVolumeId: boolean,
193
194
  filterOptions?: FilterOptions,
194
195
  signal?: AbortSignal,
195
- ): AsyncGenerator<EncryptedNode, unknown[]> {
196
+ ): AsyncGenerator<T, unknown[]> {
196
197
  const errors: unknown[] = [];
197
198
 
198
199
  for (const nodeIdsBatch of batch(nodeIds, API_NODES_BATCH_SIZE)) {
199
- const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
200
- `drive/v2/volumes/${volumeId}/links`,
201
- {
202
- LinkIDs: nodeIdsBatch,
203
- },
204
- signal,
205
- );
200
+ const responseLinks = await this.fetchNodeMetadata(volumeId, nodeIdsBatch, signal);
206
201
 
207
- for (const link of response.Links) {
202
+ for (const link of responseLinks) {
208
203
  try {
209
- const encryptedNode = linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
204
+ const encryptedNode = this.linkToEncryptedNode(volumeId, link, isOwnVolumeId);
210
205
  if (filterOptions?.type && encryptedNode.type !== filterOptions.type) {
211
206
  continue;
212
207
  }
@@ -221,6 +216,14 @@ export class NodeAPIService {
221
216
  return errors;
222
217
  }
223
218
 
219
+ protected abstract fetchNodeMetadata(
220
+ volumeId: string,
221
+ linkIds: string[],
222
+ signal?: AbortSignal,
223
+ ): Promise<TMetadataResponseLink[]>;
224
+
225
+ protected abstract linkToEncryptedNode(volumeId: string, link: TMetadataResponseLink, isOwnVolumeId: boolean): T;
226
+
224
227
  // Improvement requested: load next page sooner before all IDs are yielded.
225
228
  async *iterateChildrenNodeUids(
226
229
  parentNodeUid: string,
@@ -292,7 +295,7 @@ export class NodeAPIService {
292
295
  },
293
296
  newNode: {
294
297
  encryptedName: string;
295
- nameSignatureEmail: string;
298
+ nameSignatureEmail: string | AnonymousUser;
296
299
  hash?: string;
297
300
  },
298
301
  signal?: AbortSignal,
@@ -332,9 +335,9 @@ export class NodeAPIService {
332
335
  parentUid: string;
333
336
  armoredNodePassphrase: string;
334
337
  armoredNodePassphraseSignature?: string;
335
- signatureEmail?: string;
338
+ signatureEmail?: string | AnonymousUser;
336
339
  encryptedName: string;
337
- nameSignatureEmail?: string;
340
+ nameSignatureEmail?: string | AnonymousUser;
338
341
  hash: string;
339
342
  contentHash?: string;
340
343
  },
@@ -343,24 +346,29 @@ export class NodeAPIService {
343
346
  const { volumeId, nodeId } = splitNodeUid(nodeUid);
344
347
  const { nodeId: newParentNodeId } = splitNodeUid(newNode.parentUid);
345
348
 
346
- await this.apiService.put<Omit<PutMoveNodeRequest, 'SignatureAddress' | 'MIMEType'>, PutMoveNodeResponse>(
347
- `drive/v2/volumes/${volumeId}/links/${nodeId}/move`,
348
- {
349
- ParentLinkID: newParentNodeId,
350
- NodePassphrase: newNode.armoredNodePassphrase,
351
- // @ts-expect-error: API accepts NodePassphraseSignature as optional.
352
- NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
353
- // @ts-expect-error: API accepts SignatureEmail as optional.
354
- SignatureEmail: newNode.signatureEmail,
355
- Name: newNode.encryptedName,
356
- // @ts-expect-error: API accepts NameSignatureEmail as optional.
357
- NameSignatureEmail: newNode.nameSignatureEmail,
358
- Hash: newNode.hash,
359
- OriginalHash: oldNode.hash,
360
- ContentHash: newNode.contentHash || null,
361
- },
362
- signal,
363
- );
349
+ try {
350
+ await this.apiService.put<Omit<PutMoveNodeRequest, 'SignatureAddress' | 'MIMEType'>, PutMoveNodeResponse>(
351
+ `drive/v2/volumes/${volumeId}/links/${nodeId}/move`,
352
+ {
353
+ ParentLinkID: newParentNodeId,
354
+ NodePassphrase: newNode.armoredNodePassphrase,
355
+ // @ts-expect-error: API accepts NodePassphraseSignature as optional.
356
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
357
+ // @ts-expect-error: API accepts SignatureEmail as optional.
358
+ SignatureEmail: newNode.signatureEmail,
359
+ Name: newNode.encryptedName,
360
+ // @ts-expect-error: API accepts NameSignatureEmail as optional.
361
+ NameSignatureEmail: newNode.nameSignatureEmail,
362
+ Hash: newNode.hash,
363
+ OriginalHash: oldNode.hash,
364
+ ContentHash: newNode.contentHash || null,
365
+ },
366
+ signal,
367
+ );
368
+ } catch (error: unknown) {
369
+ handleNodeWithSameNameExistsValidationError(volumeId, error);
370
+ throw error;
371
+ }
364
372
  }
365
373
 
366
374
  async copyNode(
@@ -369,9 +377,9 @@ export class NodeAPIService {
369
377
  parentUid: string;
370
378
  armoredNodePassphrase: string;
371
379
  armoredNodePassphraseSignature?: string;
372
- signatureEmail?: string;
380
+ signatureEmail?: string | AnonymousUser;
373
381
  encryptedName: string;
374
- nameSignatureEmail?: string;
382
+ nameSignatureEmail?: string | AnonymousUser;
375
383
  hash: string;
376
384
  },
377
385
  signal?: AbortSignal,
@@ -379,23 +387,29 @@ export class NodeAPIService {
379
387
  const { volumeId, nodeId } = splitNodeUid(nodeUid);
380
388
  const { volumeId: parentVolumeId, nodeId: parentNodeId } = splitNodeUid(newNode.parentUid);
381
389
 
382
- const response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
383
- `drive/volumes/${volumeId}/links/${nodeId}/copy`,
384
- {
385
- TargetVolumeID: parentVolumeId,
386
- TargetParentLinkID: parentNodeId,
387
- NodePassphrase: newNode.armoredNodePassphrase,
388
- // @ts-expect-error: API accepts NodePassphraseSignature as optional.
389
- NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
390
- // @ts-expect-error: API accepts SignatureEmail as optional.
391
- SignatureEmail: newNode.signatureEmail,
392
- Name: newNode.encryptedName,
393
- // @ts-expect-error: API accepts NameSignatureEmail as optional.
394
- NameSignatureEmail: newNode.nameSignatureEmail,
395
- Hash: newNode.hash,
396
- },
397
- signal,
398
- );
390
+ let response: PostCopyNodeResponse;
391
+ try {
392
+ response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
393
+ `drive/volumes/${volumeId}/links/${nodeId}/copy`,
394
+ {
395
+ TargetVolumeID: parentVolumeId,
396
+ TargetParentLinkID: parentNodeId,
397
+ NodePassphrase: newNode.armoredNodePassphrase,
398
+ // @ts-expect-error: API accepts NodePassphraseSignature as optional.
399
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
400
+ // @ts-expect-error: API accepts SignatureEmail as optional.
401
+ SignatureEmail: newNode.signatureEmail,
402
+ Name: newNode.encryptedName,
403
+ // @ts-expect-error: API accepts NameSignatureEmail as optional.
404
+ NameSignatureEmail: newNode.nameSignatureEmail,
405
+ Hash: newNode.hash,
406
+ },
407
+ signal,
408
+ );
409
+ } catch (error: unknown) {
410
+ handleNodeWithSameNameExistsValidationError(volumeId, error);
411
+ throw error;
412
+ }
399
413
 
400
414
  return makeNodeUid(volumeId, response.LinkID);
401
415
  }
@@ -430,7 +444,7 @@ export class NodeAPIService {
430
444
  }
431
445
  }
432
446
 
433
- async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
447
+ async *deleteTrashedNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
434
448
  for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
435
449
  const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
436
450
  `drive/v2/volumes/${volumeId}/trash/delete_multiple`,
@@ -445,6 +459,21 @@ export class NodeAPIService {
445
459
  }
446
460
  }
447
461
 
462
+ async *deleteExistingNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
463
+ for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
464
+ const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
465
+ `drive/v2/volumes/${volumeId}/delete_multiple`,
466
+ {
467
+ LinkIDs: batchNodeIds,
468
+ },
469
+ signal,
470
+ );
471
+
472
+ // TODO: remove `as` when backend fixes OpenAPI schema.
473
+ yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
474
+ }
475
+ }
476
+
448
477
  async createFolder(
449
478
  parentUid: string,
450
479
  newNode: {
@@ -452,7 +481,7 @@ export class NodeAPIService {
452
481
  armoredHashKey: string;
453
482
  armoredNodePassphrase: string;
454
483
  armoredNodePassphraseSignature: string;
455
- signatureEmail: string;
484
+ signatureEmail: string | AnonymousUser;
456
485
  encryptedName: string;
457
486
  hash: string;
458
487
  armoredExtendedAttributes?: string;
@@ -478,21 +507,7 @@ export class NodeAPIService {
478
507
  },
479
508
  );
480
509
  } 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
- }
510
+ handleNodeWithSameNameExistsValidationError(volumeId, error);
496
511
  throw error;
497
512
  }
498
513
 
@@ -570,6 +585,35 @@ export class NodeAPIService {
570
585
  }
571
586
  }
572
587
 
588
+ export class NodeAPIService extends NodeAPIServiceBase {
589
+ constructor(logger: Logger, apiService: DriveAPIService, clientUid: string | undefined) {
590
+ super(logger, apiService, clientUid);
591
+ }
592
+
593
+ protected async fetchNodeMetadata(
594
+ volumeId: string,
595
+ linkIds: string[],
596
+ signal?: AbortSignal,
597
+ ): Promise<PostLoadLinksMetadataResponse['Links']> {
598
+ const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
599
+ `drive/v2/volumes/${volumeId}/links`,
600
+ {
601
+ LinkIDs: linkIds,
602
+ },
603
+ signal,
604
+ );
605
+ return response.Links;
606
+ }
607
+
608
+ protected linkToEncryptedNode(
609
+ volumeId: string,
610
+ link: PostLoadLinksMetadataResponse['Links'][0],
611
+ isOwnVolumeId: boolean,
612
+ ): EncryptedNode {
613
+ return linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
614
+ }
615
+ }
616
+
573
617
  type LinkResponse = {
574
618
  LinkID: string;
575
619
  Response: {
@@ -602,56 +646,36 @@ function* handleResponseErrors(
602
646
  }
603
647
  }
604
648
 
605
- function linkToEncryptedNode(
649
+ function handleNodeWithSameNameExistsValidationError(volumeId: string, error: unknown): void {
650
+ if (error instanceof ValidationError) {
651
+ if (error.code === ErrorCode.ALREADY_EXISTS) {
652
+ const typedDetails = error.details as
653
+ | {
654
+ ConflictLinkID: string;
655
+ }
656
+ | undefined;
657
+
658
+ const existingNodeUid = typedDetails?.ConflictLinkID
659
+ ? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
660
+ : undefined;
661
+
662
+ throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
663
+ }
664
+ }
665
+ }
666
+
667
+ export function linkToEncryptedNode(
606
668
  logger: Logger,
607
669
  volumeId: string,
608
- link: PostLoadLinksMetadataResponse['Links'][0],
670
+ link: Pick<PostLoadLinksMetadataResponse['Links'][0], 'Link' | 'Membership' | 'Sharing' | 'Folder' | 'File'>,
609
671
  isAdmin: boolean,
610
672
  ): EncryptedNode {
611
- const membershipRole = permissionsToMemberRole(logger, link.Membership?.Permissions);
612
-
613
- const baseNodeMetadata = {
614
- // Internal metadata
615
- hash: link.Link.NameHash || undefined,
616
- encryptedName: link.Link.Name,
617
-
618
- // Basic node metadata
619
- uid: makeNodeUid(volumeId, link.Link.LinkID),
620
- parentUid: link.Link.ParentLinkID ? makeNodeUid(volumeId, link.Link.ParentLinkID) : undefined,
621
- type: nodeTypeNumberToNodeType(logger, link.Link.Type),
622
- creationTime: new Date(link.Link.CreateTime * 1000),
623
- trashTime: link.Link.TrashTime ? new Date(link.Link.TrashTime * 1000) : undefined,
624
-
625
- // Sharing node metadata
626
- shareId: link.Sharing?.ShareID || undefined,
627
- isShared: !!link.Sharing,
628
- isSharedPublicly: !!link.Sharing?.ShareURLID,
629
- directRole: isAdmin ? MemberRole.Admin : membershipRole,
630
- membership: link.Membership
631
- ? {
632
- role: membershipRole,
633
- inviteTime: new Date(link.Membership.InviteTime * 1000),
634
- }
635
- : undefined,
636
- };
637
-
638
- const baseCryptoNodeMetadata = {
639
- signatureEmail: link.Link.SignatureEmail || undefined,
640
- nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
641
- armoredKey: link.Link.NodeKey,
642
- armoredNodePassphrase: link.Link.NodePassphrase,
643
- armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
644
- membership: link.Membership
645
- ? {
646
- inviterEmail: link.Membership.InviterEmail,
647
- base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
648
- armoredInviterSharePassphraseKeyPacketSignature:
649
- link.Membership.InviterSharePassphraseKeyPacketSignature,
650
- armoredInviteeSharePassphraseSessionKeySignature:
651
- link.Membership.InviteeSharePassphraseSessionKeySignature,
652
- }
653
- : undefined,
654
- };
673
+ const { baseNodeMetadata, baseCryptoNodeMetadata } = linkToEncryptedNodeBaseMetadata(
674
+ logger,
675
+ volumeId,
676
+ link,
677
+ isAdmin,
678
+ );
655
679
 
656
680
  if (link.Link.Type === 1 && link.Folder) {
657
681
  return {
@@ -693,6 +717,11 @@ function linkToEncryptedNode(
693
717
  };
694
718
  }
695
719
 
720
+ // TODO: Remove this once client do not use main SDK for photos.
721
+ // At the beginning, the client used main SDK for some photo actions.
722
+ // This was a temporary solution before the Photos SDK was implemented.
723
+ // Now the client must use Photos SDK for all photo-related actions.
724
+ // Knowledge of albums in main SDK is deprecated and will be removed.
696
725
  if (link.Link.Type === 3) {
697
726
  return {
698
727
  ...baseNodeMetadata,
@@ -705,6 +734,64 @@ function linkToEncryptedNode(
705
734
  throw new Error(`Unknown node type: ${link.Link.Type}`);
706
735
  }
707
736
 
737
+ export function linkToEncryptedNodeBaseMetadata(
738
+ logger: Logger,
739
+ volumeId: string,
740
+ link: Pick<PostLoadLinksMetadataResponse['Links'][0], 'Link' | 'Membership' | 'Sharing'>,
741
+ isAdmin: boolean,
742
+ ) {
743
+ const membershipRole = permissionsToMemberRole(logger, link.Membership?.Permissions);
744
+
745
+ const baseNodeMetadata = {
746
+ // Internal metadata
747
+ hash: link.Link.NameHash || undefined,
748
+ encryptedName: link.Link.Name,
749
+
750
+ // Basic node metadata
751
+ uid: makeNodeUid(volumeId, link.Link.LinkID),
752
+ parentUid: link.Link.ParentLinkID ? makeNodeUid(volumeId, link.Link.ParentLinkID) : undefined,
753
+ type: nodeTypeNumberToNodeType(logger, link.Link.Type),
754
+ creationTime: new Date(link.Link.CreateTime * 1000),
755
+ modificationTime: new Date(link.Link.ModifyTime * 1000),
756
+ trashTime: link.Link.TrashTime ? new Date(link.Link.TrashTime * 1000) : undefined,
757
+
758
+ // Sharing node metadata
759
+ shareId: link.Sharing?.ShareID || undefined,
760
+ isShared: !!link.Sharing,
761
+ isSharedPublicly: !!link.Sharing?.ShareURLID,
762
+ directRole: isAdmin ? MemberRole.Admin : membershipRole,
763
+ membership: link.Membership
764
+ ? {
765
+ role: membershipRole,
766
+ inviteTime: new Date(link.Membership.InviteTime * 1000),
767
+ }
768
+ : undefined,
769
+ };
770
+
771
+ const baseCryptoNodeMetadata = {
772
+ signatureEmail: link.Link.SignatureEmail || undefined,
773
+ nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
774
+ armoredKey: link.Link.NodeKey,
775
+ armoredNodePassphrase: link.Link.NodePassphrase,
776
+ armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
777
+ membership: link.Membership
778
+ ? {
779
+ inviterEmail: link.Membership.InviterEmail,
780
+ base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
781
+ armoredInviterSharePassphraseKeyPacketSignature:
782
+ link.Membership.InviterSharePassphraseKeyPacketSignature,
783
+ armoredInviteeSharePassphraseSessionKeySignature:
784
+ link.Membership.InviteeSharePassphraseSessionKeySignature,
785
+ }
786
+ : undefined,
787
+ };
788
+
789
+ return {
790
+ baseNodeMetadata,
791
+ baseCryptoNodeMetadata,
792
+ };
793
+ }
794
+
708
795
  export function* groupNodeUidsByVolumeAndIteratePerBatch(
709
796
  nodeUids: string[],
710
797
  ): Generator<{ volumeId: string; batchNodeIds: string[]; batchNodeUids: string[] }> {
@@ -732,7 +819,6 @@ export function* groupNodeUidsByVolumeAndIteratePerBatch(
732
819
  }
733
820
  }
734
821
 
735
-
736
822
  function transformRevisionResponse(
737
823
  volumeId: string,
738
824
  nodeId: string,
@@ -23,6 +23,7 @@ function generateNode(
23
23
  isShared: false,
24
24
  isSharedPublicly: false,
25
25
  creationTime: new Date(),
26
+ modificationTime: new Date(),
26
27
  trashTime: undefined,
27
28
  volumeId: 'volumeId',
28
29
  isStale: false,
@@ -9,7 +9,9 @@ export enum CACHE_TAG_KEYS {
9
9
  Roots = 'nodeRoot',
10
10
  }
11
11
 
12
- type DecryptedNodeResult = { uid: string; ok: true; node: DecryptedNode } | { uid: string; ok: false; error: string };
12
+ type DecryptedNodeResult<T extends DecryptedNode> =
13
+ | { uid: string; ok: true; node: T }
14
+ | { uid: string; ok: false; error: string };
13
15
 
14
16
  /**
15
17
  * Provides caching for nodes metadata.
@@ -19,7 +21,7 @@ type DecryptedNodeResult = { uid: string; ok: true; node: DecryptedNode } | { ui
19
21
  *
20
22
  * The cache of node metadata should not contain any crypto material.
21
23
  */
22
- export class NodesCache {
24
+ export abstract class NodesCacheBase<T extends DecryptedNode = DecryptedNode> {
23
25
  constructor(
24
26
  private logger: Logger,
25
27
  private driveCache: ProtonDriveEntitiesCache,
@@ -28,9 +30,9 @@ export class NodesCache {
28
30
  this.driveCache = driveCache;
29
31
  }
30
32
 
31
- async setNode(node: DecryptedNode): Promise<void> {
33
+ async setNode(node: T): Promise<void> {
32
34
  const key = getCacheUid(node.uid);
33
- const nodeData = serialiseNode(node);
35
+ const nodeData = this.serialiseNode(node);
34
36
  const { volumeId } = splitNodeUid(node.uid);
35
37
 
36
38
  const tags = [`volume:${volumeId}`];
@@ -46,11 +48,11 @@ export class NodesCache {
46
48
  await this.driveCache.setEntity(key, nodeData, tags);
47
49
  }
48
50
 
49
- async getNode(nodeUid: string): Promise<DecryptedNode> {
51
+ async getNode(nodeUid: string): Promise<T> {
50
52
  const key = getCacheUid(nodeUid);
51
53
  const nodeData = await this.driveCache.getEntity(key);
52
54
  try {
53
- return deserialiseNode(nodeData);
55
+ return this.deserialiseNode(nodeData);
54
56
  } catch (error: unknown) {
55
57
  await this.removeCorruptedNode({ nodeUid }, error);
56
58
  throw new Error(`Failed to deserialise node: ${error instanceof Error ? error.message : error}`, {
@@ -59,6 +61,10 @@ export class NodesCache {
59
61
  }
60
62
  }
61
63
 
64
+ protected abstract serialiseNode(node: T): string;
65
+
66
+ protected abstract deserialiseNode(nodeData: string): T;
67
+
62
68
  /**
63
69
  * Set all nodes on given node as stale. This is useful when we
64
70
  * get refresh event from the server and we thus don't know
@@ -146,7 +152,7 @@ export class NodesCache {
146
152
  return cacheUids;
147
153
  }
148
154
 
149
- async *iterateNodes(nodeUids: string[]): AsyncGenerator<DecryptedNodeResult> {
155
+ async *iterateNodes(nodeUids: string[]): AsyncGenerator<DecryptedNodeResult<T>> {
150
156
  const cacheUids = nodeUids.map(getCacheUid);
151
157
  for await (const result of this.driveCache.iterateEntities(cacheUids)) {
152
158
  const node = await this.convertCacheResult(result);
@@ -156,7 +162,7 @@ export class NodesCache {
156
162
  }
157
163
  }
158
164
 
159
- async *iterateChildren(parentNodeUid: string): AsyncGenerator<DecryptedNodeResult> {
165
+ async *iterateChildren(parentNodeUid: string): AsyncGenerator<DecryptedNodeResult<T>> {
160
166
  for await (const result of this.driveCache.iterateEntitiesByTag(
161
167
  `${CACHE_TAG_KEYS.ParentUid}:${parentNodeUid}`,
162
168
  )) {
@@ -171,7 +177,7 @@ export class NodesCache {
171
177
  yield* this.driveCache.iterateEntitiesByTag(`${CACHE_TAG_KEYS.Roots}:${volumeId}`);
172
178
  }
173
179
 
174
- async *iterateTrashedNodes(): AsyncGenerator<DecryptedNodeResult> {
180
+ async *iterateTrashedNodes(): AsyncGenerator<DecryptedNodeResult<T>> {
175
181
  for await (const result of this.driveCache.iterateEntitiesByTag(CACHE_TAG_KEYS.Trashed)) {
176
182
  const node = await this.convertCacheResult(result);
177
183
  if (node) {
@@ -184,7 +190,7 @@ export class NodesCache {
184
190
  * Converts result from the cache with cache UID and data to result of node
185
191
  * with node UID and DecryptedNode.
186
192
  */
187
- private async convertCacheResult(result: EntityResult<string>): Promise<DecryptedNodeResult | null> {
193
+ private async convertCacheResult(result: EntityResult<string>): Promise<DecryptedNodeResult<T> | null> {
188
194
  let nodeUid;
189
195
  try {
190
196
  nodeUid = getNodeUid(result.key);
@@ -195,7 +201,7 @@ export class NodesCache {
195
201
  if (result.ok) {
196
202
  let node;
197
203
  try {
198
- node = deserialiseNode(result.value);
204
+ node = this.deserialiseNode(result.value);
199
205
  } catch (error: unknown) {
200
206
  await this.removeCorruptedNode({ nodeUid }, error);
201
207
  return null;
@@ -232,6 +238,16 @@ export class NodesCache {
232
238
  }
233
239
  }
234
240
 
241
+ export class NodesCache extends NodesCacheBase<DecryptedNode> {
242
+ protected serialiseNode(node: DecryptedNode): string {
243
+ return serialiseNode(node);
244
+ }
245
+
246
+ protected deserialiseNode(nodeData: string): DecryptedNode {
247
+ return deserialiseNode(nodeData);
248
+ }
249
+ }
250
+
235
251
  function getCacheUid(nodeUid: string) {
236
252
  return `node-${nodeUid}`;
237
253
  }
@@ -243,11 +259,12 @@ function getNodeUid(cacheUid: string) {
243
259
  return cacheUid.substring(5);
244
260
  }
245
261
 
246
- function serialiseNode(node: DecryptedNode) {
262
+ export function serialiseNode(node: DecryptedNode) {
247
263
  return JSON.stringify(node);
248
264
  }
249
265
 
250
- function deserialiseNode(nodeData: string): DecryptedNode {
266
+ // TODO: use better deserialisation with validation
267
+ export function deserialiseNode(nodeData: string): DecryptedNode {
251
268
  const node = JSON.parse(nodeData);
252
269
  if (
253
270
  !node ||
@@ -263,6 +280,7 @@ function deserialiseNode(nodeData: string): DecryptedNode {
263
280
  typeof node.isShared !== 'boolean' ||
264
281
  !node.creationTime ||
265
282
  typeof node.creationTime !== 'string' ||
283
+ typeof node.modificationTime !== 'string' ||
266
284
  (typeof node.trashTime !== 'string' && node.trashTime !== undefined) ||
267
285
  (typeof node.folder !== 'object' && node.folder !== undefined) ||
268
286
  (typeof node.folder?.claimedModificationTime !== 'string' && node.folder?.claimedModificationTime !== undefined)
@@ -272,6 +290,7 @@ function deserialiseNode(nodeData: string): DecryptedNode {
272
290
  return {
273
291
  ...node,
274
292
  creationTime: new Date(node.creationTime),
293
+ modificationTime: new Date(node.modificationTime),
275
294
  trashTime: node.trashTime ? new Date(node.trashTime) : undefined,
276
295
  activeRevision: node.activeRevision ? deserialiseRevision(node.activeRevision) : undefined,
277
296
  membership: node.membership
@@ -34,7 +34,7 @@ export class NodesCryptoReporter {
34
34
  signatureType: string,
35
35
  verified: VERIFICATION_STATUS,
36
36
  verificationErrors?: Error[],
37
- claimedAuthor?: string,
37
+ claimedAuthor?: string | AnonymousUser,
38
38
  notAvailableVerificationKeys = false,
39
39
  ): Promise<Author> {
40
40
  const author = handleClaimedAuthor(
@@ -54,7 +54,7 @@ export class NodesCryptoReporter {
54
54
  node: { uid: string; creationTime: Date },
55
55
  field: MetricVerificationErrorField,
56
56
  verificationErrors?: Error[],
57
- claimedAuthor?: string,
57
+ claimedAuthor?: string | AnonymousUser,
58
58
  ) {
59
59
  if (this.reportedVerificationErrors.has(node.uid)) {
60
60
  return;
@@ -128,7 +128,7 @@ function handleClaimedAuthor(
128
128
  signatureType: string,
129
129
  verified: VERIFICATION_STATUS,
130
130
  verificationErrors?: Error[],
131
- claimedAuthor?: string,
131
+ claimedAuthor?: string | AnonymousUser,
132
132
  notAvailableVerificationKeys = false,
133
133
  ): Author {
134
134
  if (!claimedAuthor && notAvailableVerificationKeys) {