@protontech/drive-sdk 0.3.2 → 0.4.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 (171) hide show
  1. package/dist/interface/nodes.d.ts +4 -0
  2. package/dist/interface/nodes.js.map +1 -1
  3. package/dist/internal/apiService/errorCodes.d.ts +1 -0
  4. package/dist/internal/apiService/errors.d.ts +3 -0
  5. package/dist/internal/apiService/errors.js +7 -1
  6. package/dist/internal/apiService/errors.js.map +1 -1
  7. package/dist/internal/devices/interface.d.ts +1 -1
  8. package/dist/internal/devices/manager.js +1 -1
  9. package/dist/internal/devices/manager.js.map +1 -1
  10. package/dist/internal/devices/manager.test.js +3 -3
  11. package/dist/internal/devices/manager.test.js.map +1 -1
  12. package/dist/internal/events/apiService.js +1 -1
  13. package/dist/internal/events/apiService.js.map +1 -1
  14. package/dist/internal/events/coreEventManager.js +1 -1
  15. package/dist/internal/events/coreEventManager.js.map +1 -1
  16. package/dist/internal/events/coreEventManager.test.js +18 -24
  17. package/dist/internal/events/coreEventManager.test.js.map +1 -1
  18. package/dist/internal/events/index.d.ts +3 -4
  19. package/dist/internal/events/index.js +4 -4
  20. package/dist/internal/events/index.js.map +1 -1
  21. package/dist/internal/events/interface.d.ts +3 -0
  22. package/dist/internal/nodes/apiService.d.ts +12 -3
  23. package/dist/internal/nodes/apiService.js +54 -13
  24. package/dist/internal/nodes/apiService.js.map +1 -1
  25. package/dist/internal/nodes/apiService.test.js +35 -2
  26. package/dist/internal/nodes/apiService.test.js.map +1 -1
  27. package/dist/internal/nodes/cache.test.js +1 -0
  28. package/dist/internal/nodes/cache.test.js.map +1 -1
  29. package/dist/internal/nodes/cryptoService.d.ts +1 -1
  30. package/dist/internal/nodes/cryptoService.js +1 -1
  31. package/dist/internal/nodes/cryptoService.js.map +1 -1
  32. package/dist/internal/nodes/cryptoService.test.js +4 -4
  33. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  34. package/dist/internal/nodes/errors.d.ts +4 -0
  35. package/dist/internal/nodes/errors.js +9 -0
  36. package/dist/internal/nodes/errors.js.map +1 -0
  37. package/dist/internal/nodes/extendedAttributes.d.ts +2 -2
  38. package/dist/internal/nodes/extendedAttributes.js +15 -11
  39. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  40. package/dist/internal/nodes/extendedAttributes.test.js +19 -1
  41. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  42. package/dist/internal/nodes/index.test.js +2 -1
  43. package/dist/internal/nodes/index.test.js.map +1 -1
  44. package/dist/internal/nodes/interface.d.ts +5 -1
  45. package/dist/internal/nodes/nodesAccess.d.ts +3 -3
  46. package/dist/internal/nodes/nodesAccess.js +25 -15
  47. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  48. package/dist/internal/nodes/nodesAccess.test.js +48 -8
  49. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  50. package/dist/internal/nodes/nodesManagement.d.ts +2 -0
  51. package/dist/internal/nodes/nodesManagement.js +87 -9
  52. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  53. package/dist/internal/nodes/nodesManagement.test.js +81 -5
  54. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  55. package/dist/internal/photos/albums.d.ts +9 -7
  56. package/dist/internal/photos/albums.js +26 -13
  57. package/dist/internal/photos/albums.js.map +1 -1
  58. package/dist/internal/photos/apiService.d.ts +34 -3
  59. package/dist/internal/photos/apiService.js +96 -3
  60. package/dist/internal/photos/apiService.js.map +1 -1
  61. package/dist/internal/photos/index.d.ts +31 -4
  62. package/dist/internal/photos/index.js +57 -7
  63. package/dist/internal/photos/index.js.map +1 -1
  64. package/dist/internal/photos/interface.d.ts +25 -1
  65. package/dist/internal/photos/shares.d.ts +43 -0
  66. package/dist/internal/photos/shares.js +112 -0
  67. package/dist/internal/photos/shares.js.map +1 -0
  68. package/dist/internal/photos/timeline.d.ts +15 -0
  69. package/dist/internal/photos/timeline.js +22 -0
  70. package/dist/internal/photos/timeline.js.map +1 -0
  71. package/dist/internal/photos/upload.d.ts +59 -0
  72. package/dist/internal/photos/upload.js +104 -0
  73. package/dist/internal/photos/upload.js.map +1 -0
  74. package/dist/internal/shares/manager.d.ts +1 -1
  75. package/dist/internal/shares/manager.js +4 -4
  76. package/dist/internal/shares/manager.js.map +1 -1
  77. package/dist/internal/shares/manager.test.js +7 -7
  78. package/dist/internal/shares/manager.test.js.map +1 -1
  79. package/dist/internal/sharing/interface.d.ts +1 -1
  80. package/dist/internal/sharing/sharingAccess.js +1 -1
  81. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  82. package/dist/internal/sharing/sharingAccess.test.js +1 -1
  83. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  84. package/dist/internal/sharingPublic/apiService.js +2 -0
  85. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  86. package/dist/internal/upload/apiService.d.ts +2 -2
  87. package/dist/internal/upload/apiService.js +1 -1
  88. package/dist/internal/upload/apiService.js.map +1 -1
  89. package/dist/internal/upload/cryptoService.d.ts +2 -2
  90. package/dist/internal/upload/cryptoService.js.map +1 -1
  91. package/dist/internal/upload/fileUploader.d.ts +1 -0
  92. package/dist/internal/upload/fileUploader.js +3 -0
  93. package/dist/internal/upload/fileUploader.js.map +1 -1
  94. package/dist/internal/upload/interface.d.ts +3 -0
  95. package/dist/internal/upload/manager.d.ts +12 -11
  96. package/dist/internal/upload/manager.js +8 -2
  97. package/dist/internal/upload/manager.js.map +1 -1
  98. package/dist/internal/upload/manager.test.js +8 -0
  99. package/dist/internal/upload/manager.test.js.map +1 -1
  100. package/dist/internal/upload/streamUploader.d.ts +34 -24
  101. package/dist/internal/upload/streamUploader.js +7 -4
  102. package/dist/internal/upload/streamUploader.js.map +1 -1
  103. package/dist/internal/upload/streamUploader.test.js +1 -1
  104. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  105. package/dist/protonDriveClient.d.ts +20 -3
  106. package/dist/protonDriveClient.js +23 -4
  107. package/dist/protonDriveClient.js.map +1 -1
  108. package/dist/protonDrivePhotosClient.d.ts +102 -12
  109. package/dist/protonDrivePhotosClient.js +149 -29
  110. package/dist/protonDrivePhotosClient.js.map +1 -1
  111. package/dist/transformers.d.ts +1 -1
  112. package/dist/transformers.js +1 -0
  113. package/dist/transformers.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/interface/nodes.ts +4 -0
  116. package/src/internal/apiService/errorCodes.ts +1 -0
  117. package/src/internal/apiService/errors.ts +6 -0
  118. package/src/internal/devices/interface.ts +1 -1
  119. package/src/internal/devices/manager.test.ts +3 -3
  120. package/src/internal/devices/manager.ts +1 -1
  121. package/src/internal/events/apiService.ts +1 -1
  122. package/src/internal/events/coreEventManager.test.ts +21 -27
  123. package/src/internal/events/coreEventManager.ts +1 -1
  124. package/src/internal/events/index.ts +3 -4
  125. package/src/internal/events/interface.ts +4 -0
  126. package/src/internal/nodes/apiService.test.ts +58 -1
  127. package/src/internal/nodes/apiService.ts +104 -17
  128. package/src/internal/nodes/cache.test.ts +1 -0
  129. package/src/internal/nodes/cryptoService.test.ts +8 -8
  130. package/src/internal/nodes/cryptoService.ts +1 -1
  131. package/src/internal/nodes/errors.ts +5 -0
  132. package/src/internal/nodes/extendedAttributes.test.ts +23 -1
  133. package/src/internal/nodes/extendedAttributes.ts +26 -18
  134. package/src/internal/nodes/index.test.ts +2 -1
  135. package/src/internal/nodes/interface.ts +6 -1
  136. package/src/internal/nodes/nodesAccess.test.ts +68 -8
  137. package/src/internal/nodes/nodesAccess.ts +42 -15
  138. package/src/internal/nodes/nodesManagement.test.ts +100 -5
  139. package/src/internal/nodes/nodesManagement.ts +101 -13
  140. package/src/internal/photos/albums.ts +31 -12
  141. package/src/internal/photos/apiService.ts +159 -4
  142. package/src/internal/photos/index.ts +116 -9
  143. package/src/internal/photos/interface.ts +23 -1
  144. package/src/internal/photos/shares.ts +134 -0
  145. package/src/internal/photos/timeline.ts +24 -0
  146. package/src/internal/photos/upload.ts +209 -0
  147. package/src/internal/shares/manager.test.ts +7 -7
  148. package/src/internal/shares/manager.ts +4 -4
  149. package/src/internal/sharing/interface.ts +1 -1
  150. package/src/internal/sharing/sharingAccess.test.ts +1 -1
  151. package/src/internal/sharing/sharingAccess.ts +1 -1
  152. package/src/internal/sharingPublic/apiService.ts +2 -0
  153. package/src/internal/upload/apiService.ts +3 -3
  154. package/src/internal/upload/cryptoService.ts +2 -2
  155. package/src/internal/upload/fileUploader.ts +12 -0
  156. package/src/internal/upload/interface.ts +3 -0
  157. package/src/internal/upload/manager.test.ts +8 -0
  158. package/src/internal/upload/manager.ts +20 -10
  159. package/src/internal/upload/streamUploader.test.ts +17 -12
  160. package/src/internal/upload/streamUploader.ts +35 -27
  161. package/src/protonDriveClient.ts +33 -4
  162. package/src/protonDrivePhotosClient.ts +251 -32
  163. package/src/transformers.ts +2 -0
  164. package/dist/internal/photos/cache.d.ts +0 -6
  165. package/dist/internal/photos/cache.js +0 -15
  166. package/dist/internal/photos/cache.js.map +0 -1
  167. package/dist/internal/photos/photosTimeline.d.ts +0 -10
  168. package/dist/internal/photos/photosTimeline.js +0 -19
  169. package/dist/internal/photos/photosTimeline.js.map +0 -1
  170. package/src/internal/photos/cache.ts +0 -11
  171. 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);
