@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
@@ -6,6 +6,7 @@ import { DecryptedNode } from './interface';
6
6
  import { NodesManagement } from './nodesManagement';
7
7
  import { NodeResult } from '../../interface';
8
8
  import { NodeOutOfSyncError } from './errors';
9
+ import { ValidationError } from '../../errors';
9
10
 
10
11
  describe('NodesManagement', () => {
11
12
  let apiService: NodeAPIService;
@@ -57,7 +58,7 @@ describe('NodesManagement', () => {
57
58
  restoreNodes: jest.fn(async function* (uids) {
58
59
  yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
59
60
  }),
60
- deleteNodes: jest.fn(async function* (uids) {
61
+ deleteTrashedNodes: jest.fn(async function* (uids) {
61
62
  yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
62
63
  }),
63
64
  createFolder: jest.fn(),
@@ -117,7 +118,12 @@ describe('NodesManagement', () => {
117
118
  nameSessionKey: `${uid}-nameSessionKey`,
118
119
  }),
119
120
  ),
120
- getRootNodeEmailKey: jest.fn().mockResolvedValue({ email: 'root-email', addressKey: 'root-key' }),
121
+ getNodeSigningKeys: jest.fn().mockResolvedValue({
122
+ type: 'userAddress',
123
+ email: 'root-email',
124
+ addressId: 'root-addressId',
125
+ key: 'root-key',
126
+ }),
121
127
  notifyNodeChanged: jest.fn(),
122
128
  notifyNodeDeleted: jest.fn(),
123
129
  notifyChildCreated: jest.fn(),
@@ -136,11 +142,11 @@ describe('NodesManagement', () => {
136
142
  nameAuthor: { ok: true, value: 'newSignatureEmail' },
137
143
  hash: 'newHash',
138
144
  });
139
- expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('nodeUid');
145
+ expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({ nodeUid: 'nodeUid', parentNodeUid: 'parentUid' });
140
146
  expect(cryptoService.encryptNewName).toHaveBeenCalledWith(
141
147
  { key: 'parentUid-key', hashKey: 'parentUid-hashKey' },
142
148
  'nodeUid-nameSessionKey',
143
- { email: 'root-email', addressKey: 'root-key' },
149
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
144
150
  'new name',
145
151
  );
146
152
  expect(apiService.renameNode).toHaveBeenCalledWith(
@@ -181,9 +187,12 @@ describe('NodesManagement', () => {
181
187
  keyAuthor: { ok: true, value: 'movedSignatureEmail' },
182
188
  nameAuthor: { ok: true, value: 'movedNameSignatureEmail' },
183
189
  });
184
- expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
190
+ expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({
191
+ nodeUid: 'nodeUid',
192
+ parentNodeUid: 'newParentNodeUid',
193
+ });
185
194
  expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
186
- nodes.nodeUid,
195
+ nodes.nodeUid.name,
187
196
  expect.objectContaining({
188
197
  key: 'nodeUid-key',
189
198
  passphrase: 'nodeUid-passphrase',
@@ -192,7 +201,7 @@ describe('NodesManagement', () => {
192
201
  nameSessionKey: 'nodeUid-nameSessionKey',
193
202
  }),
194
203
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
195
- { email: 'root-email', addressKey: 'root-key' },
204
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
196
205
  );
197
206
  expect(apiService.moveNode).toHaveBeenCalledWith(
198
207
  'nodeUid',
@@ -223,7 +232,7 @@ describe('NodesManagement', () => {
223
232
  const newNode = await management.moveNode('anonymousNodeUid', 'newParentNodeUid');
224
233
 
225
234
  expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
226
- nodes.anonymousNodeUid,
235
+ nodes.anonymousNodeUid.name,
227
236
  expect.objectContaining({
228
237
  key: 'anonymousNodeUid-key',
229
238
  passphrase: 'anonymousNodeUid-passphrase',
@@ -232,7 +241,7 @@ describe('NodesManagement', () => {
232
241
  nameSessionKey: 'anonymousNodeUid-nameSessionKey',
233
242
  }),
234
243
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
235
- { email: 'root-email', addressKey: 'root-key' },
244
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
236
245
  );
237
246
  expect(newNode).toEqual({
238
247
  ...nodes.anonymousNodeUid,
@@ -276,9 +285,12 @@ describe('NodesManagement', () => {
276
285
  keyAuthor: { ok: true, value: 'copiedSignatureEmail' },
277
286
  nameAuthor: { ok: true, value: 'copiedNameSignatureEmail' },
278
287
  });
279
- expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
288
+ expect(nodesAccess.getNodeSigningKeys).toHaveBeenCalledWith({
289
+ nodeUid: 'nodeUid',
290
+ parentNodeUid: 'newParentNodeUid',
291
+ });
280
292
  expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
281
- nodes.nodeUid,
293
+ nodes.nodeUid.name,
282
294
  expect.objectContaining({
283
295
  key: 'nodeUid-key',
284
296
  passphrase: 'nodeUid-passphrase',
@@ -287,7 +299,7 @@ describe('NodesManagement', () => {
287
299
  nameSessionKey: 'nodeUid-nameSessionKey',
288
300
  }),
289
301
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
290
- { email: 'root-email', addressKey: 'root-key' },
302
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
291
303
  );
292
304
  expect(apiService.copyNode).toHaveBeenCalledWith('nodeUid', {
293
305
  parentUid: 'newParentNodeUid',
@@ -313,7 +325,7 @@ describe('NodesManagement', () => {
313
325
  const newNode = await management.copyNode('anonymousNodeUid', 'newParentNodeUid');
314
326
 
315
327
  expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
316
- nodes.anonymousNodeUid,
328
+ nodes.anonymousNodeUid.name,
317
329
  expect.objectContaining({
318
330
  key: 'anonymousNodeUid-key',
319
331
  passphrase: 'anonymousNodeUid-passphrase',
@@ -322,7 +334,7 @@ describe('NodesManagement', () => {
322
334
  nameSessionKey: 'anonymousNodeUid-nameSessionKey',
323
335
  }),
324
336
  expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
325
- { email: 'root-email', addressKey: 'root-key' },
337
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
326
338
  );
327
339
  expect(newNode).toEqual({
328
340
  ...nodes.anonymousNodeUid,
@@ -339,6 +351,49 @@ describe('NodesManagement', () => {
339
351
  });
340
352
  });
341
353
 
354
+ it('copyNode manages copy of node with new name', async () => {
355
+ const encryptedCrypto = {
356
+ encryptedName: 'copiedArmoredNodeName',
357
+ hash: 'copiedHash',
358
+ armoredNodePassphrase: 'copiedArmoredNodePassphrase',
359
+ armoredNodePassphraseSignature: 'copiedArmoredNodePassphraseSignature',
360
+ signatureEmail: 'copiedSignatureEmail',
361
+ nameSignatureEmail: 'copiedNameSignatureEmail',
362
+ };
363
+ cryptoService.encryptNodeWithNewParent = jest.fn().mockResolvedValue(encryptedCrypto);
364
+
365
+ const newName = 'new name';
366
+ const newNode = await management.copyNode('nodeUid', 'newParentNodeUid', newName);
367
+
368
+ expect(newNode).toEqual({
369
+ ...nodes.nodeUid,
370
+ name: { ok: true, value: newName },
371
+ uid: 'newCopiedNodeUid',
372
+ parentUid: 'newParentNodeUid',
373
+ encryptedName: 'copiedArmoredNodeName',
374
+ hash: 'copiedHash',
375
+ keyAuthor: { ok: true, value: 'copiedSignatureEmail' },
376
+ nameAuthor: { ok: true, value: 'copiedNameSignatureEmail' },
377
+ });
378
+ expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
379
+ { ok: true, value: newName },
380
+ expect.objectContaining({
381
+ key: 'nodeUid-key',
382
+ passphrase: 'nodeUid-passphrase',
383
+ passphraseSessionKey: 'nodeUid-passphraseSessionKey',
384
+ contentKeyPacketSessionKey: 'nodeUid-contentKeyPacketSessionKey',
385
+ nameSessionKey: 'nodeUid-nameSessionKey',
386
+ }),
387
+ expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
388
+ { type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
389
+ );
390
+ });
391
+
392
+ it('copyNode throws error if name is invalid', async () => {
393
+ const promise = management.copyNode('nodeUid', 'newParentNodeUid', 'invalid/name');
394
+ await expect(promise).rejects.toThrow(ValidationError);
395
+ });
396
+
342
397
  it('trashes node and updates cache', async () => {
343
398
  const uids = ['v1~n1', 'v1~n2'];
344
399
  const trashed = new Set();
@@ -1,17 +1,17 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { MemberRole, NodeType, NodeResult, NodeResultWithNewUid, resultOk } from '../../interface';
3
+ import { MemberRole, NodeType, NodeResult, NodeResultWithNewUid, resultOk, InvalidNameError } from '../../interface';
4
4
  import { AbortError, ValidationError } from '../../errors';
5
- import { getErrorMessage } from '../errors';
5
+ import { createErrorFromUnknown, getErrorMessage } from '../errors';
6
6
  import { splitNodeUid } from '../uids';
7
- import { NodeAPIService } from './apiService';
7
+ import { NodeAPIServiceBase } from './apiService';
8
8
  import { NodesCryptoCache } from './cryptoCache';
9
9
  import { NodesCryptoService } from './cryptoService';
10
10
  import { NodeOutOfSyncError } from './errors';
11
11
  import { generateFolderExtendedAttributes } from './extendedAttributes';
12
- import { DecryptedNode } from './interface';
12
+ import { DecryptedNode, EncryptedNode } from './interface';
13
13
  import { splitExtension, joinNameAndExtension } from './nodeName';
14
- import { NodesAccess } from './nodesAccess';
14
+ import { NodesAccessBase } from './nodesAccess';
15
15
  import { validateNodeName } from './validations';
16
16
 
17
17
  const AVAILABLE_NAME_BATCH_SIZE = 10;
@@ -26,12 +26,16 @@ const AVAILABLE_NAME_LIMIT = 1000;
26
26
  * This module uses other modules providing low-level operations, such
27
27
  * as API service, cache, crypto service, etc.
28
28
  */
29
- export class NodesManagement {
29
+ export abstract class NodesManagementBase<
30
+ TEncryptedNode extends EncryptedNode = EncryptedNode,
31
+ TDecryptedNode extends DecryptedNode = DecryptedNode,
32
+ TNodesCryptoService extends NodesCryptoService = NodesCryptoService,
33
+ > {
30
34
  constructor(
31
- private apiService: NodeAPIService,
32
- private cryptoCache: NodesCryptoCache,
33
- private cryptoService: NodesCryptoService,
34
- private nodesAccess: NodesAccess,
35
+ protected apiService: NodeAPIServiceBase<TEncryptedNode>,
36
+ protected cryptoCache: NodesCryptoCache,
37
+ protected cryptoService: NodesCryptoService,
38
+ protected nodesAccess: NodesAccessBase<TEncryptedNode, TDecryptedNode, TNodesCryptoService>,
35
39
  ) {
36
40
  this.apiService = apiService;
37
41
  this.cryptoCache = cryptoCache;
@@ -43,13 +47,13 @@ export class NodesManagement {
43
47
  nodeUid: string,
44
48
  newName: string,
45
49
  options = { allowRenameRootNode: false },
46
- ): Promise<DecryptedNode> {
50
+ ): Promise<TDecryptedNode> {
47
51
  validateNodeName(newName);
48
52
 
49
53
  const node = await this.nodesAccess.getNode(nodeUid);
50
54
  const { nameSessionKey: nodeNameSessionKey } = await this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid);
51
55
  const parentKeys = await this.nodesAccess.getParentKeys(node);
52
- const address = await this.nodesAccess.getRootNodeEmailKey(nodeUid);
56
+ const signingKeys = await this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: node.parentUid });
53
57
 
54
58
  if (!options.allowRenameRootNode && (!node.hash || !parentKeys.hashKey)) {
55
59
  throw new ValidationError(c('Error').t`Renaming root item is not allowed`);
@@ -58,7 +62,7 @@ export class NodesManagement {
58
62
  const { signatureEmail, armoredNodeName, hash } = await this.cryptoService.encryptNewName(
59
63
  parentKeys,
60
64
  nodeNameSessionKey,
61
- address,
65
+ signingKeys,
62
66
  newName,
63
67
  );
64
68
 
@@ -91,11 +95,11 @@ export class NodesManagement {
91
95
  }
92
96
 
93
97
  await this.nodesAccess.notifyNodeChanged(nodeUid);
94
- const newNode: DecryptedNode = {
98
+ const newNode: TDecryptedNode = {
95
99
  ...node,
96
100
  name: resultOk(newName),
97
101
  encryptedName: armoredNodeName,
98
- nameAuthor: resultOk(signatureEmail),
102
+ nameAuthor: resultOk(signatureEmail || null),
99
103
  hash,
100
104
  };
101
105
  return newNode;
@@ -123,15 +127,13 @@ export class NodesManagement {
123
127
  }
124
128
  }
125
129
 
126
- async moveNode(nodeUid: string, newParentUid: string): Promise<DecryptedNode> {
127
- const [node, address] = await Promise.all([
128
- this.nodesAccess.getNode(nodeUid),
129
- this.nodesAccess.getRootNodeEmailKey(newParentUid),
130
- ]);
130
+ async moveNode(nodeUid: string, newParentUid: string): Promise<TDecryptedNode> {
131
+ const node = await this.nodesAccess.getNode(nodeUid);
131
132
 
132
- const [keys, newParentKeys] = await Promise.all([
133
+ const [keys, newParentKeys, signingKeys] = await Promise.all([
133
134
  this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
134
135
  this.nodesAccess.getNodeKeys(newParentUid),
136
+ this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: newParentUid }),
135
137
  ]);
136
138
 
137
139
  if (!node.hash) {
@@ -142,10 +144,10 @@ export class NodesManagement {
142
144
  }
143
145
 
144
146
  const encryptedCrypto = await this.cryptoService.encryptNodeWithNewParent(
145
- node,
147
+ node.name,
146
148
  keys,
147
149
  { key: newParentKeys.key, hashKey: newParentKeys.hashKey },
148
- address,
150
+ signingKeys,
149
151
  );
150
152
 
151
153
  // Node could be uploaded or renamed by anonymous user and thus have
@@ -174,7 +176,7 @@ export class NodesManagement {
174
176
  // TODO: When moving photos, we need to pass content hash.
175
177
  },
176
178
  );
177
- const newNode: DecryptedNode = {
179
+ const newNode: TDecryptedNode = {
178
180
  ...node,
179
181
  encryptedName: encryptedCrypto.encryptedName,
180
182
  parentUid: newParentUid,
@@ -188,16 +190,18 @@ export class NodesManagement {
188
190
 
189
191
  // Improvement requested: copy nodes in parallel using copy_multiple endpoint
190
192
  async *copyNodes(
191
- nodeUids: string[],
193
+ nodeUidsOrWithNames: (string | { uid: string; name: string })[],
192
194
  newParentNodeUid: string,
193
195
  signal?: AbortSignal,
194
196
  ): AsyncGenerator<NodeResultWithNewUid> {
195
- for (const nodeUid of nodeUids) {
197
+ for (const nodeUidOrWithName of nodeUidsOrWithNames) {
196
198
  if (signal?.aborted) {
197
199
  throw new AbortError(c('Error').t`Copy operation aborted`);
198
200
  }
201
+ const nodeUid = typeof nodeUidOrWithName === 'string' ? nodeUidOrWithName : nodeUidOrWithName.uid;
202
+ const name = typeof nodeUidOrWithName === 'string' ? undefined : nodeUidOrWithName.name;
199
203
  try {
200
- const { uid: newNodeUid } = await this.copyNode(nodeUid, newParentNodeUid);
204
+ const { uid: newNodeUid } = await this.copyNode(nodeUid, newParentNodeUid, name);
201
205
  yield {
202
206
  uid: nodeUid,
203
207
  newUid: newNodeUid,
@@ -207,21 +211,24 @@ export class NodesManagement {
207
211
  yield {
208
212
  uid: nodeUid,
209
213
  ok: false,
210
- error: getErrorMessage(error),
214
+ error: createErrorFromUnknown(error),
211
215
  };
212
216
  }
213
217
  }
214
218
  }
215
219
 
216
- async copyNode(nodeUid: string, newParentUid: string): Promise<DecryptedNode> {
217
- const [node, address] = await Promise.all([
218
- this.nodesAccess.getNode(nodeUid),
219
- this.nodesAccess.getRootNodeEmailKey(newParentUid),
220
- ]);
220
+ async copyNode(nodeUid: string, newParentUid: string, name?: string): Promise<TDecryptedNode> {
221
+ if (name) {
222
+ validateNodeName(name);
223
+ }
221
224
 
222
- const [keys, newParentKeys] = await Promise.all([
225
+ const node = await this.nodesAccess.getNode(nodeUid);
226
+ const nodeName = name ? resultOk<string, Error | InvalidNameError>(name) : node.name;
227
+
228
+ const [keys, newParentKeys, signingKeys] = await Promise.all([
223
229
  this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
224
230
  this.nodesAccess.getNodeKeys(newParentUid),
231
+ this.nodesAccess.getNodeSigningKeys({ nodeUid, parentNodeUid: newParentUid }),
225
232
  ]);
226
233
 
227
234
  if (!newParentKeys.hashKey) {
@@ -229,10 +236,10 @@ export class NodesManagement {
229
236
  }
230
237
 
231
238
  const encryptedCrypto = await this.cryptoService.encryptNodeWithNewParent(
232
- node,
239
+ nodeName,
233
240
  keys,
234
241
  { key: newParentKeys.key, hashKey: newParentKeys.hashKey },
235
- address,
242
+ signingKeys,
236
243
  );
237
244
 
238
245
  // Node could be uploaded or renamed by anonymous user and thus have
@@ -254,8 +261,9 @@ export class NodesManagement {
254
261
  nameSignatureEmail: encryptedCrypto.nameSignatureEmail,
255
262
  hash: encryptedCrypto.hash,
256
263
  });
257
- const newNode: DecryptedNode = {
264
+ const newNode: TDecryptedNode = {
258
265
  ...node,
266
+ name: nodeName,
259
267
  uid: newNodeUid,
260
268
  encryptedName: encryptedCrypto.encryptedName,
261
269
  parentUid: newParentUid,
@@ -286,7 +294,7 @@ export class NodesManagement {
286
294
  }
287
295
 
288
296
  async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
289
- for await (const result of this.apiService.deleteNodes(nodeUids, signal)) {
297
+ for await (const result of this.apiService.deleteTrashedNodes(nodeUids, signal)) {
290
298
  if (result.ok) {
291
299
  await this.nodesAccess.notifyNodeDeleted(result.uid);
292
300
  }
@@ -295,7 +303,7 @@ export class NodesManagement {
295
303
  }
296
304
 
297
305
  // FIXME create test for create folder
298
- async createFolder(parentNodeUid: string, folderName: string, modificationTime?: Date): Promise<DecryptedNode> {
306
+ async createFolder(parentNodeUid: string, folderName: string, modificationTime?: Date): Promise<TDecryptedNode> {
299
307
  validateNodeName(folderName);
300
308
 
301
309
  const parentKeys = await this.nodesAccess.getNodeKeys(parentNodeUid);
@@ -303,12 +311,12 @@ export class NodesManagement {
303
311
  throw new ValidationError(c('Error').t`Creating folders in non-folders is not allowed`);
304
312
  }
305
313
 
306
- const address = await this.nodesAccess.getRootNodeEmailKey(parentNodeUid);
314
+ const signingKeys = await this.nodesAccess.getNodeSigningKeys({ parentNodeUid });
307
315
  const extendedAttributes = generateFolderExtendedAttributes(modificationTime);
308
316
 
309
317
  const { encryptedCrypto, keys } = await this.cryptoService.createFolder(
310
318
  { key: parentKeys.key, hashKey: parentKeys.hashKey },
311
- address,
319
+ signingKeys,
312
320
  folderName,
313
321
  extendedAttributes,
314
322
  );
@@ -324,8 +332,33 @@ export class NodesManagement {
324
332
  });
325
333
 
326
334
  await this.nodesAccess.notifyChildCreated(parentNodeUid);
335
+ const node = this.generateNodeFolder(nodeUid, parentNodeUid, folderName, encryptedCrypto);
336
+ await this.cryptoCache.setNodeKeys(nodeUid, keys);
337
+ return node;
338
+ }
327
339
 
328
- const node: DecryptedNode = {
340
+ protected abstract generateNodeFolder(
341
+ nodeUid: string,
342
+ parentUid: string,
343
+ name: string,
344
+ encryptedCrypto: {
345
+ hash: string;
346
+ encryptedName: string;
347
+ signatureEmail: string | null;
348
+ },
349
+ ): TDecryptedNode;
350
+
351
+ protected generateNodeFolderBase(
352
+ nodeUid: string,
353
+ parentNodeUid: string,
354
+ name: string,
355
+ encryptedCrypto: {
356
+ hash: string;
357
+ encryptedName: string;
358
+ signatureEmail: string | null;
359
+ },
360
+ ): DecryptedNode {
361
+ return {
329
362
  // Internal metadata
330
363
  hash: encryptedCrypto.hash,
331
364
  encryptedName: encryptedCrypto.encryptedName,
@@ -336,6 +369,7 @@ export class NodesManagement {
336
369
  type: NodeType.Folder,
337
370
  mediaType: 'Folder',
338
371
  creationTime: new Date(),
372
+ modificationTime: new Date(),
339
373
 
340
374
  // Share node metadata
341
375
  isShared: false,
@@ -344,14 +378,11 @@ export class NodesManagement {
344
378
 
345
379
  // Decrypted metadata
346
380
  isStale: false,
347
- keyAuthor: resultOk(encryptedCrypto.signatureEmail),
348
- nameAuthor: resultOk(encryptedCrypto.signatureEmail),
349
- name: resultOk(folderName),
381
+ keyAuthor: resultOk(encryptedCrypto.signatureEmail || null),
382
+ nameAuthor: resultOk(encryptedCrypto.signatureEmail || null),
383
+ name: resultOk(name),
350
384
  treeEventScopeId: splitNodeUid(nodeUid).volumeId,
351
385
  };
352
-
353
- await this.cryptoCache.setNodeKeys(nodeUid, keys);
354
- return node;
355
386
  }
356
387
 
357
388
  async findAvailableName(parentFolderUid: string, name: string): Promise<string> {
@@ -392,3 +423,18 @@ export class NodesManagement {
392
423
  throw new ValidationError(c('Error').t`No available name found`);
393
424
  }
394
425
  }
426
+
427
+ export class NodesManagement extends NodesManagementBase {
428
+ protected generateNodeFolder(
429
+ nodeUid: string,
430
+ parentNodeUid: string,
431
+ name: string,
432
+ encryptedCrypto: {
433
+ hash: string;
434
+ encryptedName: string;
435
+ signatureEmail: string | null;
436
+ },
437
+ ): DecryptedNode {
438
+ return this.generateNodeFolderBase(nodeUid, parentNodeUid, name, encryptedCrypto);
439
+ }
440
+ }
@@ -1,6 +1,6 @@
1
1
  import { Logger } from '../../interface';
2
2
  import { makeNodeUidFromRevisionUid } from '../uids';
3
- import { NodeAPIService } from './apiService';
3
+ import { NodeAPIServiceBase } from './apiService';
4
4
  import { NodesCryptoService } from './cryptoService';
5
5
  import { NodesAccess } from './nodesAccess';
6
6
  import { parseFileExtendedAttributes } from './extendedAttributes';
@@ -12,9 +12,9 @@ import { DecryptedRevision } from './interface';
12
12
  export class NodesRevisons {
13
13
  constructor(
14
14
  private logger: Logger,
15
- private apiService: NodeAPIService,
15
+ private apiService: NodeAPIServiceBase,
16
16
  private cryptoService: NodesCryptoService,
17
- private nodesAccess: NodesAccess,
17
+ private nodesAccess: Pick<NodesAccess, 'getNodeKeys'>,
18
18
  ) {
19
19
  this.logger = logger;
20
20
  this.apiService = apiService;
@@ -1,7 +1,7 @@
1
1
  import { BatchLoading } from '../batchLoading';
2
2
  import { DecryptedNode } from '../nodes';
3
3
  import { PhotosAPIService } from './apiService';
4
- import { NodesService } from './interface';
4
+ import { PhotosNodesAccess } from './nodes';
5
5
  import { PhotoSharesManager } from './shares';
6
6
 
7
7
  const BATCH_LOADING_SIZE = 10;
@@ -13,7 +13,7 @@ export class Albums {
13
13
  constructor(
14
14
  private apiService: PhotosAPIService,
15
15
  private photoShares: PhotoSharesManager,
16
- private nodesService: NodesService,
16
+ private nodesService: PhotosNodesAccess,
17
17
  ) {
18
18
  this.apiService = apiService;
19
19
  this.photoShares = photoShares;
@@ -6,6 +6,10 @@ import {
6
6
  ProtonDriveEntitiesCache,
7
7
  ProtonDriveTelemetry,
8
8
  } from '../../interface';
9
+ import { NodesCryptoService } from '../nodes/cryptoService';
10
+ import { NodesCryptoReporter } from '../nodes/cryptoReporter';
11
+ import { NodesCryptoCache } from '../nodes/cryptoCache';
12
+ import { ShareTargetType } from '../shares';
9
13
  import { SharesCache } from '../shares/cache';
10
14
  import { SharesCryptoCache } from '../shares/cryptoCache';
11
15
  import { SharesCryptoService } from '../shares/cryptoService';
@@ -14,7 +18,8 @@ import { UploadTelemetry } from '../upload/telemetry';
14
18
  import { UploadQueue } from '../upload/queue';
15
19
  import { Albums } from './albums';
16
20
  import { PhotosAPIService } from './apiService';
17
- import { NodesService, SharesService } from './interface';
21
+ import { SharesService } from './interface';
22
+ import { PhotosNodesAPIService, PhotosNodesAccess, PhotosNodesCache, PhotosNodesManagement } from './nodes';
18
23
  import { PhotoSharesManager } from './shares';
19
24
  import { PhotosTimeline } from './timeline';
20
25
  import {
@@ -24,7 +29,10 @@ import {
24
29
  PhotoUploadManager,
25
30
  PhotoUploadMetadata,
26
31
  } from './upload';
27
- import { ShareTargetType } from '../shares';
32
+ import { NodesRevisons } from '../nodes/nodesRevisions';
33
+ import { NodesEventsHandler } from '../nodes/events';
34
+
35
+ export type { DecryptedPhotoNode } from './interface';
28
36
 
29
37
  // Only photos and albums can be shared in photos volume.
30
38
  export const PHOTOS_SHARE_TARGET_TYPES = [ShareTargetType.Photo, ShareTargetType.Album];
@@ -40,7 +48,7 @@ export function initPhotosModule(
40
48
  apiService: DriveAPIService,
41
49
  driveCrypto: DriveCrypto,
42
50
  photoShares: PhotoSharesManager,
43
- nodesService: NodesService,
51
+ nodesService: PhotosNodesAccess,
44
52
  ) {
45
53
  const api = new PhotosAPIService(apiService);
46
54
  const timeline = new PhotosTimeline(
@@ -89,6 +97,40 @@ export function initPhotoSharesModule(
89
97
  );
90
98
  }
91
99
 
100
+ /**
101
+ * Provides facade for the photo nodes module.
102
+ *
103
+ * The photo nodes module wraps the core nodes module and adds photo specific
104
+ * metadata. It provides the same interface so it can be used in the same way.
105
+ */
106
+ export function initPhotosNodesModule(
107
+ telemetry: ProtonDriveTelemetry,
108
+ apiService: DriveAPIService,
109
+ driveEntitiesCache: ProtonDriveEntitiesCache,
110
+ driveCryptoCache: ProtonDriveCryptoCache,
111
+ account: ProtonDriveAccount,
112
+ driveCrypto: DriveCrypto,
113
+ sharesService: PhotoSharesManager,
114
+ clientUid: string | undefined,
115
+ ) {
116
+ const api = new PhotosNodesAPIService(telemetry.getLogger('nodes-api'), apiService, clientUid);
117
+ const cache = new PhotosNodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
118
+ const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
119
+ const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
120
+ const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
121
+ const nodesAccess = new PhotosNodesAccess(telemetry, api, cache, cryptoCache, cryptoService, sharesService);
122
+ const nodesEventHandler = new NodesEventsHandler(telemetry.getLogger('nodes-events'), cache);
123
+ const nodesManagement = new PhotosNodesManagement(api, cryptoCache, cryptoService, nodesAccess);
124
+ const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
125
+
126
+ return {
127
+ access: nodesAccess,
128
+ management: nodesManagement,
129
+ revisions: nodesRevisions,
130
+ eventHandler: nodesEventHandler,
131
+ };
132
+ }
133
+
92
134
  /**
93
135
  * Provides facade for the photo upload module.
94
136
  *
@@ -1,6 +1,6 @@
1
1
  import { PrivateKey } from '../../crypto';
2
- import { MissingNode, MetricVolumeType } from '../../interface';
3
- import { DecryptedNode } from '../nodes';
2
+ import { MetricVolumeType, PhotoAttributes } from '../../interface';
3
+ import { DecryptedNode, EncryptedNode, DecryptedUnparsedNode } from '../nodes/interface';
4
4
  import { EncryptedShare } from '../shares';
5
5
 
6
6
  export interface SharesService {
@@ -23,10 +23,22 @@ export interface SharesService {
23
23
  getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType>;
24
24
  }
25
25
 
26
- export interface NodesService {
27
- getNode(nodeUid: string): Promise<DecryptedNode>;
28
- iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode>;
29
- getNodeKeys(nodeUid: string): Promise<{
30
- hashKey?: Uint8Array;
31
- }>;
32
- }
26
+ export type EncryptedPhotoNode = EncryptedNode & {
27
+ photo?: EcnryptedPhotoAttributes;
28
+ };
29
+
30
+ export type DecryptedUnparsedPhotoNode = DecryptedUnparsedNode & {
31
+ photo?: PhotoAttributes;
32
+ };
33
+
34
+ export type DecryptedPhotoNode = DecryptedNode & {
35
+ photo?: PhotoAttributes;
36
+ };
37
+
38
+ export type EcnryptedPhotoAttributes = Omit<PhotoAttributes, 'albums'> & {
39
+ contentHash?: string;
40
+ albums: (PhotoAttributes['albums'][0] & {
41
+ nameHash?: string;
42
+ contentHash?: string;
43
+ })[];
44
+ };