@protontech/drive-sdk 0.4.1 → 0.5.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 (130) hide show
  1. package/dist/diagnostic/sdkDiagnostic.js +1 -1
  2. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  3. package/dist/interface/download.d.ts +4 -4
  4. package/dist/interface/upload.d.ts +6 -3
  5. package/dist/internal/apiService/apiService.d.ts +3 -0
  6. package/dist/internal/apiService/apiService.js +25 -2
  7. package/dist/internal/apiService/apiService.js.map +1 -1
  8. package/dist/internal/apiService/apiService.test.js +38 -0
  9. package/dist/internal/apiService/apiService.test.js.map +1 -1
  10. package/dist/internal/apiService/driveTypes.d.ts +31 -48
  11. package/dist/internal/apiService/errors.js +3 -0
  12. package/dist/internal/apiService/errors.js.map +1 -1
  13. package/dist/internal/apiService/errors.test.js +15 -7
  14. package/dist/internal/apiService/errors.test.js.map +1 -1
  15. package/dist/internal/asyncIteratorMap.d.ts +1 -1
  16. package/dist/internal/asyncIteratorMap.js +6 -1
  17. package/dist/internal/asyncIteratorMap.js.map +1 -1
  18. package/dist/internal/asyncIteratorMap.test.js +9 -0
  19. package/dist/internal/asyncIteratorMap.test.js.map +1 -1
  20. package/dist/internal/download/fileDownloader.d.ts +3 -3
  21. package/dist/internal/download/fileDownloader.js +5 -5
  22. package/dist/internal/download/fileDownloader.js.map +1 -1
  23. package/dist/internal/download/fileDownloader.test.js +8 -8
  24. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  25. package/dist/internal/nodes/apiService.d.ts +6 -1
  26. package/dist/internal/nodes/apiService.js +44 -32
  27. package/dist/internal/nodes/apiService.js.map +1 -1
  28. package/dist/internal/nodes/apiService.test.js +148 -17
  29. package/dist/internal/nodes/apiService.test.js.map +1 -1
  30. package/dist/internal/nodes/debouncer.d.ts +23 -0
  31. package/dist/internal/nodes/debouncer.js +80 -0
  32. package/dist/internal/nodes/debouncer.js.map +1 -0
  33. package/dist/internal/nodes/debouncer.test.d.ts +1 -0
  34. package/dist/internal/nodes/debouncer.test.js +100 -0
  35. package/dist/internal/nodes/debouncer.test.js.map +1 -0
  36. package/dist/internal/nodes/nodesAccess.d.ts +2 -1
  37. package/dist/internal/nodes/nodesAccess.js +24 -5
  38. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  39. package/dist/internal/nodes/nodesAccess.test.js +2 -2
  40. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  41. package/dist/internal/photos/upload.d.ts +2 -1
  42. package/dist/internal/photos/upload.js +3 -3
  43. package/dist/internal/photos/upload.js.map +1 -1
  44. package/dist/internal/sharingPublic/apiService.d.ts +2 -2
  45. package/dist/internal/sharingPublic/apiService.js +1 -63
  46. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  47. package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -4
  48. package/dist/internal/sharingPublic/cryptoCache.js +0 -28
  49. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -1
  50. package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
  51. package/dist/internal/sharingPublic/cryptoReporter.js +44 -0
  52. package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
  53. package/dist/internal/sharingPublic/cryptoService.d.ts +3 -4
  54. package/dist/internal/sharingPublic/cryptoService.js +5 -43
  55. package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
  56. package/dist/internal/sharingPublic/index.d.ts +21 -3
  57. package/dist/internal/sharingPublic/index.js +43 -12
  58. package/dist/internal/sharingPublic/index.js.map +1 -1
  59. package/dist/internal/sharingPublic/interface.d.ts +0 -1
  60. package/dist/internal/sharingPublic/nodes.d.ts +13 -0
  61. package/dist/internal/sharingPublic/nodes.js +28 -0
  62. package/dist/internal/sharingPublic/nodes.js.map +1 -0
  63. package/dist/internal/sharingPublic/session/session.d.ts +3 -3
  64. package/dist/internal/sharingPublic/session/url.test.js +3 -3
  65. package/dist/internal/sharingPublic/shares.d.ts +34 -0
  66. package/dist/internal/sharingPublic/shares.js +69 -0
  67. package/dist/internal/sharingPublic/shares.js.map +1 -0
  68. package/dist/internal/upload/apiService.js +10 -1
  69. package/dist/internal/upload/apiService.js.map +1 -1
  70. package/dist/internal/upload/controller.d.ts +8 -2
  71. package/dist/internal/upload/controller.js.map +1 -1
  72. package/dist/internal/upload/fileUploader.d.ts +6 -3
  73. package/dist/internal/upload/fileUploader.js +3 -3
  74. package/dist/internal/upload/fileUploader.js.map +1 -1
  75. package/dist/internal/upload/fileUploader.test.js +23 -11
  76. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  77. package/dist/internal/upload/streamUploader.d.ts +6 -2
  78. package/dist/internal/upload/streamUploader.js +8 -4
  79. package/dist/internal/upload/streamUploader.js.map +1 -1
  80. package/dist/internal/upload/streamUploader.test.js +10 -6
  81. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  82. package/dist/protonDriveClient.d.ts +3 -3
  83. package/dist/protonDriveClient.js +4 -4
  84. package/dist/protonDriveClient.js.map +1 -1
  85. package/dist/protonDrivePublicLinkClient.d.ts +31 -4
  86. package/dist/protonDrivePublicLinkClient.js +52 -9
  87. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/diagnostic/sdkDiagnostic.ts +1 -1
  90. package/src/interface/download.ts +4 -4
  91. package/src/interface/upload.ts +3 -3
  92. package/src/internal/apiService/apiService.test.ts +50 -0
  93. package/src/internal/apiService/apiService.ts +33 -2
  94. package/src/internal/apiService/driveTypes.ts +31 -48
  95. package/src/internal/apiService/errors.test.ts +10 -0
  96. package/src/internal/apiService/errors.ts +5 -1
  97. package/src/internal/asyncIteratorMap.test.ts +12 -0
  98. package/src/internal/asyncIteratorMap.ts +8 -0
  99. package/src/internal/download/fileDownloader.test.ts +8 -8
  100. package/src/internal/download/fileDownloader.ts +5 -5
  101. package/src/internal/nodes/apiService.test.ts +199 -16
  102. package/src/internal/nodes/apiService.ts +62 -49
  103. package/src/internal/nodes/debouncer.test.ts +129 -0
  104. package/src/internal/nodes/debouncer.ts +93 -0
  105. package/src/internal/nodes/nodesAccess.test.ts +2 -2
  106. package/src/internal/nodes/nodesAccess.ts +30 -5
  107. package/src/internal/photos/upload.ts +4 -1
  108. package/src/internal/sharingPublic/apiService.ts +4 -87
  109. package/src/internal/sharingPublic/cryptoCache.ts +0 -34
  110. package/src/internal/sharingPublic/cryptoReporter.ts +73 -0
  111. package/src/internal/sharingPublic/cryptoService.ts +4 -80
  112. package/src/internal/sharingPublic/index.ts +68 -6
  113. package/src/internal/sharingPublic/interface.ts +0 -9
  114. package/src/internal/sharingPublic/nodes.ts +37 -0
  115. package/src/internal/sharingPublic/session/apiService.ts +1 -1
  116. package/src/internal/sharingPublic/session/session.ts +3 -3
  117. package/src/internal/sharingPublic/session/url.test.ts +3 -3
  118. package/src/internal/sharingPublic/shares.ts +86 -0
  119. package/src/internal/upload/apiService.ts +12 -1
  120. package/src/internal/upload/controller.ts +2 -2
  121. package/src/internal/upload/fileUploader.test.ts +25 -11
  122. package/src/internal/upload/fileUploader.ts +4 -3
  123. package/src/internal/upload/streamUploader.test.ts +15 -3
  124. package/src/internal/upload/streamUploader.ts +8 -3
  125. package/src/protonDriveClient.ts +4 -4
  126. package/src/protonDrivePublicLinkClient.ts +93 -12
  127. package/dist/internal/sharingPublic/manager.d.ts +0 -19
  128. package/dist/internal/sharingPublic/manager.js +0 -81
  129. package/dist/internal/sharingPublic/manager.js.map +0 -1
  130. package/src/internal/sharingPublic/manager.ts +0 -86
