@protontech/drive-sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/diagnostic/{sdkDiagnosticFull.d.ts → diagnostic.d.ts} +5 -4
  2. package/dist/diagnostic/{sdkDiagnosticFull.js → diagnostic.js} +13 -10
  3. package/dist/diagnostic/diagnostic.js.map +1 -0
  4. package/dist/diagnostic/index.js +2 -4
  5. package/dist/diagnostic/index.js.map +1 -1
  6. package/dist/diagnostic/interface.d.ts +22 -1
  7. package/dist/diagnostic/sdkDiagnostic.d.ts +3 -2
  8. package/dist/diagnostic/sdkDiagnostic.js +79 -7
  9. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  10. package/dist/interface/index.d.ts +2 -2
  11. package/dist/interface/index.js.map +1 -1
  12. package/dist/interface/nodes.d.ts +9 -0
  13. package/dist/interface/telemetry.d.ts +4 -1
  14. package/dist/interface/telemetry.js.map +1 -1
  15. package/dist/interface/upload.d.ts +1 -12
  16. package/dist/internal/apiService/driveTypes.d.ts +2571 -2356
  17. package/dist/internal/download/controller.d.ts +2 -0
  18. package/dist/internal/download/controller.js +15 -1
  19. package/dist/internal/download/controller.js.map +1 -1
  20. package/dist/internal/download/fileDownloader.js +6 -1
  21. package/dist/internal/download/fileDownloader.js.map +1 -1
  22. package/dist/internal/nodes/apiService.d.ts +11 -1
  23. package/dist/internal/nodes/apiService.js +47 -13
  24. package/dist/internal/nodes/apiService.js.map +1 -1
  25. package/dist/internal/nodes/apiService.test.js +61 -3
  26. package/dist/internal/nodes/apiService.test.js.map +1 -1
  27. package/dist/internal/nodes/cryptoService.d.ts +4 -0
  28. package/dist/internal/nodes/cryptoService.js +6 -0
  29. package/dist/internal/nodes/cryptoService.js.map +1 -1
  30. package/dist/internal/nodes/debouncer.d.ts +3 -2
  31. package/dist/internal/nodes/debouncer.js +16 -4
  32. package/dist/internal/nodes/debouncer.js.map +1 -1
  33. package/dist/internal/nodes/debouncer.test.js +20 -12
  34. package/dist/internal/nodes/debouncer.test.js.map +1 -1
  35. package/dist/internal/nodes/extendedAttributes.js +2 -2
  36. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  37. package/dist/internal/nodes/index.d.ts +1 -1
  38. package/dist/internal/nodes/index.js +3 -3
  39. package/dist/internal/nodes/index.js.map +1 -1
  40. package/dist/internal/nodes/index.test.js +1 -1
  41. package/dist/internal/nodes/index.test.js.map +1 -1
  42. package/dist/internal/nodes/nodeName.d.ts +8 -0
  43. package/dist/internal/nodes/nodeName.js +30 -0
  44. package/dist/internal/nodes/nodeName.js.map +1 -0
  45. package/dist/internal/nodes/nodeName.test.d.ts +1 -0
  46. package/dist/internal/nodes/nodeName.test.js +50 -0
  47. package/dist/internal/nodes/nodeName.test.js.map +1 -0
  48. package/dist/internal/nodes/nodesAccess.d.ts +5 -4
  49. package/dist/internal/nodes/nodesAccess.js +6 -5
  50. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  51. package/dist/internal/nodes/nodesAccess.test.js +17 -5
  52. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  53. package/dist/internal/nodes/nodesManagement.d.ts +3 -2
  54. package/dist/internal/nodes/nodesManagement.js +35 -4
  55. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  56. package/dist/internal/nodes/nodesManagement.test.js +64 -1
  57. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  58. package/dist/internal/photos/apiService.js +9 -20
  59. package/dist/internal/photos/apiService.js.map +1 -1
  60. package/dist/internal/photos/index.d.ts +2 -0
  61. package/dist/internal/photos/index.js +4 -0
  62. package/dist/internal/photos/index.js.map +1 -1
  63. package/dist/internal/photos/upload.js +7 -1
  64. package/dist/internal/photos/upload.js.map +1 -1
  65. package/dist/internal/shares/index.d.ts +1 -0
  66. package/dist/internal/shares/index.js +3 -0
  67. package/dist/internal/shares/index.js.map +1 -1
  68. package/dist/internal/shares/interface.d.ts +8 -0
  69. package/dist/internal/shares/interface.js +10 -1
  70. package/dist/internal/shares/interface.js.map +1 -1
  71. package/dist/internal/sharing/apiService.d.ts +4 -2
  72. package/dist/internal/sharing/apiService.js +18 -14
  73. package/dist/internal/sharing/apiService.js.map +1 -1
  74. package/dist/internal/sharing/index.d.ts +2 -1
  75. package/dist/internal/sharing/index.js +6 -2
  76. package/dist/internal/sharing/index.js.map +1 -1
  77. package/dist/internal/sharing/sharingManagement.d.ts +4 -1
  78. package/dist/internal/sharing/sharingManagement.js +7 -4
  79. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  80. package/dist/internal/sharingPublic/apiService.d.ts +8 -10
  81. package/dist/internal/sharingPublic/apiService.js +9 -63
  82. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  83. package/dist/internal/sharingPublic/index.d.ts +3 -3
  84. package/dist/internal/sharingPublic/index.js +7 -12
  85. package/dist/internal/sharingPublic/index.js.map +1 -1
  86. package/dist/internal/sharingPublic/nodes.d.ts +13 -8
  87. package/dist/internal/sharingPublic/nodes.js +20 -2
  88. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  89. package/dist/internal/sharingPublic/session/apiService.d.ts +7 -5
  90. package/dist/internal/sharingPublic/session/apiService.js +25 -4
  91. package/dist/internal/sharingPublic/session/apiService.js.map +1 -1
  92. package/dist/internal/sharingPublic/session/interface.d.ts +17 -0
  93. package/dist/internal/sharingPublic/session/manager.d.ts +12 -4
  94. package/dist/internal/sharingPublic/session/manager.js +14 -4
  95. package/dist/internal/sharingPublic/session/manager.js.map +1 -1
  96. package/dist/internal/sharingPublic/session/session.d.ts +5 -2
  97. package/dist/internal/sharingPublic/session/session.js +7 -3
  98. package/dist/internal/sharingPublic/session/session.js.map +1 -1
  99. package/dist/internal/sharingPublic/shares.d.ts +3 -10
  100. package/dist/internal/sharingPublic/shares.js +10 -33
  101. package/dist/internal/sharingPublic/shares.js.map +1 -1
  102. package/dist/internal/upload/apiService.d.ts +0 -9
  103. package/dist/internal/upload/apiService.js +0 -16
  104. package/dist/internal/upload/apiService.js.map +1 -1
  105. package/dist/internal/upload/controller.d.ts +3 -1
  106. package/dist/internal/upload/controller.js +16 -2
  107. package/dist/internal/upload/controller.js.map +1 -1
  108. package/dist/internal/upload/cryptoService.d.ts +0 -4
  109. package/dist/internal/upload/cryptoService.js +0 -6
  110. package/dist/internal/upload/cryptoService.js.map +1 -1
  111. package/dist/internal/upload/fileUploader.d.ts +0 -1
  112. package/dist/internal/upload/fileUploader.js +2 -6
  113. package/dist/internal/upload/fileUploader.js.map +1 -1
  114. package/dist/internal/upload/manager.d.ts +0 -1
  115. package/dist/internal/upload/manager.js +0 -51
  116. package/dist/internal/upload/manager.js.map +1 -1
  117. package/dist/internal/upload/manager.test.js +0 -61
  118. package/dist/internal/upload/manager.test.js.map +1 -1
  119. package/dist/internal/upload/streamUploader.d.ts +4 -3
  120. package/dist/internal/upload/streamUploader.js +61 -18
  121. package/dist/internal/upload/streamUploader.js.map +1 -1
  122. package/dist/internal/upload/streamUploader.test.js +38 -12
  123. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  124. package/dist/protonDriveClient.d.ts +24 -4
  125. package/dist/protonDriveClient.js +26 -7
  126. package/dist/protonDriveClient.js.map +1 -1
  127. package/dist/protonDrivePhotosClient.d.ts +100 -4
  128. package/dist/protonDrivePhotosClient.js +160 -9
  129. package/dist/protonDrivePhotosClient.js.map +1 -1
  130. package/dist/protonDrivePublicLinkClient.d.ts +4 -3
  131. package/dist/protonDrivePublicLinkClient.js +2 -2
  132. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  133. package/dist/tests/telemetry.d.ts +4 -2
  134. package/dist/tests/telemetry.js +3 -1
  135. package/dist/tests/telemetry.js.map +1 -1
  136. package/dist/transformers.d.ts +3 -2
  137. package/dist/transformers.js +6 -0
  138. package/dist/transformers.js.map +1 -1
  139. package/package.json +1 -1
  140. package/src/diagnostic/{sdkDiagnosticFull.ts → diagnostic.ts} +10 -6
  141. package/src/diagnostic/index.ts +3 -5
  142. package/src/diagnostic/interface.ts +39 -0
  143. package/src/diagnostic/sdkDiagnostic.ts +110 -9
  144. package/src/interface/index.ts +2 -1
  145. package/src/interface/nodes.ts +3 -0
  146. package/src/interface/telemetry.ts +5 -0
  147. package/src/interface/upload.ts +1 -13
  148. package/src/internal/apiService/driveTypes.ts +2698 -2529
  149. package/src/internal/download/controller.ts +13 -1
  150. package/src/internal/download/fileDownloader.ts +8 -1
  151. package/src/internal/nodes/apiService.test.ts +65 -1
  152. package/src/internal/nodes/apiService.ts +80 -17
  153. package/src/internal/nodes/cryptoService.ts +9 -0
  154. package/src/internal/nodes/debouncer.test.ts +25 -13
  155. package/src/internal/nodes/debouncer.ts +20 -4
  156. package/src/internal/nodes/extendedAttributes.ts +2 -2
  157. package/src/internal/nodes/index.test.ts +1 -0
  158. package/src/internal/nodes/index.ts +3 -9
  159. package/src/internal/nodes/nodeName.test.ts +57 -0
  160. package/src/internal/nodes/nodeName.ts +26 -0
  161. package/src/internal/nodes/nodesAccess.test.ts +17 -5
  162. package/src/internal/nodes/nodesAccess.ts +15 -5
  163. package/src/internal/nodes/nodesManagement.test.ts +68 -1
  164. package/src/internal/nodes/nodesManagement.ts +54 -6
  165. package/src/internal/photos/apiService.ts +12 -29
  166. package/src/internal/photos/index.ts +4 -0
  167. package/src/internal/photos/upload.ts +19 -1
  168. package/src/internal/shares/index.ts +1 -0
  169. package/src/internal/shares/interface.ts +9 -0
  170. package/src/internal/sharing/apiService.ts +17 -14
  171. package/src/internal/sharing/index.ts +7 -1
  172. package/src/internal/sharing/sharingManagement.ts +7 -4
  173. package/src/internal/sharingPublic/apiService.ts +23 -77
  174. package/src/internal/sharingPublic/index.ts +13 -11
  175. package/src/internal/sharingPublic/nodes.ts +33 -11
  176. package/src/internal/sharingPublic/session/apiService.ts +31 -9
  177. package/src/internal/sharingPublic/session/interface.ts +20 -0
  178. package/src/internal/sharingPublic/session/manager.ts +31 -8
  179. package/src/internal/sharingPublic/session/session.ts +10 -5
  180. package/src/internal/sharingPublic/shares.ts +7 -43
  181. package/src/internal/upload/apiService.ts +0 -39
  182. package/src/internal/upload/controller.ts +16 -4
  183. package/src/internal/upload/cryptoService.ts +0 -9
  184. package/src/internal/upload/fileUploader.ts +2 -7
  185. package/src/internal/upload/manager.test.ts +0 -65
  186. package/src/internal/upload/manager.ts +0 -64
  187. package/src/internal/upload/streamUploader.test.ts +46 -14
  188. package/src/internal/upload/streamUploader.ts +74 -21
  189. package/src/protonDriveClient.ts +46 -9
  190. package/src/protonDrivePhotosClient.ts +193 -8
  191. package/src/protonDrivePublicLinkClient.ts +7 -4
  192. package/src/tests/telemetry.ts +6 -3
  193. package/src/transformers.ts +8 -0
  194. package/dist/diagnostic/sdkDiagnosticFull.js.map +0 -1
  195. package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -15
  196. package/dist/internal/sharingPublic/cryptoCache.js +0 -44
  197. package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
  198. package/dist/internal/sharingPublic/cryptoService.d.ts +0 -8
  199. package/dist/internal/sharingPublic/cryptoService.js +0 -19
  200. package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
  201. package/dist/internal/sharingPublic/interface.d.ts +0 -5
  202. package/dist/internal/sharingPublic/interface.js +0 -3
  203. package/dist/internal/sharingPublic/interface.js.map +0 -1
  204. package/src/internal/sharingPublic/cryptoCache.ts +0 -45
  205. package/src/internal/sharingPublic/cryptoService.ts +0 -22
  206. package/src/internal/sharingPublic/interface.ts +0 -5