@@ -491,6 +577,7 @@ function linkToEncryptedNode(
491
577
  // Sharing node metadata
492
578
  shareId: link.Sharing?.ShareID || undefined,
493
579
  isShared: !!link.Sharing,
580
+ isSharedPublicly: !!link.Sharing?.ShareURLID,
494
581
  directRole: isAdmin ? MemberRole.Admin : membershipRole,
495
582
  membership: link.Membership
496
583
  ? {
@@ -21,6 +21,7 @@ function generateNode(
21
21
  type: NodeType.File,
22
22
  mediaType: 'text',
23
23
  isShared: false,
24
+ isSharedPublicly: false,
24
25
  creationTime: new Date(),
25
26
  trashTime: undefined,
26
27
  volumeId: 'volumeId',
@@ -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
+ }
@@ -50,7 +50,7 @@ describe('extended attrbiutes', () => {
50
50
  });
51
51
  });
52
52
 
53
- describe('should generate file attributes', () => {
53
+ describe('should generate file attributes without additional metadata', () => {
54
54
  const testCases: [object, string | undefined][] = [
55
55
  [{}, undefined],
56
56
  [
@@ -82,6 +82,28 @@ describe('extended attrbiutes', () => {
82
82
  });
83
83
  });
84
84
 
85
+ describe('should generate file attributes with additional metadata', () => {
86
+ const testCases: [object, string | undefined][] = [
87
+ [{}, '{"Media":{"Width":100,"Height":100}}'],
88
+ [{ size: undefined }, '{"Media":{"Width":100,"Height":100}}'],
89
+ [{ size: 123 }, '{"Common":{"Size":123},"Media":{"Width":100,"Height":100}}'],
90
+ ];
91
+ testCases.forEach(([input, expectedAttributes]) => {
92
+ it(`should generate ${input}`, () => {
93
+ const output = generateFileExtendedAttributes(input, { Media: { Width: 100, Height: 100 } });
94
+ expect(output).toBe(expectedAttributes);
95
+ });
96
+ });
97
+ });
98
+
99
+ describe('should throw an error if additional metadata contains common attributes', () => {
100
+ it('should throw an error', () => {
101
+ expect(() => generateFileExtendedAttributes({ size: 123 }, { Common: { Hello: 'World' } })).toThrow(
102
+ 'Common attributes are not allowed in additional metadata',
103
+ );
104
+ });
105
+ });
106
+
85
107
  describe('should parses file attributes', () => {
86
108
  const testCases: [Date, string, FileExtendedAttributesParsed][] = [
87
109
  [new Date('2025-01-01'), '', {}],
@@ -83,34 +83,42 @@ export function parseFolderExtendedAttributes(logger: Logger, extendedAttributes
83
83
  }
84
84
  }
85
85
 
86
- export function generateFileExtendedAttributes(options: {
87
- modificationTime?: Date;
88
- size?: number;
89
- blockSizes?: number[];
90
- digests?: {
91
- sha1?: string;
92
- };
93
- }): string | undefined {
86
+ export function generateFileExtendedAttributes(
87
+ common: {
88
+ modificationTime?: Date;
89
+ size?: number;
90
+ blockSizes?: number[];
91
+ digests?: {
92
+ sha1?: string;
93
+ };
94
+ },
95
+ additionalMetadata?: object,
96
+ ): string | undefined {
97
+ if (additionalMetadata && 'Common' in additionalMetadata) {
98
+ throw new Error('Common attributes are not allowed in additional metadata');
99
+ }
100
+
94
101
  const commonAttributes: FileExtendedAttributesSchema['Common'] = {};
95
- if (options.modificationTime) {
96
- commonAttributes.ModificationTime = dateToIsoString(options.modificationTime);
102
+ if (common.modificationTime) {
103
+ commonAttributes.ModificationTime = dateToIsoString(common.modificationTime);
97
104
  }
98
- if (options.size !== undefined) {
99
- commonAttributes.Size = options.size;
105
+ if (common.size !== undefined) {
106
+ commonAttributes.Size = common.size;
100
107
  }
101
- if (options.blockSizes?.length) {
102
- commonAttributes.BlockSizes = options.blockSizes;
108
+ if (common.blockSizes?.length) {
109
+ commonAttributes.BlockSizes = common.blockSizes;
103
110
  }
104
- if (options.digests?.sha1) {
111
+ if (common.digests?.sha1) {
105
112
  commonAttributes.Digests = {
106
- SHA1: options.digests.sha1,
113
+ SHA1: common.digests.sha1,
107
114
  };
108
115
  }
109
- if (!Object.keys(commonAttributes).length) {
116
+ if (!Object.keys(commonAttributes).length && !additionalMetadata) {
110
117
  return undefined;
111
118
  }
112
119
  return JSON.stringify({
113
- Common: commonAttributes,
120
+ ...(Object.keys(commonAttributes).length ? { Common: commonAttributes } : {}),
121
+ ...(additionalMetadata ? { ...additionalMetadata } : {}),
114
122
  });
115
123
  }
116
124
 
@@ -24,6 +24,7 @@ function generateNode(uid: string, parentUid = 'volumeId~root', params: Partial<
24
24
  type: NodeType.File,
25
25
  mediaType: 'text',
26
26
  isShared: false,
27
+ isSharedPublicly: false,
27
28
  creationTime: new Date(),
28
29
  trashTime: undefined,
29
30
  isStale: false,
@@ -52,7 +53,7 @@ describe('nodesModules integration tests', () => {
52
53
  driveCrypto = {};
53
54
  // @ts-expect-error No need to implement all methods for mocking
54
55
  sharesService = {
55
- getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
56
+ getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
56
57
  };
57
58
 
58
59
  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
  */
@@ -34,6 +38,7 @@ interface BaseNode {
34
38
  // Share node metadata
35
39
  shareId?: string;
36
40
  isShared: boolean;
41
+ isSharedPublicly: boolean;
37
42
  directRole: MemberRole;
38
43
  membership?: {
39
44
  role: MemberRole;
@@ -172,7 +177,7 @@ export interface DecryptedRevision extends Revision {
172
177
  * Interface describing the dependencies to the shares module.
173
178
  */
174
179
  export interface SharesService {
175
- getMyFilesIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
180
+ getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
176
181
  getSharePrivateKey(shareId: string): Promise<PrivateKey>;
177
182
  getMyFilesShareMemberEmailKey(): Promise<{
178
183
  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