@protontech/drive-sdk 0.3.2 → 0.4.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 (120) hide show
  1. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  2. package/dist/internal/apiService/errors.d.ts +3 -0
  3. package/dist/internal/apiService/errors.js +7 -1
  4. package/dist/internal/apiService/errors.js.map +1 -1
  5. package/dist/internal/devices/interface.d.ts +1 -1
  6. package/dist/internal/devices/manager.js +1 -1
  7. package/dist/internal/devices/manager.js.map +1 -1
  8. package/dist/internal/devices/manager.test.js +3 -3
  9. package/dist/internal/devices/manager.test.js.map +1 -1
  10. package/dist/internal/events/apiService.js +1 -1
  11. package/dist/internal/events/apiService.js.map +1 -1
  12. package/dist/internal/events/coreEventManager.js +1 -1
  13. package/dist/internal/events/coreEventManager.js.map +1 -1
  14. package/dist/internal/events/coreEventManager.test.js +18 -24
  15. package/dist/internal/events/coreEventManager.test.js.map +1 -1
  16. package/dist/internal/events/index.d.ts +3 -4
  17. package/dist/internal/events/index.js +4 -4
  18. package/dist/internal/events/index.js.map +1 -1
  19. package/dist/internal/events/interface.d.ts +3 -0
  20. package/dist/internal/nodes/apiService.d.ts +12 -3
  21. package/dist/internal/nodes/apiService.js +53 -13
  22. package/dist/internal/nodes/apiService.js.map +1 -1
  23. package/dist/internal/nodes/apiService.test.js +19 -2
  24. package/dist/internal/nodes/apiService.test.js.map +1 -1
  25. package/dist/internal/nodes/cryptoService.d.ts +1 -1
  26. package/dist/internal/nodes/cryptoService.js +1 -1
  27. package/dist/internal/nodes/cryptoService.js.map +1 -1
  28. package/dist/internal/nodes/cryptoService.test.js +4 -4
  29. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  30. package/dist/internal/nodes/errors.d.ts +4 -0
  31. package/dist/internal/nodes/errors.js +9 -0
  32. package/dist/internal/nodes/errors.js.map +1 -0
  33. package/dist/internal/nodes/index.test.js +1 -1
  34. package/dist/internal/nodes/index.test.js.map +1 -1
  35. package/dist/internal/nodes/interface.d.ts +4 -1
  36. package/dist/internal/nodes/nodesAccess.d.ts +3 -3
  37. package/dist/internal/nodes/nodesAccess.js +25 -15
  38. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  39. package/dist/internal/nodes/nodesAccess.test.js +48 -8
  40. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  41. package/dist/internal/nodes/nodesManagement.d.ts +2 -0
  42. package/dist/internal/nodes/nodesManagement.js +86 -9
  43. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  44. package/dist/internal/nodes/nodesManagement.test.js +81 -5
  45. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  46. package/dist/internal/photos/albums.d.ts +9 -7
  47. package/dist/internal/photos/albums.js +26 -13
  48. package/dist/internal/photos/albums.js.map +1 -1
  49. package/dist/internal/photos/apiService.d.ts +34 -3
  50. package/dist/internal/photos/apiService.js +96 -3
  51. package/dist/internal/photos/apiService.js.map +1 -1
  52. package/dist/internal/photos/index.d.ts +20 -4
  53. package/dist/internal/photos/index.js +30 -7
  54. package/dist/internal/photos/index.js.map +1 -1
  55. package/dist/internal/photos/interface.d.ts +25 -1
  56. package/dist/internal/photos/shares.d.ts +43 -0
  57. package/dist/internal/photos/shares.js +112 -0
  58. package/dist/internal/photos/shares.js.map +1 -0
  59. package/dist/internal/photos/timeline.d.ts +15 -0
  60. package/dist/internal/photos/timeline.js +22 -0
  61. package/dist/internal/photos/timeline.js.map +1 -0
  62. package/dist/internal/shares/manager.d.ts +1 -1
  63. package/dist/internal/shares/manager.js +4 -4
  64. package/dist/internal/shares/manager.js.map +1 -1
  65. package/dist/internal/shares/manager.test.js +7 -7
  66. package/dist/internal/shares/manager.test.js.map +1 -1
  67. package/dist/internal/sharing/interface.d.ts +1 -1
  68. package/dist/internal/sharing/sharingAccess.js +1 -1
  69. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  70. package/dist/internal/sharing/sharingAccess.test.js +1 -1
  71. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  72. package/dist/protonDriveClient.d.ts +20 -3
  73. package/dist/protonDriveClient.js +23 -4
  74. package/dist/protonDriveClient.js.map +1 -1
  75. package/dist/protonDrivePhotosClient.d.ts +86 -12
  76. package/dist/protonDrivePhotosClient.js +132 -29
  77. package/dist/protonDrivePhotosClient.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/internal/apiService/errorCodes.ts +1 -0
  80. package/src/internal/apiService/errors.ts +6 -0
  81. package/src/internal/devices/interface.ts +1 -1
  82. package/src/internal/devices/manager.test.ts +3 -3
  83. package/src/internal/devices/manager.ts +1 -1
  84. package/src/internal/events/apiService.ts +1 -1
  85. package/src/internal/events/coreEventManager.test.ts +21 -27
  86. package/src/internal/events/coreEventManager.ts +1 -1
  87. package/src/internal/events/index.ts +3 -4
  88. package/src/internal/events/interface.ts +4 -0
  89. package/src/internal/nodes/apiService.test.ts +35 -1
  90. package/src/internal/nodes/apiService.ts +103 -17
  91. package/src/internal/nodes/cryptoService.test.ts +8 -8
  92. package/src/internal/nodes/cryptoService.ts +1 -1
  93. package/src/internal/nodes/errors.ts +5 -0
  94. package/src/internal/nodes/index.test.ts +1 -1
  95. package/src/internal/nodes/interface.ts +5 -1
  96. package/src/internal/nodes/nodesAccess.test.ts +68 -8
  97. package/src/internal/nodes/nodesAccess.ts +42 -15
  98. package/src/internal/nodes/nodesManagement.test.ts +100 -5
  99. package/src/internal/nodes/nodesManagement.ts +100 -13
  100. package/src/internal/photos/albums.ts +31 -12
  101. package/src/internal/photos/apiService.ts +159 -4
  102. package/src/internal/photos/index.ts +54 -9
  103. package/src/internal/photos/interface.ts +23 -1
  104. package/src/internal/photos/shares.ts +134 -0
  105. package/src/internal/photos/timeline.ts +24 -0
  106. package/src/internal/shares/manager.test.ts +7 -7
  107. package/src/internal/shares/manager.ts +4 -4
  108. package/src/internal/sharing/interface.ts +1 -1
  109. package/src/internal/sharing/sharingAccess.test.ts +1 -1
  110. package/src/internal/sharing/sharingAccess.ts +1 -1
  111. package/src/protonDriveClient.ts +33 -4
  112. package/src/protonDrivePhotosClient.ts +211 -32
  113. package/dist/internal/photos/cache.d.ts +0 -6
  114. package/dist/internal/photos/cache.js +0 -15
  115. package/dist/internal/photos/cache.js.map +0 -1
  116. package/dist/internal/photos/photosTimeline.d.ts +0 -10
  117. package/dist/internal/photos/photosTimeline.js +0 -19
  118. package/dist/internal/photos/photosTimeline.js.map +0 -1
  119. package/src/internal/photos/cache.ts +0 -11
  120. package/src/internal/photos/photosTimeline.ts +0 -17