@@ -50,7 +50,7 @@ describe('NodesManagement', () => {
50
50
  apiService = {
51
51
  renameNode: jest.fn(),
52
52
  moveNode: jest.fn(),
53
- copyNode: jest.fn(),
53
+ copyNode: jest.fn().mockResolvedValue('newCopiedNodeUid'),
54
54
  trashNodes: jest.fn(async function* (uids) {
55
55
  yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
56
56
  }),
@@ -61,6 +61,10 @@ describe('NodesManagement', () => {
61
61
  yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
62
62
  }),
63
63
  createFolder: jest.fn(),
64
+ checkAvailableHashes: jest.fn().mockResolvedValue({
65
+ availableHashes: ['name1Hash'],
66
+ pendingHashes: [],
67
+ }),
64
68
  };
65
69
  // @ts-expect-error No need to implement all methods for mocking
66
70
  cryptoCache = {
@@ -75,6 +79,20 @@ describe('NodesManagement', () => {
75
79
  }),
76
80
  encryptNodeWithNewParent: jest.fn(),
77
81
  createFolder: jest.fn(),
82
+ generateNameHashes: jest.fn().mockResolvedValue([
83
+ {
84
+ name: 'name1',
85
+ hash: 'name1Hash',
86
+ },
87
+ {
88
+ name: 'name2',
89
+ hash: 'name2Hash',
90
+ },
91
+ {
92
+ name: 'name3',
93
+ hash: 'name3Hash',
94
+ },
95
+ ]),
78
96
  };
79
97
  // @ts-expect-error No need to implement all methods for mocking
80
98
  nodesAccess = {
@@ -251,6 +269,7 @@ describe('NodesManagement', () => {
251
269
 
252
270
  expect(newNode).toEqual({
253
271
  ...nodes.nodeUid,
272
+ uid: 'newCopiedNodeUid',
254
273
  parentUid: 'newParentNodeUid',
255
274
  encryptedName: 'copiedArmoredNodeName',
256
275
  hash: 'copiedHash',
@@ -307,6 +326,7 @@ describe('NodesManagement', () => {
307
326
  );
308
327
  expect(newNode).toEqual({
309
328
  ...nodes.anonymousNodeUid,
329
+ uid: 'newCopiedNodeUid',
310
330
  parentUid: 'newParentNodeUid',
311
331
  encryptedName: 'copiedArmoredNodeName',
312
332
  hash: 'copiedHash',
@@ -338,4 +358,51 @@ describe('NodesManagement', () => {
338
358
  expect(restored).toEqual(new Set(uids));
339
359
  expect(nodesAccess.notifyNodeChanged).toHaveBeenCalledTimes(2);
340
360
  });
361
+
362
+ describe('findAvailableName', () => {
363
+ it('should find available name', async () => {
364
+ apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
365
+ return {
366
+ availableHashes: ['name3Hash'],
367
+ pendingHashes: [],
368
+ };
369
+ });
370
+
371
+ const result = await management.findAvailableName('parentUid', 'name');
372
+ expect(result).toBe('name3');
373
+ expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(1);
374
+ expect(apiService.checkAvailableHashes).toHaveBeenCalledWith('parentUid', [
375
+ 'name1Hash',
376
+ 'name2Hash',
377
+ 'name3Hash',
378
+ ]);
379
+ });
380
+
381
+ it('should find available name with multiple pages', async () => {
382
+ let firstCall = false;
383
+ apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
384
+ if (!firstCall) {
385
+ firstCall = true;
386
+ return {
387
+ // First page has no available hashes
388
+ availableHashes: [],
389
+ pendingHashes: [],
390
+ };
391
+ }
392
+ return {
393
+ availableHashes: ['name3Hash'],
394
+ pendingHashes: [],
395
+ };
396
+ });
397
+
398
+ const result = await management.findAvailableName('parentUid', 'name');
399
+ expect(result).toBe('name3');
400
+ expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(2);
401
+ expect(apiService.checkAvailableHashes).toHaveBeenCalledWith('parentUid', [
402
+ 'name1Hash',
403
+ 'name2Hash',
404
+ 'name3Hash',
405
+ ]);
406
+ });
407
+ });
341
408
  });
@@ -1,6 +1,6 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { MemberRole, NodeType, NodeResult, resultOk } from '../../interface';
3
+ import { MemberRole, NodeType, NodeResult, NodeResultWithNewUid, resultOk } from '../../interface';
4
4
  import { AbortError, ValidationError } from '../../errors';
5
5
  import { getErrorMessage } from '../errors';
6
6
  import { splitNodeUid } from '../uids';
@@ -8,10 +8,14 @@ import { NodeAPIService } from './apiService';
8
8
  import { NodesCryptoCache } from './cryptoCache';
9
9
  import { NodesCryptoService } from './cryptoService';
10
10
  import { NodeOutOfSyncError } from './errors';
11
+ import { generateFolderExtendedAttributes } from './extendedAttributes';
11
12
  import { DecryptedNode } from './interface';
13
+ import { splitExtension, joinNameAndExtension } from './nodeName';
12
14
  import { NodesAccess } from './nodesAccess';
13
15
  import { validateNodeName } from './validations';
14
- import { generateFolderExtendedAttributes } from './extendedAttributes';
16
+
17
+ const AVAILABLE_NAME_BATCH_SIZE = 10;
18
+ const AVAILABLE_NAME_LIMIT = 1000;
15
19
 
16
20
  /**
17
21
  * Provides high-level actions for managing nodes.
@@ -182,16 +186,21 @@ export class NodesManagement {
182
186
  return newNode;
183
187
  }
184
188
 
185
- // Improvement requested: copy nodes in parallel
186
- async *copyNodes(nodeUids: string[], newParentNodeUid: string, signal?: AbortSignal): AsyncGenerator<NodeResult> {
189
+ // Improvement requested: copy nodes in parallel using copy_multiple endpoint
190
+ async *copyNodes(
191
+ nodeUids: string[],
192
+ newParentNodeUid: string,
193
+ signal?: AbortSignal,
194
+ ): AsyncGenerator<NodeResultWithNewUid> {
187
195
  for (const nodeUid of nodeUids) {
188
196
  if (signal?.aborted) {
189
197
  throw new AbortError(c('Error').t`Copy operation aborted`);
190
198
  }
191
199
  try {
192
- await this.copyNode(nodeUid, newParentNodeUid);
200
+ const { uid: newNodeUid } = await this.copyNode(nodeUid, newParentNodeUid);
193
201
  yield {
194
202
  uid: nodeUid,
203
+ newUid: newNodeUid,
195
204
  ok: true,
196
205
  };
197
206
  } catch (error: unknown) {
@@ -237,7 +246,7 @@ export class NodesManagement {
237
246
  signatureEmail: encryptedCrypto.signatureEmail,
238
247
  armoredNodePassphraseSignature: encryptedCrypto.armoredNodePassphraseSignature,
239
248
  };
240
- await this.apiService.copyNode(nodeUid, {
249
+ const newNodeUid = await this.apiService.copyNode(nodeUid, {
241
250
  ...keySignatureProperties,
242
251
  parentUid: newParentUid,
243
252
  armoredNodePassphrase: encryptedCrypto.armoredNodePassphrase,
@@ -247,6 +256,7 @@ export class NodesManagement {
247
256
  });
248
257
  const newNode: DecryptedNode = {
249
258
  ...node,
259
+ uid: newNodeUid,
250
260
  encryptedName: encryptedCrypto.encryptedName,
251
261
  parentUid: newParentUid,
252
262
  hash: encryptedCrypto.hash,
@@ -343,4 +353,42 @@ export class NodesManagement {
343
353
  await this.cryptoCache.setNodeKeys(nodeUid, keys);
344
354
  return node;
345
355
  }
356
+
357
+ async findAvailableName(parentFolderUid: string, name: string): Promise<string> {
358
+ const { hashKey: parentHashKey } = await this.nodesAccess.getNodeKeys(parentFolderUid);
359
+ if (!parentHashKey) {
360
+ throw new ValidationError(c('Error').t`Creating files in non-folders is not allowed`);
361
+ }
362
+
363
+ const [namePart, extension] = splitExtension(name);
364
+
365
+ let startIndex = 1;
366
+ while (startIndex < AVAILABLE_NAME_LIMIT) {
367
+ const namesToCheck = startIndex === 1 ? [name] : [];
368
+ for (let i = startIndex; i < startIndex + AVAILABLE_NAME_BATCH_SIZE; i++) {
369
+ namesToCheck.push(joinNameAndExtension(namePart, i, extension));
370
+ }
371
+
372
+ const hashesToCheck = await this.cryptoService.generateNameHashes(parentHashKey, namesToCheck);
373
+
374
+ const { availableHashes } = await this.apiService.checkAvailableHashes(
375
+ parentFolderUid,
376
+ hashesToCheck.map(({ hash }) => hash),
377
+ );
378
+
379
+ if (!availableHashes.length) {
380
+ startIndex += AVAILABLE_NAME_BATCH_SIZE;
381
+ continue;
382
+ }
383
+
384
+ const availableHash = hashesToCheck.find(({ hash }) => hash === availableHashes[0]);
385
+ if (!availableHash) {
386
+ throw Error('Backend returned unexpected hash');
387
+ }
388
+
389
+ return availableHash.name;
390
+ }
391
+
392
+ throw new ValidationError(c('Error').t`No available name found`);
393
+ }
346
394
  }
@@ -1,12 +1,9 @@
1
- import { c } from 'ttag';
2
-
3
- import { DriveAPIService, drivePaths, NotFoundAPIError } from '../apiService';
1
+ import { DriveAPIService, drivePaths } from '../apiService';
4
2
  import { EncryptedRootShare, EncryptedShareCrypto, ShareType } from '../shares/interface';
5
3
  import { makeNodeUid } from '../uids';
6
4
 
7
- type GetVolumesResponse = drivePaths['/drive/volumes']['get']['responses']['200']['content']['application/json'];
8
-
9
- type GetShareResponse = drivePaths['/drive/shares/{shareID}']['get']['responses']['200']['content']['application/json'];
5
+ type GetPhotoShareResponse =
6
+ drivePaths['/drive/v2/shares/photos']['get']['responses']['200']['content']['application/json'];
10
7
 
11
8
  type PostCreateVolumeRequest = Extract<
12
9
  drivePaths['/drive/photos/volumes']['post']['requestBody'],
@@ -34,33 +31,19 @@ export class PhotosAPIService {
34
31
  }
35
32
 
36
33
  async getPhotoShare(): Promise<EncryptedRootShare> {
37
- // TODO: Switch to drive/v2/shares/photos once available.
38
-
39
- const volumesResponse = await this.apiService.get<GetVolumesResponse>('drive/volumes');
40
-
41
- const photoVolume = volumesResponse.Volumes.find((volume) => volume.Type === 2);
42
-
43
- if (!photoVolume) {
44
- throw new NotFoundAPIError(c('Error').t`Photo volume not found`);
45
- }
46
-
47
- const response = await this.apiService.get<GetShareResponse>(`drive/shares/${photoVolume.Share.ShareID}`);
48
-
49
- if (!response.AddressID) {
50
- throw new Error('Photo root share has not address ID set');
51
- }
34
+ const response = await this.apiService.get<GetPhotoShareResponse>('drive/v2/shares/photos');
52
35
 
53
36
  return {
54
- volumeId: response.VolumeID,
55
- shareId: response.ShareID,
56
- rootNodeId: response.LinkID,
57
- creatorEmail: response.Creator,
37
+ volumeId: response.Volume.VolumeID,
38
+ shareId: response.Share.ShareID,
39
+ rootNodeId: response.Link.Link.LinkID,
40
+ creatorEmail: response.Share.CreatorEmail,
58
41
  encryptedCrypto: {
59
- armoredKey: response.Key,
60
- armoredPassphrase: response.Passphrase,
61
- armoredPassphraseSignature: response.PassphraseSignature,
42
+ armoredKey: response.Share.Key,
43
+ armoredPassphrase: response.Share.Passphrase,
44
+ armoredPassphraseSignature: response.Share.PassphraseSignature,
62
45
  },
63
- addressId: response.AddressID,
46
+ addressId: response.Share.AddressID,
64
47
  type: ShareType.Photo,
65
48
  };
66
49
  }
@@ -24,6 +24,10 @@ import {
24
24
  PhotoUploadManager,
25
25
  PhotoUploadMetadata,
26
26
  } from './upload';
27
+ import { ShareTargetType } from '../shares';
28
+
29
+ // Only photos and albums can be shared in photos volume.
30
+ export const PHOTOS_SHARE_TARGET_TYPES = [ShareTargetType.Photo, ShareTargetType.Album];
27
31
 
28
32
  /**
29
33
  * Provides facade for the whole photos module.
@@ -85,7 +85,25 @@ export class PhotoStreamUploader extends StreamUploader {
85
85
  controller: UploadController,
86
86
  signal?: AbortSignal,
87
87
  ) {
88
- super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, controller, signal);
88
+ const abortController = new AbortController();
89
+ if (signal) {
90
+ signal.addEventListener('abort', () => {
91
+ abortController.abort();
92
+ });
93
+ }
94
+
95
+ super(
96
+ telemetry,
97
+ apiService,
98
+ cryptoService,
99
+ uploadManager,
100
+ blockVerifier,
101
+ revisionDraft,
102
+ metadata,
103
+ onFinish,
104
+ controller,
105
+ abortController,
106
+ );
89
107
  this.photoUploadManager = uploadManager;
90
108
  this.photoMetadata = metadata;
91
109
  }
@@ -12,6 +12,7 @@ import { SharesCache } from './cache';
12
12
  import { SharesCryptoService } from './cryptoService';
13
13
  import { SharesManager } from './manager';
14
14
 
15
+ export { ShareTargetType } from './interface';
15
16
  export type { EncryptedShare } from './interface';
16
17
 
17
18
  /**
@@ -1,6 +1,15 @@
1
1
  import { PrivateKey, SessionKey } from '../../crypto';
2
2
  import { Result, UnverifiedAuthorError } from '../../interface';
3
3
 
4
+ export enum ShareTargetType {
5
+ Root = 0,
6
+ Folder = 1,
7
+ File = 2,
8
+ Album = 3,
9
+ Photo = 4,
10
+ ProtonVendor = 5,
11
+ }
12
+
4
13
  /**
5
14
  * Internal interface providing basic identification of volume and its root
6
15
  * share and node.
@@ -7,6 +7,7 @@ import {
7
7
  permissionsToMemberRole,
8
8
  memberRoleToPermission,
9
9
  } from '../apiService';
10
+ import { ShareTargetType } from '../shares';
10
11
  import {
11
12
  makeNodeUid,
12
13
  splitNodeUid,
@@ -119,14 +120,6 @@ type PutShareUrlRequest = Extract<
119
120
  type PutShareUrlResponse =
120
121
  drivePaths['/drive/shares/{shareID}/urls/{urlID}']['put']['responses']['200']['content']['application/json'];
121
122
 
122
- // We do not support photos and albums yet.
123
- const SUPPORTED_SHARE_TARGET_TYPES = [
124
- 0, // Root
125
- 1, // Folder
126
- 2, // File
127
- 5, // Proton vendor (documents and sheets)
128
- ];
129
-
130
123
  /**
131
124
  * Provides API communication for fetching and managing sharing.
132
125
  *
@@ -137,9 +130,11 @@ export class SharingAPIService {
137
130
  constructor(
138
131
  private logger: Logger,
139
132
  private apiService: DriveAPIService,
133
+ private shareTargetTypes: ShareTargetType[],
140
134
  ) {
141
135
  this.logger = logger;
142
136
  this.apiService = apiService;
137
+ this.shareTargetTypes = shareTargetTypes;
143
138
  }
144
139
 
145
140
  async *iterateSharedNodeUids(volumeId: string, signal?: AbortSignal): AsyncGenerator<string> {
@@ -163,6 +158,7 @@ export class SharingAPIService {
163
158
  async *iterateSharedWithMeNodeUids(signal?: AbortSignal): AsyncGenerator<string> {
164
159
  let anchor = '';
165
160
  while (true) {
161
+ // TODO: Use ShareTargetTypes filter when it is supported by the API.
166
162
  const response = await this.apiService.get<GetSharedWithMeNodesResponse>(
167
163
  `drive/v2/sharedwithme?${anchor ? `AnchorID=${anchor}` : ''}`,
168
164
  signal,
@@ -170,8 +166,8 @@ export class SharingAPIService {
170
166
  for (const link of response.Links) {
171
167
  const nodeUid = makeNodeUid(link.VolumeID, link.LinkID);
172
168
 
173
- if (!SUPPORTED_SHARE_TARGET_TYPES.includes(link.ShareTargetType)) {
174
- this.logger.warn(`Unsupported share target type ${link.ShareTargetType} for node ${nodeUid}`);
169
+ if (!this.shareTargetTypes.includes(link.ShareTargetType)) {
170
+ this.logger.debug(`Unsupported share target type ${link.ShareTargetType} for node ${nodeUid}`);
175
171
  continue;
176
172
  }
177
173
 
@@ -188,14 +184,21 @@ export class SharingAPIService {
188
184
  async *iterateInvitationUids(signal?: AbortSignal): AsyncGenerator<string> {
189
185
  let anchor = '';
190
186
  while (true) {
187
+ const params = new URLSearchParams();
188
+ this.shareTargetTypes.forEach((type) => {
189
+ params.append('ShareTargetTypes[]', type.toString());
190
+ });
191
+ if (anchor) {
192
+ params.append('AnchorID', anchor);
193
+ }
191
194
  const response = await this.apiService.get<GetInvitationsResponse>(
192
- `drive/v2/shares/invitations?${anchor ? `AnchorID=${anchor}` : ''}`,
195
+ `drive/v2/shares/invitations?${params.toString()}`,
193
196
  signal,
194
197
  );
195
198
  for (const invitation of response.Invitations) {
196
199
  const invitationUid = makeInvitationUid(invitation.ShareID, invitation.InvitationID);
197
200
 
198
- if (!SUPPORTED_SHARE_TARGET_TYPES.includes(invitation.ShareTargetType)) {
201
+ if (!this.shareTargetTypes.includes(invitation.ShareTargetType)) {
199
202
  this.logger.warn(
200
203
  `Unsupported share target type ${invitation.ShareTargetType} for invitation ${invitationUid}`,
201
204
  );
@@ -347,8 +350,8 @@ export class SharingAPIService {
347
350
  return response.Share.ID;
348
351
  }
349
352
 
350
- async deleteShare(shareId: string): Promise<void> {
351
- await this.apiService.delete(`drive/shares/${shareId}?Force=1`);
353
+ async deleteShare(shareId: string, force: boolean = false): Promise<void> {
354
+ await this.apiService.delete(`drive/shares/${shareId}?Force=${force ? 1 : 0}`);
352
355
  }
353
356
 
354
357
  async inviteProtonUser(
@@ -1,6 +1,7 @@
1
1
  import { ProtonDriveAccount, ProtonDriveEntitiesCache, ProtonDriveTelemetry } from '../../interface';
2
2
  import { DriveCrypto } from '../../crypto';
3
3
  import { DriveAPIService } from '../apiService';
4
+ import { ShareTargetType } from '../shares';
4
5
  import { SharingAPIService } from './apiService';
5
6
  import { SharingCache } from './cache';
6
7
  import { SharingCryptoService } from './cryptoService';
@@ -9,6 +10,10 @@ import { SharingManagement } from './sharingManagement';
9
10
  import { SharesService, NodesService } from './interface';
10
11
  import { SharingEventHandler } from './events';
11
12
 
13
+ // Root shares are not allowed to be shared.
14
+ // Photos and Albums are not supported in main volume (core Drive).
15
+ const DEFAULT_SHARE_TARGET_TYPES = [ShareTargetType.Folder, ShareTargetType.File, ShareTargetType.ProtonVendor];
16
+
12
17
  /**
13
18
  * Provides facade for the whole sharing module.
14
19
  *
@@ -24,8 +29,9 @@ export function initSharingModule(
24
29
  crypto: DriveCrypto,
25
30
  sharesService: SharesService,
26
31
  nodesService: NodesService,
32
+ shareTargetTypes: ShareTargetType[] = DEFAULT_SHARE_TARGET_TYPES,
27
33
  ) {
28
- const api = new SharingAPIService(telemetry.getLogger('sharing-api'), apiService);
34
+ const api = new SharingAPIService(telemetry.getLogger('sharing-api'), apiService, shareTargetTypes);
29
35
  const cache = new SharingCache(driveEntitiesCache);
30
36
  const cryptoService = new SharingCryptoService(telemetry, crypto, account, sharesService);
31
37
  const sharingAccess = new SharingAccess(api, cache, cryptoService, sharesService, nodesService);
@@ -286,7 +286,7 @@ export class SharingManagement {
286
286
 
287
287
  if (!settings) {
288
288
  this.logger.info(`Unsharing node ${nodeUid}`);
289
- await this.deleteShare(currentSharing.share.shareId, nodeUid);
289
+ await this.deleteShareWithForce(currentSharing.share.shareId, nodeUid);
290
290
  return;
291
291
  }
292
292
 
@@ -348,7 +348,7 @@ export class SharingManagement {
348
348
  // update local state immediately.
349
349
  this.logger.info(`Deleting share ${currentSharing.share.shareId} for node ${nodeUid}`);
350
350
  try {
351
- await this.deleteShare(currentSharing.share.shareId, nodeUid);
351
+ await this.deleteShareWithForce(currentSharing.share.shareId, nodeUid);
352
352
  } catch (error: unknown) {
353
353
  // If deleting the share fails, we don't want to throw an error
354
354
  // as it might be a race condition that other client updated
@@ -433,8 +433,11 @@ export class SharingManagement {
433
433
  };
434
434
  }
435
435
 
436
- private async deleteShare(shareId: string, nodeUid: string): Promise<void> {
437
- await this.apiService.deleteShare(shareId);
436
+ /**
437
+ * Deletes the share even if it is not empty.
438
+ */
439
+ private async deleteShareWithForce(shareId: string, nodeUid: string): Promise<void> {
440
+ await this.apiService.deleteShare(shareId, true);
438
441
  await this.nodesService.notifyNodeChanged(nodeUid);
439
442
  if (await this.cache.hasSharedByMeNodeUidsLoaded()) {
440
443
  await this.cache.removeSharedByMeNodeUid(nodeUid);
@@ -1,92 +1,38 @@
1
- import { DriveAPIService, drivePaths, nodeTypeNumberToNodeType } from '../apiService';
2
- import { Logger, MemberRole } from '../../interface';
3
- import { makeNodeUid } from '../uids';
4
- import { EncryptedNode } from '../nodes/interface';
5
- import { EncryptedShareCrypto } from './interface';
1
+ import { DriveAPIService, drivePaths } from '../apiService';
6
2
 
7
- type GetTokenInfoResponse = drivePaths['/drive/urls/{token}']['get']['responses']['200']['content']['application/json'];
3
+ type PostTokenInfoRequest = Extract<
4
+ drivePaths['/drive/v2/urls/{token}/bookmark']['post']['requestBody'],
5
+ { content: object }
6
+ >['content']['application/json'];
7
+ type PostTokenInfoResponse =
8
+ drivePaths['/drive/v2/urls/{token}/bookmark']['post']['responses']['200']['content']['application/json'];
8
9
 
9
10
  /**
10
- * Provides API communication for accessing public link data.
11
+ * Provides API communication for actions on the public link.
11
12
  *
12
13
  * The service is responsible for transforming local objects to API payloads
13
14
  * and vice versa. It should not contain any business logic.
14
15
  */
15
16
  export class SharingPublicAPIService {
16
- constructor(
17
- private logger: Logger,
18
- private apiService: DriveAPIService,
19
- ) {
20
- this.logger = logger;
17
+ constructor(private apiService: DriveAPIService) {
21
18
  this.apiService = apiService;
22
19
  }
23
20
 
24
- async getPublicLinkRoot(token: string): Promise<{
25
- encryptedNode: EncryptedNode;
26
- encryptedShare: EncryptedShareCrypto;
27
- }> {
28
- const response = await this.apiService.get<GetTokenInfoResponse>(`drive/urls/${token}`);
29
- const encryptedNode = tokenToEncryptedNode(this.logger, response.Token);
30
-
31
- return {
32
- encryptedNode: encryptedNode,
33
- encryptedShare: {
34
- base64UrlPasswordSalt: response.Token.SharePasswordSalt,
35
- armoredKey: response.Token.ShareKey,
36
- armoredPassphrase: response.Token.SharePassphrase,
37
- },
38
- };
39
- }
40
- }
41
-
42
- function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token']): EncryptedNode {
43
- const baseNodeMetadata = {
44
- // Internal metadata
45
- encryptedName: token.Name,
46
-
47
- // Basic node metadata
48
- uid: makeNodeUid(token.VolumeID, token.LinkID),
49
- parentUid: undefined,
50
- type: nodeTypeNumberToNodeType(logger, token.LinkType),
51
- creationTime: new Date(), // TODO
52
-
53
- isShared: false,
54
- isSharedPublicly: false,
55
- directRole: MemberRole.Viewer, // TODO
56
- };
57
-
58
- const baseCryptoNodeMetadata = {
59
- signatureEmail: token.SignatureEmail || undefined,
60
- armoredKey: token.NodeKey,
61
- armoredNodePassphrase: token.NodePassphrase,
62
- armoredNodePassphraseSignature: token.NodePassphraseSignature || undefined,
63
- };
64
-
65
- if (token.LinkType === 1 && token.NodeHashKey) {
66
- return {
67
- ...baseNodeMetadata,
68
- encryptedCrypto: {
69
- ...baseCryptoNodeMetadata,
70
- folder: {
71
- armoredHashKey: token.NodeHashKey as string,
21
+ async bookmarkPublicLink(bookmark: {
22
+ token: string;
23
+ encryptedUrlPassword: string;
24
+ addressId: string;
25
+ addressKeyId: string;
26
+ }): Promise<void> {
27
+ await this.apiService.post<PostTokenInfoRequest, PostTokenInfoResponse>(
28
+ `drive/v2/urls/${bookmark.token}/bookmark`,
29
+ {
30
+ BookmarkShareURL: {
31
+ EncryptedUrlPassword: bookmark.encryptedUrlPassword,
32
+ AddressID: bookmark.addressId,
33
+ AddressKeyID: bookmark.addressKeyId,
72
34
  },
73
35
  },
74
- };
36
+ );
75
37
  }
76
-
77
- if (token.LinkType === 2 && token.ContentKeyPacket) {
78
- return {
79
- ...baseNodeMetadata,
80
- totalStorageSize: token.Size || undefined,
81
- mediaType: token.MIMEType || undefined,
82
- encryptedCrypto: {
83
- ...baseCryptoNodeMetadata,
84
- file: {
85
- base64ContentKeyPacket: token.ContentKeyPacket,
86
- },
87
- },
88
- };
89
- }
90
-
91
- throw new Error(`Unknown node type: ${token.LinkType}`);
92
38
  }