@@ -0,0 +1,93 @@
1
+ import { Logger } from "../../interface";
2
+ import { LoggerWithPrefix } from '../../telemetry';
3
+
4
+ /**
5
+ * The timeout for which the node is considered to be loading.
6
+ * If the node is not loaded after this timeout, it is considered to be
7
+ * loaded or failed to be loaded, and allowed other places to proceed.
8
+ *
9
+ * Decrypting many nodes in parallel can take a lot of time, so we allow
10
+ * more time for this.
11
+ */
12
+ const DEBOUNCE_TIMEOUT = 5000;
13
+
14
+ /**
15
+ * Helper to avoid loading the same node twice.
16
+ *
17
+ * Each place that loads a node should report it is being loaded,
18
+ * and when it is finished, it should report it is finished.
19
+ * The finish must be called even if the node fails to be loaded
20
+ * to clear the promise.
21
+ *
22
+ * Each place that loads a node from cache should first wait for
23
+ * the node to be loaded if that is the case.
24
+ */
25
+ export class NodesDebouncer {
26
+ private promises: Map<
27
+ string,
28
+ {
29
+ promise: Promise<void>;
30
+ resolve: () => void;
31
+ timeout: NodeJS.Timeout;
32
+ }
33
+ > = new Map();
34
+
35
+ constructor(private logger: Logger) {
36
+ this.logger = new LoggerWithPrefix(logger, 'debouncer');
37
+ }
38
+
39
+ loadingNodes(nodeUids: string[]) {
40
+ for (const nodeUid of nodeUids) {
41
+ this.loadingNode(nodeUid);
42
+ }
43
+ }
44
+
45
+ loadingNode(nodeUid: string) {
46
+ const { promise, resolve } = Promise.withResolvers<void>();
47
+ if (this.promises.has(nodeUid)) {
48
+ this.logger.warn(`Loading twice for: ${nodeUid}`);
49
+ return;
50
+ }
51
+
52
+ const timeout = setTimeout(() => {
53
+ this.logger.warn(`Timeout for: ${nodeUid}`);
54
+ this.finishedLoadingNode(nodeUid);
55
+ }, DEBOUNCE_TIMEOUT);
56
+ this.promises.set(nodeUid, { promise, resolve, timeout });
57
+ }
58
+
59
+ finishedLoadingNodes(nodeUids: string[]) {
60
+ for (const nodeUid of nodeUids) {
61
+ this.finishedLoadingNode(nodeUid);
62
+ }
63
+ }
64
+
65
+ finishedLoadingNode(nodeUid: string) {
66
+ const result = this.promises.get(nodeUid);
67
+ if (!result) {
68
+ return;
69
+ }
70
+
71
+ clearTimeout(result.timeout);
72
+ result.resolve();
73
+ this.promises.delete(nodeUid);
74
+ }
75
+
76
+ async waitForLoadingNode(nodeUid: string) {
77
+ const result = this.promises.get(nodeUid);
78
+ if (!result) {
79
+ return;
80
+ }
81
+
82
+ this.logger.debug(`Wait for: ${nodeUid}`);
83
+ await result.promise;
84
+ }
85
+
86
+ clear() {
87
+ for (const result of this.promises.values()) {
88
+ clearTimeout(result.timeout);
89
+ result.resolve();
90
+ }
91
+ this.promises.clear();
92
+ }
93
+ }
@@ -352,7 +352,7 @@ describe('nodesAccess', () => {
352
352
  expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
353
353
  });