@@ -6,6 +6,7 @@ import { MemberRole, RevisionState } from '../../interface/nodes';
6
6
  import {
7
7
  DriveAPIService,
8
8
  drivePaths,
9
+ InvalidRequirementsAPIError,
9
10
  isCodeOk,
10
11
  nodeTypeNumberToNodeType,
11
12
  permissionsToMemberRole,
@@ -13,7 +14,8 @@ import {
13
14
  import { asyncIteratorRace } from '../asyncIteratorRace';
14
15
  import { batch } from '../batch';
15
16
  import { splitNodeUid, makeNodeUid, makeNodeRevisionUid, splitNodeRevisionUid, makeNodeThumbnailUid } from '../uids';
16
- import { EncryptedNode, EncryptedRevision, Thumbnail } from './interface';
17
+ import { NodeOutOfSyncError } from './errors';
18
+ import { EncryptedNode, EncryptedRevision, FilterOptions, Thumbnail } from './interface';
17
19
 
18
20
  // This is the number of calls to the API that are made in parallel.
19
21
  const API_CONCURRENCY = 15;
@@ -48,6 +50,13 @@ type PutMoveNodeRequest = Extract<
48
50
  type PutMoveNodeResponse =
49
51
  drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/move']['put']['responses']['200']['content']['application/json'];
50
52
 
53
+ type PostCopyNodeRequest = Extract<
54
+ drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['requestBody'],
55
+ { content: object }
56
+ >['content']['application/json'];
57
+ type PostCopyNodeResponse =
58
+ drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['responses']['200']['content']['application/json'];
59
+
51
60
  type PostTrashNodesRequest = Extract<
52
61
  drivePaths['/drive/v2/volumes/{volumeID}/trash_multiple']['post']['requestBody'],
53
62
  { content: object }
@@ -108,7 +117,7 @@ export class NodeAPIService {
108
117
  }
109
118
 
110
119
  async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
111
- const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, signal);
120
+ const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, undefined, signal);
112
121
  const result = await nodesGenerator.next();
113
122
  if (!result.value) {
114
123
  throw new ValidationError(c('Error').t`Node not found`);
@@ -117,7 +126,12 @@ export class NodeAPIService {
117
126
  return result.value;
118
127
  }
119
128
 
120
- async *iterateNodes(nodeUids: string[], ownVolumeId: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
129
+ async *iterateNodes(
130
+ nodeUids: string[],
131
+ ownVolumeId: string,
132
+ filterOptions?: FilterOptions,
133
+ signal?: AbortSignal,
134
+ ): AsyncGenerator<EncryptedNode> {
121
135
  const allNodeIds = nodeUids.map(splitNodeUid);
122
136
 
123
137
  const nodeIdsByVolumeId = new Map<string, string[]>();
@@ -139,7 +153,13 @@ export class NodeAPIService {
139
153
  const isAdmin = volumeId === ownVolumeId;
140
154
 
141
155
  yield (async function* () {
142
- const errorsPerVolume = yield* iterateNodesPerVolume(volumeId, nodeIds, isAdmin, signal);
156
+ const errorsPerVolume = yield* iterateNodesPerVolume(
157
+ volumeId,
158
+ nodeIds,
159
+ isAdmin,
160
+ filterOptions,
161
+ signal,
162
+ );
143
163
  if (errorsPerVolume.length) {
144
164
  errors.push(...errorsPerVolume);
145
165
  }
@@ -159,6 +179,7 @@ export class NodeAPIService {
159
179
  volumeId: string,
160
180
  nodeIds: string[],
161
181
  isOwnVolumeId: boolean,
182
+ filterOptions?: FilterOptions,
162
183
  signal?: AbortSignal,
163
184
  ): AsyncGenerator<EncryptedNode, unknown[]> {
164
185
  const errors: unknown[] = [];
@@ -174,7 +195,11 @@ export class NodeAPIService {
174
195
 
175
196
  for (const link of response.Links) {
176
197
  try {
177
- yield linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
198
+ const encryptedNode = linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
199
+ if (filterOptions?.type && encryptedNode.type !== filterOptions.type) {
200
+ continue;
201
+ }
202
+ yield encryptedNode;
178
203
  } catch (error: unknown) {
179
204
  this.logger.error(`Failed to transform node ${link.Link.LinkID}`, error);
180
205
  errors.push(error);
@@ -186,13 +211,25 @@ export class NodeAPIService {
186
211
  }
187
212
 
188
213
  // Improvement requested: load next page sooner before all IDs are yielded.
189
- async *iterateChildrenNodeUids(parentNodeUid: string, signal?: AbortSignal): AsyncGenerator<string> {
214
+ async *iterateChildrenNodeUids(
215
+ parentNodeUid: string,
216
+ onlyFolders: boolean = false,
217
+ signal?: AbortSignal,
218
+ ): AsyncGenerator<string> {
190
219
  const { volumeId, nodeId } = splitNodeUid(parentNodeUid);
191
220
 
192
221
  let anchor = '';
193
222
  while (true) {
223
+ const queryParams = new URLSearchParams();
224
+ if (onlyFolders) {
225
+ queryParams.set('FoldersOnly', '1');
226
+ }
227
+ if (anchor) {
228
+ queryParams.set('AnchorID', anchor);
229
+ }
230
+
194
231
  const response = await this.apiService.get<GetChildrenResponse>(
195
- `drive/v2/volumes/${volumeId}/folders/${nodeId}/children?${anchor ? `AnchorID=${anchor}` : ''}`,
232
+ `drive/v2/volumes/${volumeId}/folders/${nodeId}/children?${queryParams.toString()}`,
196
233
  signal,
197
234
  );
198
235
  for (const linkID of response.LinkIDs) {
@@ -251,16 +288,28 @@ export class NodeAPIService {
251
288
  ): Promise<void> {
252
289
  const { volumeId, nodeId } = splitNodeUid(nodeUid);
253
290
 
254
- await this.apiService.put<Omit<PutRenameNodeRequest, 'SignatureAddress' | 'MIMEType'>, PutRenameNodeResponse>(
255
- `drive/v2/volumes/${volumeId}/links/${nodeId}/rename`,
256
- {
257
- Name: newNode.encryptedName,
258
- NameSignatureEmail: newNode.nameSignatureEmail,
259
- Hash: newNode.hash,
260
- OriginalHash: originalNode.hash || null,
261
- },
262
- signal,
263
- );
291
+ try {
292
+ await this.apiService.put<
293
+ Omit<PutRenameNodeRequest, 'SignatureAddress' | 'MIMEType'>,
294
+ PutRenameNodeResponse
295
+ >(
296
+ `drive/v2/volumes/${volumeId}/links/${nodeId}/rename`,
297
+ {
298
+ Name: newNode.encryptedName,
299
+ NameSignatureEmail: newNode.nameSignatureEmail,
300
+ Hash: newNode.hash,
301
+ OriginalHash: originalNode.hash || null,
302
+ },
303
+ signal,
304
+ );
305
+ } catch (error: unknown) {
306
+ // API returns generic code 2000 when node is out of sync.
307
+ // We map this to specific error for clarity.
308
+ if (error instanceof InvalidRequirementsAPIError) {
309
+ throw new NodeOutOfSyncError(error.message, error.code, { cause: error });
310
+ }
311
+ throw error;
312
+ }
264
313
  }
265
314
 
266
315
  async moveNode(
@@ -303,6 +352,43 @@ export class NodeAPIService {
303
352
  );
304
353
  }
305
354
 
355
+ async copyNode(
356
+ nodeUid: string,
357
+ newNode: {
358
+ parentUid: string;
359
+ armoredNodePassphrase: string;
360
+ armoredNodePassphraseSignature?: string;
361
+ signatureEmail?: string;
362
+ encryptedName: string;
363
+ nameSignatureEmail?: string;
364
+ hash: string;
365
+ },
366
+ signal?: AbortSignal,
367
+ ): Promise<string> {
368
+ const { volumeId, nodeId } = splitNodeUid(nodeUid);
369
+ const { volumeId: parentVolumeId, nodeId: parentNodeId } = splitNodeUid(newNode.parentUid);
370
+
371
+ const response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
372
+ `drive/volumes/${volumeId}/links/${nodeId}/copy`,
373
+ {
374
+ TargetVolumeID: parentVolumeId,
375
+ TargetParentLinkID: parentNodeId,
376
+ NodePassphrase: newNode.armoredNodePassphrase,
377
+ // @ts-expect-error: API accepts NodePassphraseSignature as optional.
378
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
379
+ // @ts-expect-error: API accepts SignatureEmail as optional.
380
+ SignatureEmail: newNode.signatureEmail,
381
+ Name: newNode.encryptedName,
382
+ // @ts-expect-error: API accepts NameSignatureEmail as optional.
383
+ NameSignatureEmail: newNode.nameSignatureEmail,
384
+ Hash: newNode.hash,
385
+ },
386
+ signal,
387
+ );
388
+
389
+ return makeNodeUid(volumeId, response.LinkID);
390
+ }
391
+
306
392
  // Improvement requested: split into multiple calls for many nodes.
307
393
  async *trashNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
308
394
  const nodeIds = nodeUids.map(splitNodeUid);
@@ -985,7 +985,7 @@ describe('nodesCryptoService', () => {
985
985
  });
986
986
  });
987
987
 
988
- describe('moveNode', () => {
988
+ describe('encryptNodeWithNewParent', () => {
989
989
  it('should encrypt node data for move operation', async () => {
990
990
  const node = {
991
991
  name: { ok: true, value: 'testFile.txt' },
@@ -1012,7 +1012,7 @@ describe('nodesCryptoService', () => {
1012
1012
  armoredPassphraseSignature: 'passphraseSignature',
1013
1013
  });
1014
1014
 
1015
- const result = await cryptoService.moveNode(node, keys as any, parentKeys, address);
1015
+ const result = await cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address);
1016
1016
 
1017
1017
  expect(result).toEqual({
1018
1018
  encryptedName: 'encryptedNodeName',
@@ -1056,9 +1056,9 @@ describe('nodesCryptoService', () => {
1056
1056
  addressKey: 'addressKey' as any,
1057
1057
  };
1058
1058
 
1059
- await expect(cryptoService.moveNode(node, keys as any, parentKeys, address)).rejects.toThrow(
1060
- 'Moving item to a non-folder is not allowed',
1061
- );
1059
+ await expect(
1060
+ cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address),
1061
+ ).rejects.toThrow('Moving item to a non-folder is not allowed');
1062
1062
  });
1063
1063
 
1064
1064
  it('should throw error when node has invalid name', async () => {
@@ -1079,9 +1079,9 @@ describe('nodesCryptoService', () => {
1079
1079
  addressKey: 'addressKey' as any,
1080
1080
  };
1081
1081
 
1082
- await expect(cryptoService.moveNode(node, keys as any, parentKeys, address)).rejects.toThrow(
1083
- 'Cannot move item without a valid name, please rename the item first',
1084
- );
1082
+ await expect(
1083
+ cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address),
1084
+ ).rejects.toThrow('Cannot move item without a valid name, please rename the item first');
1085
1085
  });
1086
1086
  });
1087
1087
  });
@@ -601,7 +601,7 @@ export class NodesCryptoService {
601
601
  };
602
602
  }
603
603
 
604
- async moveNode(
604
+ async encryptNodeWithNewParent(
605
605
  node: Pick<DecryptedNode, 'name'>,
606
606
  keys: { passphrase: string; passphraseSessionKey: SessionKey; nameSessionKey: SessionKey },
607
607
  parentKeys: { key: PrivateKey; hashKey: Uint8Array },
@@ -0,0 +1,5 @@
1
+ import { ValidationError } from "../../errors";
2
+
3
+ export class NodeOutOfSyncError extends ValidationError {
4
+ name = 'NodeOutOfSyncError';
5
+ }
@@ -52,7 +52,7 @@ describe('nodesModules integration tests', () => {
52
52
  driveCrypto = {};
53
53
  // @ts-expect-error No need to implement all methods for mocking
54
54
  sharesService = {
55
- getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
55
+ getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
56
56
  };
57
57
 
58
58
  nodesModule = initNodesModule(
@@ -12,6 +12,10 @@ import {
12
12
  RevisionState,
13
13
  } from '../../interface';
14
14
 
15
+ export type FilterOptions = {
16
+ type?: NodeType;
17
+ };
18
+
15
19
  /**
16
20
  * Internal common node interface for both encrypted or decrypted node.
17
21
  */
@@ -172,7 +176,7 @@ export interface DecryptedRevision extends Revision {
172
176
  * Interface describing the dependencies to the shares module.
173
177
  */
174
178
  export interface SharesService {
175
- getMyFilesIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
179
+ getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
176
180
  getSharePrivateKey(shareId: string): Promise<PrivateKey>;
177
181
  getMyFilesShareMemberEmailKey(): Promise<{
178
182
  email: string;
@@ -46,7 +46,7 @@ describe('nodesAccess', () => {
46
46
  };
47
47
  // @ts-expect-error No need to implement all methods for mocking
48
48
  shareService = {
49
- getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
49
+ getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
50
50
  getSharePrivateKey: jest.fn(),
51
51
  };
52
52
 
@@ -209,7 +209,12 @@ describe('nodesAccess', () => {
209
209
 
210
210
  const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
211
211
  expect(result).toMatchObject([node1, node4, node2, node3]);
212
- expect(apiService.iterateNodes).toHaveBeenCalledWith([node2.uid, node3.uid], 'volumeId', undefined);
212
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(
213
+ [node2.uid, node3.uid],
214
+ 'volumeId',
215
+ undefined, // filterOptions
216
+ undefined, // signal
217
+ );
213
218
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(2);
214
219
  expect(cache.setNode).toHaveBeenCalledTimes(2);
215
220
  expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(2);
@@ -226,7 +231,11 @@ describe('nodesAccess', () => {
226
231
 
227
232
  const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
228
233
  expect(result).toMatchObject([node1, node2, node3, node4]);
229
- expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('volumeId~parentNodeid', undefined);
234
+ expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
235
+ 'volumeId~parentNodeid',
236
+ false, // onlyFolders
237
+ undefined, // signal
238
+ );
230
239
  expect(apiService.iterateNodes).not.toHaveBeenCalled();
231
240
  expect(cache.setFolderChildrenLoaded).toHaveBeenCalledWith('volumeId~parentNodeid');
232
241
  });
@@ -247,11 +256,16 @@ describe('nodesAccess', () => {
247
256
 
248
257
  const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
249
258
  expect(result).toMatchObject([node1, node2, node3, node4]);
250
- expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('volumeId~parentNodeid', undefined);
259
+ expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
260
+ 'volumeId~parentNodeid',
261
+ false, // onlyFolders
262
+ undefined, // signal
263
+ );
251
264
  expect(apiService.iterateNodes).toHaveBeenCalledWith(
252
265
  ['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'],
253
266
  'volumeId',
254
- undefined,
267
+ undefined, // filterOptions
268
+ undefined, // signal
255
269
  );
256
270
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
257
271
  expect(cache.setNode).toHaveBeenCalledTimes(4);
@@ -320,6 +334,50 @@ describe('nodesAccess', () => {
320
334
  expect(error.cause).toEqual([new DecryptionError('Decryption failed')]);
321
335
  }
322
336
  });
337
+
338
+ it('should return only filtered nodes from cache', async () => {
339
+ cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(true);
340
+ cache.iterateChildren = jest.fn().mockImplementation(async function* () {
341
+ yield { ok: true, node: { ...node1, type: NodeType.Folder } };
342
+ yield { ok: true, node: { ...node2, type: NodeType.Folder } };
343
+ yield { ok: true, node: { ...node3, type: NodeType.File } };
344
+ yield { ok: true, node: { ...node4, type: NodeType.File } };
345
+ });
346
+
347
+ const result = await Array.fromAsync(
348
+ access.iterateFolderChildren('volumeId~parentNodeid', { type: NodeType.Folder }),
349
+ );
350
+
351
+ expect(result).toMatchObject([node1, node2]);
352
+ expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
353
+ });
354
+
355
+ it.only('should return only filtered nodes from API', async () => {
356
+ cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(false);
357
+ cache.getNode = jest.fn().mockImplementation((uid: string) => {
358
+ if (uid === parentNode.uid) {
359
+ return parentNode;
360
+ }
361
+ throw new Error('Entity not found');
362
+ });
363
+ apiService.iterateChildrenNodeUids = jest.fn().mockImplementation(async function* () {
364
+ yield 'volumeId~node1';
365
+ yield 'volumeId~node2';
366
+ yield 'volumeId~node3';
367
+ yield 'volumeId~node4';
368
+ });
369
+ apiService.iterateNodes = jest.fn().mockImplementation(async function* () {
370
+ yield { ...node1, parentUid: 'volumeId~parentNodeId', type: NodeType.Folder };
371
+ yield { ...node2, parentUid: 'volumeId~parentNodeId', type: NodeType.Folder };
372
+ });
373
+
374
+ const result = await Array.fromAsync(
375
+ access.iterateFolderChildren('volumeId~parentNodeid', { type: NodeType.Folder }),
376
+ );
377
+
378
+ expect(result).toMatchObject([node1, node2]);
379
+ expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
380
+ });
323
381
  });
324
382
 
325
383
  describe('iterateTrashedNodes', () => {
@@ -330,7 +388,7 @@ describe('nodesAccess', () => {
330
388
  const node4 = { uid: 'volumeId~node4', isStale: false } as DecryptedNode;
331
389
 
332
390
  beforeEach(() => {
333
- shareService.getMyFilesIDs = jest.fn().mockResolvedValue({ volumeId });
391
+ shareService.getOwnVolumeIDs = jest.fn().mockResolvedValue({ volumeId });
334
392
  apiService.iterateTrashedNodeUids = jest.fn().mockImplementation(async function* () {
335
393
  yield node1.uid;
336
394
  yield node2.uid;
@@ -359,7 +417,8 @@ describe('nodesAccess', () => {
359
417
  expect(apiService.iterateNodes).toHaveBeenCalledWith(
360
418
  ['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'],
361
419
  volumeId,
362
- undefined,
420
+ undefined, // filterOptions
421
+ undefined, // signal
363
422
  );
364
423
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
365
424
  expect(cache.setNode).toHaveBeenCalledTimes(4);
@@ -417,7 +476,8 @@ describe('nodesAccess', () => {
417
476
  expect(apiService.iterateNodes).toHaveBeenCalledWith(
418
477
  ['volumeId~node2', 'volumeId~node3'],
419
478
  'volumeId',
420
- undefined,
479
+ undefined, // filterOptions
480
+ undefined, // signal
421
481
  );
422
482
  });
423
483
 
@@ -12,7 +12,14 @@ import { NodesCache } from './cache';
12
12
  import { NodesCryptoCache } from './cryptoCache';
13
13
  import { NodesCryptoService } from './cryptoService';
14
14
  import { parseFileExtendedAttributes, parseFolderExtendedAttributes } from './extendedAttributes';
15
- import { SharesService, EncryptedNode, DecryptedUnparsedNode, DecryptedNode, DecryptedNodeKeys } from './interface';
15
+ import {
16
+ SharesService,
17
+ EncryptedNode,
18
+ DecryptedUnparsedNode,
19
+ DecryptedNode,
20
+ DecryptedNodeKeys,
21
+ FilterOptions,
22
+ } from './interface';
16
23
  import { validateNodeName } from './validations';
17
24
  import { isProtonDocument, isProtonSheet } from './mediaTypes';
18
25
 
@@ -49,8 +56,8 @@ export class NodesAccess {
49
56
  this.shareService = shareService;
50
57
  }
51
58
 
52
- async getMyFilesRootFolder() {
53
- const { volumeId, rootNodeId } = await this.shareService.getMyFilesIDs();
59
+ async getVolumeRootFolder() {
60
+ const { volumeId, rootNodeId } = await this.shareService.getOwnVolumeIDs();
54
61
  const nodeUid = makeNodeUid(volumeId, rootNodeId);
55
62
  return this.getNode(nodeUid);
56
63
  }
@@ -71,12 +78,16 @@ export class NodesAccess {
71
78
  return node;
72
79
  }
73
80
 
74
- async *iterateFolderChildren(parentNodeUid: string, signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
81
+ async *iterateFolderChildren(
82
+ parentNodeUid: string,
83
+ filterOptions?: FilterOptions,
84
+ signal?: AbortSignal,
85
+ ): AsyncGenerator<DecryptedNode> {
75
86
  // Ensure the parent is loaded and up-to-date.
76
87
  const parentNode = await this.getNode(parentNodeUid);
77
88
 
78
89
  const batchLoading = new BatchLoading<string, DecryptedNode>({
79
- iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal),
90
+ iterateItems: (nodeUids) => this.loadNodes(nodeUids, filterOptions, signal),
80
91
  batchSize: BATCH_LOADING_SIZE,
81
92
  });
82
93
 
@@ -84,6 +95,9 @@ export class NodesAccess {
84
95
  if (areChildrenCached) {
85
96
  for await (const node of this.cache.iterateChildren(parentNodeUid)) {
86
97
  if (node.ok && !node.node.isStale) {
98
+ if (filterOptions?.type && node.node.type !== filterOptions.type) {
99
+ continue;
100
+ }
87
101
  yield node.node;
88
102
  } else {
89
103
  yield* batchLoading.load(node.uid);
@@ -94,13 +108,17 @@ export class NodesAccess {
94
108
  }
95
109
 
96
110
  this.logger.debug(`Folder ${parentNodeUid} children are not cached`);
97
- for await (const nodeUid of this.apiService.iterateChildrenNodeUids(parentNode.uid, signal)) {
111
+ const onlyFolders = filterOptions?.type === NodeType.Folder;
112
+ for await (const nodeUid of this.apiService.iterateChildrenNodeUids(parentNode.uid, onlyFolders, signal)) {
98
113
  let node;
99
114
  try {
100
115
  node = await this.cache.getNode(nodeUid);
101
116
  } catch {}
102
117
 
103
118
  if (node && !node.isStale) {
119
+ if (filterOptions?.type && node.type !== filterOptions.type) {
120
+ continue;
121
+ }
104
122
  yield node;
105
123
  } else {
106
124
  this.logger.debug(`Node ${nodeUid} from ${parentNodeUid} is ${node?.isStale ? 'stale' : 'not cached'}`);
@@ -108,14 +126,18 @@ export class NodesAccess {
108
126
  }
109
127
  }
110
128
  yield* batchLoading.loadRest();
111
- await this.cache.setFolderChildrenLoaded(parentNodeUid);
129
+
130
+ // If some nodes were filtered out, we don't have the folder fully loaded.
131
+ if (!filterOptions) {
132
+ await this.cache.setFolderChildrenLoaded(parentNodeUid);
133
+ }
112
134
  }
113
135
 
114
136
  // Improvement requested: keep status of loaded trash and leverage cache.
115
137
  async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
116
- const { volumeId } = await this.shareService.getMyFilesIDs();
138
+ const { volumeId } = await this.shareService.getOwnVolumeIDs();
117
139
  const batchLoading = new BatchLoading<string, DecryptedNode>({
118
- iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal),
140
+ iterateItems: (nodeUids) => this.loadNodes(nodeUids, undefined, signal),
119
141
  batchSize: BATCH_LOADING_SIZE,
120
142
  });
121
143
  for await (const nodeUid of this.apiService.iterateTrashedNodeUids(volumeId, signal)) {
@@ -136,7 +158,7 @@ export class NodesAccess {
136
158
 
137
159
  async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode> {
138
160
  const batchLoading = new BatchLoading<string, DecryptedNode | MissingNode>({
139
- iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, signal),
161
+ iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, undefined, signal),
140
162
  batchSize: BATCH_LOADING_SIZE,
141
163
  });
142
164
  for await (const result of this.cache.iterateNodes(nodeUids)) {
@@ -186,13 +208,17 @@ export class NodesAccess {
186
208
  }
187
209
 
188
210
  private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
189
- const { volumeId: ownVolumeId } = await this.shareService.getMyFilesIDs();
211
+ const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
190
212
  const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
191
213
  return this.decryptNode(encryptedNode);
192
214
  }
193
215
 
194
- private async *loadNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
195
- for await (const result of this.loadNodesWithMissingReport(nodeUids, signal)) {
216
+ private async *loadNodes(
217
+ nodeUids: string[],
218
+ filterOptions?: FilterOptions,
219
+ signal?: AbortSignal,
220
+ ): AsyncGenerator<DecryptedNode> {
221
+ for await (const result of this.loadNodesWithMissingReport(nodeUids, filterOptions, signal)) {
196
222
  if ('missingUid' in result) {
197
223
  continue;
198
224
  }
@@ -202,14 +228,15 @@ export class NodesAccess {
202
228
 
203
229
  private async *loadNodesWithMissingReport(
204
230
  nodeUids: string[],
231
+ filterOptions?: FilterOptions,
205
232
  signal?: AbortSignal,
206
233
  ): AsyncGenerator<DecryptedNode | MissingNode> {
207
234
  const returnedNodeUids: string[] = [];
208
235
  const errors = [];
209
236
 
210
- const { volumeId: ownVolumeId } = await this.shareService.getMyFilesIDs();
237
+ const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
211
238
 
212
- const encryptedNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, signal);
239
+ const encryptedNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
213
240
  const decryptNodeMapper = async (encryptedNode: EncryptedNode): Promise<Result<DecryptedNode, unknown>> => {
214
241
  returnedNodeUids.push(encryptedNode.uid);
215
242
  try {