354
354
 
355
- it.only('should return only filtered nodes from API', async () => {
355
+ it('should return only filtered nodes from API', async () => {
356
356
  cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(false);
357
357
  cache.getNode = jest.fn().mockImplementation((uid: string) => {
358
358
  if (uid === parentNode.uid) {
@@ -444,7 +444,7 @@ describe('nodesAccess', () => {
444
444
  const node1 = { uid: 'volumeId~node1', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
445
445
  const node2 = { uid: 'volumeId~node2', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
446
446
  const node3 = { uid: 'volumeId~node3', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
447
- const node4 = { uid: 'volume~node4', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
447
+ const node4 = { uid: 'volumeId~node4', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
448
448
 
449
449
  it('should serve fully from cache', async () => {
450
450
  cache.iterateNodes = jest.fn().mockImplementation(async function* () {
@@ -11,6 +11,7 @@ import { NodeAPIService } from './apiService';
11
11
  import { NodesCache } from './cache';
12
12
  import { NodesCryptoCache } from './cryptoCache';
13
13
  import { NodesCryptoService } from './cryptoService';
14
+ import { NodesDebouncer } from './debouncer';
14
15
  import { parseFileExtendedAttributes, parseFolderExtendedAttributes } from './extendedAttributes';
15
16
  import {
16
17
  SharesService,
@@ -40,13 +41,18 @@ const DECRYPTION_CONCURRENCY = 30;
40
41
  * nodes metadata.
41
42
  */
42
43
  export class NodesAccess {
44
+ private debouncer: NodesDebouncer;
45
+
43
46
  constructor(
44
47
  private logger: Logger,
45
48
  private apiService: NodeAPIService,
46
49
  private cache: NodesCache,
47
50
  private cryptoCache: NodesCryptoCache,
48
51
  private cryptoService: NodesCryptoService,
49
- private shareService: SharesService,
52
+ private shareService: Pick<
53
+ SharesService,
54
+ 'getOwnVolumeIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
55
+ >,
50
56
  ) {
51
57
  this.logger = logger;
52
58
  this.apiService = apiService;
@@ -54,6 +60,7 @@ export class NodesAccess {
54
60
  this.cryptoCache = cryptoCache;
55
61
  this.cryptoService = cryptoService;
56
62
  this.shareService = shareService;
63
+ this.debouncer = new NodesDebouncer(this.logger);
57
64
  }
58
65
 
59
66
  async getVolumeRootFolder() {
@@ -65,6 +72,7 @@ export class NodesAccess {
65
72
  async getNode(nodeUid: string): Promise<DecryptedNode> {
66
73
  let cachedNode;
67
74
  try {
75
+ await this.debouncer.waitForLoadingNode(nodeUid);
68
76
  cachedNode = await this.cache.getNode(nodeUid);
69
77
  } catch {}
70
78
 
@@ -112,6 +120,7 @@ export class NodesAccess {
112
120
  for await (const nodeUid of this.apiService.iterateChildrenNodeUids(parentNode.uid, onlyFolders, signal)) {
113
121
  let node;
114
122
  try {
123
+ await this.debouncer.waitForLoadingNode(nodeUid);
115
124
  node = await this.cache.getNode(nodeUid);
116
125
  } catch {}
117
126
 
@@ -143,6 +152,7 @@ export class NodesAccess {
143
152
  for await (const nodeUid of this.apiService.iterateTrashedNodeUids(volumeId, signal)) {
144
153
  let node;
145
154
  try {
155
+ await this.debouncer.waitForLoadingNode(nodeUid);
146
156
  node = await this.cache.getNode(nodeUid);
147
157
  } catch {}
148
158
 
@@ -208,9 +218,14 @@ export class NodesAccess {
208
218
  }
209
219
 
210
220
  private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
211
- const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
212
- const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
213
- return this.decryptNode(encryptedNode);
221
+ this.debouncer.loadingNode(nodeUid);
222
+ try {
223
+ const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
224
+ const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
225
+ return this.decryptNode(encryptedNode);
226
+ } finally {
227
+ this.debouncer.finishedLoadingNode(nodeUid);
228
+ }
214
229
  }
215
230
 
216
231
  private async *loadNodes(
@@ -236,7 +251,14 @@ export class NodesAccess {
236
251
 
237
252
  const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
238
253
 
239
- const encryptedNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
254
+ const apiNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
255
+
256
+ const debouncedNodeMapper = async (encryptedNode: EncryptedNode): Promise<EncryptedNode> => {
257
+ this.debouncer.loadingNode(encryptedNode.uid);
258
+ return encryptedNode;
259
+ };
260
+ const encryptedNodesIterator = asyncIteratorMap(apiNodesIterator, debouncedNodeMapper, 1);
261
+
240
262
  const decryptNodeMapper = async (encryptedNode: EncryptedNode): Promise<Result<DecryptedNode, unknown>> => {
241
263
  returnedNodeUids.push(encryptedNode.uid);
242
264
  try {
@@ -250,6 +272,7 @@ export class NodesAccess {
250
272
  encryptedNodesIterator,
251
273
  decryptNodeMapper,
252
274
  DECRYPTION_CONCURRENCY,
275
+ signal,
253
276
  );
254
277
  for await (const node of decryptedNodesIterator) {
255
278
  if (node.ok) {
@@ -329,6 +352,7 @@ export class NodesAccess {
329
352
  this.logger.error(`Failed to cache node keys ${node.uid}`, error);
330
353
  }
331
354
  }
355
+ this.debouncer.finishedLoadingNode(node.uid);
332
356
  return { node, keys };
333
357
  }
334
358
 
@@ -360,6 +384,7 @@ export class NodesAccess {
360
384
 
361
385
  async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
362
386
  try {
387
+ await this.debouncer.waitForLoadingNode(nodeUid);
363
388
  return await this.cryptoCache.getNodeKeys(nodeUid);
364
389
  } catch {
365
390
  const { keys } = await this.loadNode(nodeUid);
@@ -5,6 +5,7 @@ import { generateFileExtendedAttributes } from '../nodes';
5
5
  import { splitNodeRevisionUid } from '../uids';
6
6
  import { UploadAPIService } from '../upload/apiService';
7
7
  import { BlockVerifier } from '../upload/blockVerifier';
8
+ import { UploadController } from '../upload/controller';
8
9
  import { UploadCryptoService } from '../upload/cryptoService';
9
10
  import { FileUploader } from '../upload/fileUploader';
10
11
  import { NodeRevisionDraft, NodesService } from '../upload/interface';
@@ -62,6 +63,7 @@ export class PhotoFileUploader extends FileUploader {
62
63
  revisionDraft,
63
64
  this.photoMetadata,
64
65
  onFinish,
66
+ this.controller,
65
67
  this.signal,
66
68
  );
67
69
  }
@@ -80,9 +82,10 @@ export class PhotoStreamUploader extends StreamUploader {
80
82
  revisionDraft: NodeRevisionDraft,
81
83
  metadata: PhotoUploadMetadata,
82
84
  onFinish: (failure: boolean) => Promise<void>,
85
+ controller: UploadController,
83
86
  signal?: AbortSignal,
84
87
  ) {
85
- super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, signal);
88
+ super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, controller, signal);
86
89
  this.photoUploadManager = uploadManager;
87
90
  this.photoMetadata = metadata;
88
91
  }
@@ -1,15 +1,11 @@
1
1
  import { DriveAPIService, drivePaths, nodeTypeNumberToNodeType } from '../apiService';
2
2
  import { Logger, MemberRole } from '../../interface';
3
- import { makeNodeUid, splitNodeUid } from '../uids';
4
- import { EncryptedShareCrypto, EncryptedNode } from './interface';
5
-
6
- const PAGE_SIZE = 50;
3
+ import { makeNodeUid } from '../uids';
4
+ import { EncryptedNode } from '../nodes/interface';
5
+ import { EncryptedShareCrypto } from './interface';
7
6
 
8
7
  type GetTokenInfoResponse = drivePaths['/drive/urls/{token}']['get']['responses']['200']['content']['application/json'];
9
8
 
10
- type GetTokenFolderChildrenResponse =
11
- drivePaths['/drive/urls/{token}/folders/{linkID}/children']['get']['responses']['200']['content']['application/json'];
12
-
13
9
  /**
14
10
  * Provides API communication for accessing public link data.
15
11
  *
@@ -41,27 +37,6 @@ export class SharingPublicAPIService {
41
37
  },
42
38
  };
43
39
  }
44
-
45
- async *iterateFolderChildren(parentUid: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
46
- const { volumeId: token, nodeId } = splitNodeUid(parentUid);
47
-
48
- let page = 0;
49
- while (true) {
50
- const response = await this.apiService.get<GetTokenFolderChildrenResponse>(
51
- `drive/urls/${token}/folders/${nodeId}/children?Page=${page}&PageSize=${PAGE_SIZE}`,
52
- signal,
53
- );
54
-
55
- for (const link of response.Links) {
56
- yield linkToEncryptedNode(this.logger, token, link);
57
- }
58
-
59
- if (response.Links.length < PAGE_SIZE) {
60
- break;
61
- }
62
- page++;
63
- }
64
- }
65
40
  }
66
41
 
67
42
  function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token']): EncryptedNode {
@@ -70,7 +45,7 @@ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token
70
45
  encryptedName: token.Name,
71
46
 
72
47
  // Basic node metadata
73
- uid: makeNodeUid(token.Token, token.LinkID),
48
+ uid: makeNodeUid(token.VolumeID, token.LinkID),
74
49
  parentUid: undefined,
75
50
  type: nodeTypeNumberToNodeType(logger, token.LinkType),
76
51
  creationTime: new Date(), // TODO
@@ -115,61 +90,3 @@ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token
115
90
 
116
91
  throw new Error(`Unknown node type: ${token.LinkType}`);
117
92
  }
118
-
119
- function linkToEncryptedNode(
120
- logger: Logger,
121
- token: string,
122
- link: GetTokenFolderChildrenResponse['Links'][0],
123
- ): EncryptedNode {
124
- const baseNodeMetadata = {
125
- // Internal metadata
126
- hash: link.Hash || undefined,
127
- encryptedName: link.Name,
128
-
129
- // Basic node metadata
130
- uid: makeNodeUid(token, link.LinkID),
131
- parentUid: link.ParentLinkID ? makeNodeUid(token, link.ParentLinkID) : undefined,
132
- type: nodeTypeNumberToNodeType(logger, link.Type),
133
- creationTime: new Date(), // TODO
134
- totalStorageSize: link.TotalSize,
135
-
136
- isShared: false,
137
- isSharedPublicly: false,
138
- directRole: MemberRole.Viewer, // TODO
139
- };
140
-
141
- const baseCryptoNodeMetadata = {
142
- signatureEmail: link.SignatureEmail || undefined,
143
- armoredKey: link.NodeKey,
144
- armoredNodePassphrase: link.NodePassphrase,
145
- armoredNodePassphraseSignature: link.NodePassphraseSignature || undefined,
146
- };
147
-
148
- if (link.Type === 1 && link.FolderProperties) {
149
- return {
150
- ...baseNodeMetadata,
151
- encryptedCrypto: {
152
- ...baseCryptoNodeMetadata,
153
- folder: {
154
- armoredHashKey: link.FolderProperties.NodeHashKey as string,
155
- },
156
- },
157
- };
158
- }
159
-
160
- if (link.Type === 2 && link.FileProperties?.ContentKeyPacket) {
161
- return {
162
- ...baseNodeMetadata,
163
- totalStorageSize: link.FileProperties.ActiveRevision?.Size || undefined,
164
- mediaType: link.MIMEType || undefined,
165
- encryptedCrypto: {
166
- ...baseCryptoNodeMetadata,
167
- file: {
168
- base64ContentKeyPacket: link.FileProperties.ContentKeyPacket,
169
- },
170
- },
171
- };
172
- }
173
-
174
- throw new Error(`Unknown node type: ${link.Type}`);
175
- }
@@ -1,6 +1,5 @@
1
1
  import { PrivateKey } from '../../crypto';
2
2
  import { ProtonDriveCryptoCache, Logger } from '../../interface';
3
- import { DecryptedNodeKeys } from './interface';
4
3
 
5
4
  /**
6
5
  * Provides caching for public link crypto material.
@@ -39,41 +38,8 @@ export class SharingPublicCryptoCache {
39
38
  }
40
39
  return shareKeyData.publicShareKey.key;
41
40
  }
42
-
43
- async setNodeKeys(nodeUid: string, keys: DecryptedNodeKeys): Promise<void> {
44
- const cacheUid = getNodeCacheKey(nodeUid);
45
- await this.driveCache.setEntity(cacheUid, {
46
- nodeKeys: keys,
47
- });
48
- }
49
-
50
- async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
51
- const nodeKeysData = await this.driveCache.getEntity(getNodeCacheKey(nodeUid));
52
- if (!nodeKeysData.nodeKeys) {
53
- try {
54
- await this.removeNodeKeys([nodeUid]);
55
- } catch (removingError: unknown) {
56
- // The node keys will not be returned, thus SDK will re-fetch
57
- // and re-cache it. Setting it again should then fix the problem.
58
- this.logger.warn(
59
- `Failed to remove corrupted public node keys from the cache: ${removingError instanceof Error ? removingError.message : removingError}`,
60
- );
61
- }
62
- throw new Error(`Failed to deserialize public node keys`);
63
- }
64
- return nodeKeysData.nodeKeys;
65
- }
66
-
67
- async removeNodeKeys(nodeUids: string[]): Promise<void> {
68
- const cacheUids = nodeUids.map(getNodeCacheKey);
69
- await this.driveCache.removeEntities(cacheUids);
70
- }
71
41
  }
72
42
 
73
43
  function getShareKeyCacheKey() {
74
44
  return 'publicShareKey';
75
45
  }
76
-
77
- function getNodeCacheKey(nodeUid: string) {
78
- return `publicNodeKeys-${nodeUid}`;
79
- }
@@ -0,0 +1,73 @@
1
+ import { c } from 'ttag';
2
+
3
+ import { VERIFICATION_STATUS } from '../../crypto';
4
+ import { getVerificationMessage } from '../errors';
5
+ import {
6
+ resultOk,
7
+ resultError,
8
+ Author,
9
+ AnonymousUser,
10
+ ProtonDriveTelemetry,
11
+ MetricVerificationErrorField,
12
+ MetricVolumeType,
13
+ MetricsDecryptionErrorField,
14
+ Logger,
15
+ } from '../../interface';
16
+
17
+ export class SharingPublicCryptoReporter {
18
+ private logger: Logger;
19
+ private telemetry: ProtonDriveTelemetry;
20
+
21
+ constructor(telemetry: ProtonDriveTelemetry) {
22
+ this.telemetry = telemetry;
23
+ this.logger = telemetry.getLogger('sharingPublic-crypto');
24
+ }
25
+
26
+ async handleClaimedAuthor(
27
+ node: { uid: string; creationTime: Date },
28
+ field: MetricVerificationErrorField,
29
+ signatureType: string,
30
+ verified: VERIFICATION_STATUS,
31
+ verificationErrors?: Error[],
32
+ claimedAuthor?: string,
33
+ notAvailableVerificationKeys = false,
34
+ ): Promise<Author> {
35
+ if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
36
+ return resultOk(claimedAuthor || (null as AnonymousUser));
37
+ }
38
+
39
+ return resultError({
40
+ claimedAuthor,
41
+ error: !claimedAuthor
42
+ ? c('Info').t`Author is not provided on public link`
43
+ : getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
44
+ });
45
+ }
46
+
47
+ reportDecryptionError(
48
+ node: { uid: string; creationTime: Date },
49
+ field: MetricsDecryptionErrorField,
50
+ error: unknown,
51
+ ) {
52
+ const fromBefore2024 = node.creationTime < new Date('2024-01-01');
53
+
54
+ this.logger.error(
55
+ `Failed to decrypt public link node ${node.uid} (from before 2024: ${fromBefore2024})`,
56
+ error,
57
+ );
58
+
59
+ this.telemetry.recordMetric({
60
+ eventName: 'decryptionError',
61
+ volumeType: MetricVolumeType.SharedPublic,
62
+ field,
63
+ fromBefore2024,
64
+ error,
65
+ uid: node.uid,
66
+ });
67
+ }
68
+
69
+ reportVerificationError() {
70
+ // Authors or signatures are not provided on public links.
71
+ // We do not report any signature verification errors at this moment.
72
+ }
73
+ }
@@ -1,30 +1,12 @@
1
- import { c } from 'ttag';
2
-
3
- import { DriveCrypto, PrivateKey, VERIFICATION_STATUS } from '../../crypto';
4
- import { getVerificationMessage } from '../errors';
5
- import {
6
- resultOk,
7
- resultError,
8
- Author,
9
- AnonymousUser,
10
- ProtonDriveTelemetry,
11
- MetricVerificationErrorField,
12
- MetricVolumeType,
13
- MetricsDecryptionErrorField,
14
- Logger,
15
- ProtonDriveAccount,
16
- } from '../../interface';
17
- import { NodesCryptoService } from '../nodes/cryptoService';
1
+ import { DriveCrypto, PrivateKey } from '../../crypto';
18
2
  import { EncryptedShareCrypto } from './interface';
19
3
 
20
- export class SharingPublicCryptoService extends NodesCryptoService {
4
+ export class SharingPublicCryptoService {
21
5
  constructor(
22
- telemetry: ProtonDriveTelemetry,
23
- driveCrypto: DriveCrypto,
24
- account: ProtonDriveAccount,
6
+ private driveCrypto: DriveCrypto,
25
7
  private password: string,
26
8
  ) {
27
- super(telemetry, driveCrypto, account, new SharingPublicCryptoReporter(telemetry));
9
+ this.driveCrypto = driveCrypto;
28
10
  this.password = password;
29
11
  }
30
12
 
@@ -38,61 +20,3 @@ export class SharingPublicCryptoService extends NodesCryptoService {
38
20
  return shareKey;
39
21
  }
40
22
  }
41
-
42
- class SharingPublicCryptoReporter {
43
- private logger: Logger;
44
- private telemetry: ProtonDriveTelemetry;
45
-
46
- constructor(telemetry: ProtonDriveTelemetry) {
47
- this.telemetry = telemetry;
48
- this.logger = telemetry.getLogger('sharingPublic-crypto');
49
- }
50
-
51
- async handleClaimedAuthor(
52
- node: { uid: string; creationTime: Date },
53
- field: MetricVerificationErrorField,
54
- signatureType: string,
55
- verified: VERIFICATION_STATUS,
56
- verificationErrors?: Error[],
57
- claimedAuthor?: string,
58
- notAvailableVerificationKeys = false,
59
- ): Promise<Author> {
60
- if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
61
- return resultOk(claimedAuthor || (null as AnonymousUser));
62
- }
63
-
64
- return resultError({
65
- claimedAuthor,
66
- error: !claimedAuthor
67
- ? c('Info').t`Author is not provided on public link`
68
- : getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
69
- });
70
- }
71
-
72
- reportDecryptionError(
73
- node: { uid: string; creationTime: Date },
74
- field: MetricsDecryptionErrorField,
75
- error: unknown,
76
- ) {
77
- const fromBefore2024 = node.creationTime < new Date('2024-01-01');
78
-
79
- this.logger.error(
80
- `Failed to decrypt public link node ${node.uid} (from before 2024: ${fromBefore2024})`,
81
- error,
82
- );
83
-
84
- this.telemetry.recordMetric({
85
- eventName: 'decryptionError',
86
- volumeType: MetricVolumeType.SharedPublic,
87
- field,
88
- fromBefore2024,
89
- error,
90
- uid: node.uid,
91
- });
92
- }
93
-
94
- reportVerificationError() {
95
- // Authors or signatures are not provided on public links.
96
- // We do not report any signature verification errors at this moment.
97
- }
98
- }
@@ -1,10 +1,22 @@
1
1
  import { DriveCrypto } from '../../crypto';
2
- import { ProtonDriveCryptoCache, ProtonDriveTelemetry, ProtonDriveAccount } from '../../interface';
2
+ import {
3
+ ProtonDriveCryptoCache,
4
+ ProtonDriveTelemetry,
5
+ ProtonDriveAccount,
6
+ ProtonDriveEntitiesCache,
7
+ } from '../../interface';
3
8
  import { DriveAPIService } from '../apiService';
9
+ import { NodeAPIService } from '../nodes/apiService';
10
+ import { NodesCache } from '../nodes/cache';
11
+ import { NodesCryptoCache } from '../nodes/cryptoCache';
12
+ import { NodesCryptoService } from '../nodes/cryptoService';
13
+ import { NodesRevisons } from '../nodes/nodesRevisions';
4
14
  import { SharingPublicAPIService } from './apiService';
5
15
  import { SharingPublicCryptoCache } from './cryptoCache';
16
+ import { SharingPublicCryptoReporter } from './cryptoReporter';
6
17
  import { SharingPublicCryptoService } from './cryptoService';
7
- import { SharingPublicManager } from './manager';
18
+ import { SharingPublicNodesAccess } from './nodes';
19
+ import { SharingPublicSharesManager } from './shares';
8
20
 
9
21
  export { SharingPublicSessionManager } from './session/manager';
10
22
 
@@ -20,22 +32,72 @@ export { SharingPublicSessionManager } from './session/manager';
20
32
  export function initSharingPublicModule(
21
33
  telemetry: ProtonDriveTelemetry,
22
34
  apiService: DriveAPIService,
35
+ driveEntitiesCache: ProtonDriveEntitiesCache,
23
36
  driveCryptoCache: ProtonDriveCryptoCache,
24
37
  driveCrypto: DriveCrypto,
25
38
  account: ProtonDriveAccount,
39
+ url: string,
26
40
  token: string,
27
41
  password: string,
28
42
  ) {
29
43
  const api = new SharingPublicAPIService(telemetry.getLogger('sharingPublic-api'), apiService);
30
44
  const cryptoCache = new SharingPublicCryptoCache(telemetry.getLogger('sharingPublic-crypto'), driveCryptoCache);
31
- const cryptoService = new SharingPublicCryptoService(telemetry, driveCrypto, account, password);
32
- const manager = new SharingPublicManager(
33
- telemetry.getLogger('sharingPublic-nodes'),
45
+ const cryptoService = new SharingPublicCryptoService(driveCrypto, password);
46
+ const shares = new SharingPublicSharesManager(api, cryptoCache, cryptoService, account, token);
47
+ const nodes = initSharingPublicNodesModule(
48
+ telemetry,
49
+ apiService,
50
+ driveEntitiesCache,
51
+ driveCryptoCache,
52
+ driveCrypto,
53
+ account,
54
+ shares,
55
+ url,
56
+ token,
57
+ );
58
+
59
+ return {
60
+ shares,
61
+ nodes,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Provides facade for the public link nodes module.
67
+ *
68
+ * The public link nodes initializes the core nodes module, but uses public
69
+ * link shares or crypto reporter instead.
70
+ */
71
+ export function initSharingPublicNodesModule(
72
+ telemetry: ProtonDriveTelemetry,
73
+ apiService: DriveAPIService,
74
+ driveEntitiesCache: ProtonDriveEntitiesCache,
75
+ driveCryptoCache: ProtonDriveCryptoCache,
76
+ driveCrypto: DriveCrypto,
77
+ account: ProtonDriveAccount,
78
+ sharesService: SharingPublicSharesManager,
79
+ url: string,
80
+ token: string,
81
+ ) {
82
+ const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
83
+ const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
84
+ const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
85
+ const cryptoReporter = new SharingPublicCryptoReporter(telemetry);
86
+ const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
87
+ const nodesAccess = new SharingPublicNodesAccess(
88
+ telemetry.getLogger('nodes'),
34
89
  api,
90
+ cache,
35
91
  cryptoCache,
36
92
  cryptoService,
93
+ sharesService,
94
+ url,
37
95
  token,
38
96
  );
97
+ const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
39
98
 
40
- return manager;
99
+ return {
100
+ access: nodesAccess,
101
+ revisions: nodesRevisions,
102
+ };
41
103
  }