@protontech/drive-sdk 0.4.0 → 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 (176) 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/nodes.d.ts +4 -0
  5. package/dist/interface/nodes.js.map +1 -1
  6. package/dist/interface/upload.d.ts +6 -3
  7. package/dist/internal/apiService/apiService.d.ts +3 -0
  8. package/dist/internal/apiService/apiService.js +25 -2
  9. package/dist/internal/apiService/apiService.js.map +1 -1
  10. package/dist/internal/apiService/apiService.test.js +38 -0
  11. package/dist/internal/apiService/apiService.test.js.map +1 -1
  12. package/dist/internal/apiService/driveTypes.d.ts +31 -48
  13. package/dist/internal/apiService/errors.js +3 -0
  14. package/dist/internal/apiService/errors.js.map +1 -1
  15. package/dist/internal/apiService/errors.test.js +15 -7
  16. package/dist/internal/apiService/errors.test.js.map +1 -1
  17. package/dist/internal/asyncIteratorMap.d.ts +1 -1
  18. package/dist/internal/asyncIteratorMap.js +6 -1
  19. package/dist/internal/asyncIteratorMap.js.map +1 -1
  20. package/dist/internal/asyncIteratorMap.test.js +9 -0
  21. package/dist/internal/asyncIteratorMap.test.js.map +1 -1
  22. package/dist/internal/download/fileDownloader.d.ts +3 -3
  23. package/dist/internal/download/fileDownloader.js +5 -5
  24. package/dist/internal/download/fileDownloader.js.map +1 -1
  25. package/dist/internal/download/fileDownloader.test.js +8 -8
  26. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  27. package/dist/internal/nodes/apiService.d.ts +6 -1
  28. package/dist/internal/nodes/apiService.js +45 -32
  29. package/dist/internal/nodes/apiService.js.map +1 -1
  30. package/dist/internal/nodes/apiService.test.js +164 -17
  31. package/dist/internal/nodes/apiService.test.js.map +1 -1
  32. package/dist/internal/nodes/cache.test.js +1 -0
  33. package/dist/internal/nodes/cache.test.js.map +1 -1
  34. package/dist/internal/nodes/debouncer.d.ts +23 -0
  35. package/dist/internal/nodes/debouncer.js +80 -0
  36. package/dist/internal/nodes/debouncer.js.map +1 -0
  37. package/dist/internal/nodes/debouncer.test.d.ts +1 -0
  38. package/dist/internal/nodes/debouncer.test.js +100 -0
  39. package/dist/internal/nodes/debouncer.test.js.map +1 -0
  40. package/dist/internal/nodes/extendedAttributes.d.ts +2 -2
  41. package/dist/internal/nodes/extendedAttributes.js +15 -11
  42. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  43. package/dist/internal/nodes/extendedAttributes.test.js +19 -1
  44. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  45. package/dist/internal/nodes/index.test.js +1 -0
  46. package/dist/internal/nodes/index.test.js.map +1 -1
  47. package/dist/internal/nodes/interface.d.ts +1 -0
  48. package/dist/internal/nodes/nodesAccess.d.ts +2 -1
  49. package/dist/internal/nodes/nodesAccess.js +24 -5
  50. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  51. package/dist/internal/nodes/nodesAccess.test.js +2 -2
  52. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  53. package/dist/internal/nodes/nodesManagement.js +1 -0
  54. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  55. package/dist/internal/photos/index.d.ts +11 -0
  56. package/dist/internal/photos/index.js +27 -0
  57. package/dist/internal/photos/index.js.map +1 -1
  58. package/dist/internal/photos/upload.d.ts +60 -0
  59. package/dist/internal/photos/upload.js +104 -0
  60. package/dist/internal/photos/upload.js.map +1 -0
  61. package/dist/internal/sharingPublic/apiService.d.ts +2 -2
  62. package/dist/internal/sharingPublic/apiService.js +2 -62
  63. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  64. package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -4
  65. package/dist/internal/sharingPublic/cryptoCache.js +0 -28
  66. package/dist/internal/sharingPublic/cryptoCache.js.map +1 -1
  67. package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
  68. package/dist/internal/sharingPublic/cryptoReporter.js +44 -0
  69. package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
  70. package/dist/internal/sharingPublic/cryptoService.d.ts +3 -4
  71. package/dist/internal/sharingPublic/cryptoService.js +5 -43
  72. package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
  73. package/dist/internal/sharingPublic/index.d.ts +21 -3
  74. package/dist/internal/sharingPublic/index.js +43 -12
  75. package/dist/internal/sharingPublic/index.js.map +1 -1
  76. package/dist/internal/sharingPublic/interface.d.ts +0 -1
  77. package/dist/internal/sharingPublic/nodes.d.ts +13 -0
  78. package/dist/internal/sharingPublic/nodes.js +28 -0
  79. package/dist/internal/sharingPublic/nodes.js.map +1 -0
  80. package/dist/internal/sharingPublic/session/session.d.ts +3 -3
  81. package/dist/internal/sharingPublic/session/url.test.js +3 -3
  82. package/dist/internal/sharingPublic/shares.d.ts +34 -0
  83. package/dist/internal/sharingPublic/shares.js +69 -0
  84. package/dist/internal/sharingPublic/shares.js.map +1 -0
  85. package/dist/internal/upload/apiService.d.ts +2 -2
  86. package/dist/internal/upload/apiService.js +11 -2
  87. package/dist/internal/upload/apiService.js.map +1 -1
  88. package/dist/internal/upload/controller.d.ts +8 -2
  89. package/dist/internal/upload/controller.js.map +1 -1
  90. package/dist/internal/upload/cryptoService.d.ts +2 -2
  91. package/dist/internal/upload/cryptoService.js.map +1 -1
  92. package/dist/internal/upload/fileUploader.d.ts +7 -3
  93. package/dist/internal/upload/fileUploader.js +6 -3
  94. package/dist/internal/upload/fileUploader.js.map +1 -1
  95. package/dist/internal/upload/fileUploader.test.js +23 -11
  96. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  97. package/dist/internal/upload/interface.d.ts +3 -0
  98. package/dist/internal/upload/manager.d.ts +12 -11
  99. package/dist/internal/upload/manager.js +8 -2
  100. package/dist/internal/upload/manager.js.map +1 -1
  101. package/dist/internal/upload/manager.test.js +8 -0
  102. package/dist/internal/upload/manager.test.js.map +1 -1
  103. package/dist/internal/upload/streamUploader.d.ts +40 -26
  104. package/dist/internal/upload/streamUploader.js +15 -8
  105. package/dist/internal/upload/streamUploader.js.map +1 -1
  106. package/dist/internal/upload/streamUploader.test.js +11 -7
  107. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  108. package/dist/protonDriveClient.d.ts +3 -3
  109. package/dist/protonDriveClient.js +4 -4
  110. package/dist/protonDriveClient.js.map +1 -1
  111. package/dist/protonDrivePhotosClient.d.ts +18 -2
  112. package/dist/protonDrivePhotosClient.js +19 -2
  113. package/dist/protonDrivePhotosClient.js.map +1 -1
  114. package/dist/protonDrivePublicLinkClient.d.ts +31 -4
  115. package/dist/protonDrivePublicLinkClient.js +52 -9
  116. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  117. package/dist/transformers.d.ts +1 -1
  118. package/dist/transformers.js +1 -0
  119. package/dist/transformers.js.map +1 -1
  120. package/package.json +1 -1
  121. package/src/diagnostic/sdkDiagnostic.ts +1 -1
  122. package/src/interface/download.ts +4 -4
  123. package/src/interface/nodes.ts +4 -0
  124. package/src/interface/upload.ts +3 -3
  125. package/src/internal/apiService/apiService.test.ts +50 -0
  126. package/src/internal/apiService/apiService.ts +33 -2
  127. package/src/internal/apiService/driveTypes.ts +31 -48
  128. package/src/internal/apiService/errors.test.ts +10 -0
  129. package/src/internal/apiService/errors.ts +5 -1
  130. package/src/internal/asyncIteratorMap.test.ts +12 -0
  131. package/src/internal/asyncIteratorMap.ts +8 -0
  132. package/src/internal/download/fileDownloader.test.ts +8 -8
  133. package/src/internal/download/fileDownloader.ts +5 -5
  134. package/src/internal/nodes/apiService.test.ts +222 -16
  135. package/src/internal/nodes/apiService.ts +63 -49
  136. package/src/internal/nodes/cache.test.ts +1 -0
  137. package/src/internal/nodes/debouncer.test.ts +129 -0
  138. package/src/internal/nodes/debouncer.ts +93 -0
  139. package/src/internal/nodes/extendedAttributes.test.ts +23 -1
  140. package/src/internal/nodes/extendedAttributes.ts +26 -18
  141. package/src/internal/nodes/index.test.ts +1 -0
  142. package/src/internal/nodes/interface.ts +1 -0
  143. package/src/internal/nodes/nodesAccess.test.ts +2 -2
  144. package/src/internal/nodes/nodesAccess.ts +30 -5
  145. package/src/internal/nodes/nodesManagement.ts +1 -0
  146. package/src/internal/photos/index.ts +62 -0
  147. package/src/internal/photos/upload.ts +212 -0
  148. package/src/internal/sharingPublic/apiService.ts +5 -86
  149. package/src/internal/sharingPublic/cryptoCache.ts +0 -34
  150. package/src/internal/sharingPublic/cryptoReporter.ts +73 -0
  151. package/src/internal/sharingPublic/cryptoService.ts +4 -80
  152. package/src/internal/sharingPublic/index.ts +68 -6
  153. package/src/internal/sharingPublic/interface.ts +0 -9
  154. package/src/internal/sharingPublic/nodes.ts +37 -0
  155. package/src/internal/sharingPublic/session/apiService.ts +1 -1
  156. package/src/internal/sharingPublic/session/session.ts +3 -3
  157. package/src/internal/sharingPublic/session/url.test.ts +3 -3
  158. package/src/internal/sharingPublic/shares.ts +86 -0
  159. package/src/internal/upload/apiService.ts +15 -4
  160. package/src/internal/upload/controller.ts +2 -2
  161. package/src/internal/upload/cryptoService.ts +2 -2
  162. package/src/internal/upload/fileUploader.test.ts +25 -11
  163. package/src/internal/upload/fileUploader.ts +16 -3
  164. package/src/internal/upload/interface.ts +3 -0
  165. package/src/internal/upload/manager.test.ts +8 -0
  166. package/src/internal/upload/manager.ts +20 -10
  167. package/src/internal/upload/streamUploader.test.ts +32 -15
  168. package/src/internal/upload/streamUploader.ts +43 -30
  169. package/src/protonDriveClient.ts +4 -4
  170. package/src/protonDrivePhotosClient.ts +46 -6
  171. package/src/protonDrivePublicLinkClient.ts +93 -12
  172. package/src/transformers.ts +2 -0
  173. package/dist/internal/sharingPublic/manager.d.ts +0 -19
  174. package/dist/internal/sharingPublic/manager.js +0 -81
  175. package/dist/internal/sharingPublic/manager.js.map +0 -1
  176. 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
+ }
@@ -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,
@@ -38,6 +38,7 @@ interface BaseNode {
38
38
  // Share node metadata
39
39
  shareId?: string;
40
40
  isShared: boolean;
41
+ isSharedPublicly: boolean;
41
42
  directRole: MemberRole;
42
43
  membership?: {
43
44
  role: MemberRole;
@@ -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);
@@ -329,6 +329,7 @@ export class NodesManagement {
329
329
 
330
330
  // Share node metadata
331
331
  isShared: false,
332
+ isSharedPublicly: false,
332
333
  directRole: MemberRole.Inherited,
333
334
 
334
335
  // Decrypted metadata
@@ -9,11 +9,21 @@ import {
9
9
  import { SharesCache } from '../shares/cache';
10
10
  import { SharesCryptoCache } from '../shares/cryptoCache';
11
11
  import { SharesCryptoService } from '../shares/cryptoService';
12
+ import { NodesService as UploadNodesService } from '../upload/interface';
13
+ import { UploadTelemetry } from '../upload/telemetry';
14
+ import { UploadQueue } from '../upload/queue';
12
15
  import { Albums } from './albums';
13
16
  import { PhotosAPIService } from './apiService';
14
17
  import { NodesService, SharesService } from './interface';
15
18
  import { PhotoSharesManager } from './shares';
16
19
  import { PhotosTimeline } from './timeline';
20
+ import {
21
+ PhotoFileUploader,
22
+ PhotoUploadAPIService,
23
+ PhotoUploadCryptoService,
24
+ PhotoUploadManager,
25
+ PhotoUploadMetadata,
26
+ } from './upload';
17
27
 
18
28
  /**
19
29
  * Provides facade for the whole photos module.
@@ -66,3 +76,55 @@ export function initPhotoSharesModule(
66
76
  sharesService,
67
77
  );
68
78
  }
79
+
80
+ /**
81
+ * Provides facade for the photo upload module.
82
+ *
83
+ * The photo upload wraps the core upload module and adds photo specific metadata.
84
+ * It provides the same interface so it can be used in the same way.
85
+ */
86
+ export function initPhotoUploadModule(
87
+ telemetry: ProtonDriveTelemetry,
88
+ apiService: DriveAPIService,
89
+ driveCrypto: DriveCrypto,
90
+ sharesService: SharesService,
91
+ nodesService: UploadNodesService,
92
+ clientUid?: string,
93
+ ) {
94
+ const api = new PhotoUploadAPIService(apiService, clientUid);
95
+ const cryptoService = new PhotoUploadCryptoService(driveCrypto, nodesService);
96
+
97
+ const uploadTelemetry = new UploadTelemetry(telemetry, sharesService);
98
+ const manager = new PhotoUploadManager(telemetry, api, cryptoService, nodesService, clientUid);
99
+
100
+ const queue = new UploadQueue();
101
+
102
+ async function getFileUploader(
103
+ parentFolderUid: string,
104
+ name: string,
105
+ metadata: PhotoUploadMetadata,
106
+ signal?: AbortSignal,
107
+ ): Promise<PhotoFileUploader> {
108
+ await queue.waitForCapacity(signal);
109
+
110
+ const onFinish = () => {
111
+ queue.releaseCapacity();
112
+ };
113
+
114
+ return new PhotoFileUploader(
115
+ uploadTelemetry,
116
+ api,
117
+ cryptoService,
118
+ manager,
119
+ parentFolderUid,
120
+ name,
121
+ metadata,
122
+ onFinish,
123
+ signal,
124
+ );
125
+ }
126
+
127
+ return {
128
+ getFileUploader,
129
+ };
130
+ }
@@ -0,0 +1,212 @@
1
+ import { DriveCrypto } from '../../crypto';
2
+ import { ProtonDriveTelemetry, UploadMetadata, Thumbnail } from '../../interface';
3
+ import { DriveAPIService, drivePaths } from '../apiService';
4
+ import { generateFileExtendedAttributes } from '../nodes';
5
+ import { splitNodeRevisionUid } from '../uids';
6
+ import { UploadAPIService } from '../upload/apiService';
7
+ import { BlockVerifier } from '../upload/blockVerifier';
8
+ import { UploadController } from '../upload/controller';
9
+ import { UploadCryptoService } from '../upload/cryptoService';
10
+ import { FileUploader } from '../upload/fileUploader';
11
+ import { NodeRevisionDraft, NodesService } from '../upload/interface';
12
+ import { UploadManager } from '../upload/manager';
13
+ import { StreamUploader } from '../upload/streamUploader';
14
+ import { UploadTelemetry } from '../upload/telemetry';
15
+
16
+ type PostCommitRevisionRequest = Extract<
17
+ drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['put']['requestBody'],
18
+ { content: object }
19
+ >['content']['application/json'];
20
+ type PostCommitRevisionResponse =
21
+ drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['put']['responses']['200']['content']['application/json'];
22
+
23
+ export type PhotoUploadMetadata = UploadMetadata & {
24
+ captureTime?: Date;
25
+ mainPhotoLinkID?: string;
26
+ // TODO: handle tags enum in the SDK
27
+ tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
28
+ };
29
+
30
+ export class PhotoFileUploader extends FileUploader {
31
+ private photoApiService: PhotoUploadAPIService;
32
+ private photoManager: PhotoUploadManager;
33
+ private photoMetadata: PhotoUploadMetadata;
34
+
35
+ constructor(
36
+ telemetry: UploadTelemetry,
37
+ apiService: PhotoUploadAPIService,
38
+ cryptoService: UploadCryptoService,
39
+ manager: PhotoUploadManager,
40
+ parentFolderUid: string,
41
+ name: string,
42
+ metadata: PhotoUploadMetadata,
43
+ onFinish: () => void,
44
+ signal?: AbortSignal,
45
+ ) {
46
+ super(telemetry, apiService, cryptoService, manager, parentFolderUid, name, metadata, onFinish, signal);
47
+ this.photoApiService = apiService;
48
+ this.photoManager = manager;
49
+ this.photoMetadata = metadata;
50
+ }
51
+
52
+ protected async newStreamUploader(
53
+ blockVerifier: BlockVerifier,
54
+ revisionDraft: NodeRevisionDraft,
55
+ onFinish: (failure: boolean) => Promise<void>,
56
+ ): Promise<StreamUploader> {
57
+ return new PhotoStreamUploader(
58
+ this.telemetry,
59
+ this.photoApiService,
60
+ this.cryptoService,
61
+ this.photoManager,
62
+ blockVerifier,
63
+ revisionDraft,
64
+ this.photoMetadata,
65
+ onFinish,
66
+ this.controller,
67
+ this.signal,
68
+ );
69
+ }
70
+ }
71
+
72
+ export class PhotoStreamUploader extends StreamUploader {
73
+ private photoUploadManager: PhotoUploadManager;
74
+ private photoMetadata: PhotoUploadMetadata;
75
+
76
+ constructor(
77
+ telemetry: UploadTelemetry,
78
+ apiService: PhotoUploadAPIService,
79
+ cryptoService: UploadCryptoService,
80
+ uploadManager: PhotoUploadManager,
81
+ blockVerifier: BlockVerifier,
82
+ revisionDraft: NodeRevisionDraft,
83
+ metadata: PhotoUploadMetadata,
84
+ onFinish: (failure: boolean) => Promise<void>,
85
+ controller: UploadController,
86
+ signal?: AbortSignal,
87
+ ) {
88
+ super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, controller, signal);
89
+ this.photoUploadManager = uploadManager;
90
+ this.photoMetadata = metadata;
91
+ }
92
+
93
+ async commitFile(thumbnails: Thumbnail[]) {
94
+ this.verifyIntegrity(thumbnails);
95
+
96
+ const extendedAttributes = {
97
+ modificationTime: this.metadata.modificationTime,
98
+ size: this.metadata.expectedSize,
99
+ blockSizes: this.uploadedBlockSizes,
100
+ digests: this.digests.digests(),
101
+ };
102
+
103
+ await this.photoUploadManager.commitDraftPhoto(this.revisionDraft, this.manifest, extendedAttributes, this.photoMetadata);
104
+ }
105
+ }
106
+
107
+ export class PhotoUploadManager extends UploadManager {
108
+ private photoApiService: PhotoUploadAPIService;
109
+ private photoCryptoService: PhotoUploadCryptoService;
110
+
111
+ constructor(
112
+ telemetry: ProtonDriveTelemetry,
113
+ apiService: PhotoUploadAPIService,
114
+ cryptoService: PhotoUploadCryptoService,
115
+ nodesService: NodesService,
116
+ clientUid: string | undefined,
117
+ ) {
118
+ super(telemetry, apiService, cryptoService, nodesService, clientUid);
119
+ this.photoApiService = apiService;
120
+ this.photoCryptoService = cryptoService;
121
+ }
122
+
123
+ async commitDraftPhoto(
124
+ nodeRevisionDraft: NodeRevisionDraft,
125
+ manifest: Uint8Array,
126
+ extendedAttributes: {
127
+ modificationTime?: Date;
128
+ size: number;
129
+ blockSizes: number[];
130
+ digests: {
131
+ sha1: string;
132
+ };
133
+ },
134
+ uploadMetadata: PhotoUploadMetadata,
135
+ ): Promise<void> {
136
+ if (!nodeRevisionDraft.parentNodeKeys) {
137
+ throw new Error('Parent node keys are required for photo upload');
138
+ }
139
+
140
+ // TODO: handle photo extended attributes in the SDK - now it must be passed from the client
141
+ const generatedExtendedAttributes = generateFileExtendedAttributes(extendedAttributes, uploadMetadata.additionalMetadata);
142
+ const nodeCommitCrypto = await this.cryptoService.commitFile(
143
+ nodeRevisionDraft.nodeKeys,
144
+ manifest,
145
+ generatedExtendedAttributes,
146
+ );
147
+
148
+ const sha1 = extendedAttributes.digests.sha1;
149
+ const contentHash = await this.photoCryptoService.generateContentHash(sha1, nodeRevisionDraft.parentNodeKeys?.hashKey);
150
+ const photo = {
151
+ contentHash,
152
+ captureTime: uploadMetadata.captureTime || extendedAttributes.modificationTime,
153
+ mainPhotoLinkID: uploadMetadata.mainPhotoLinkID,
154
+ tags: uploadMetadata.tags,
155
+ }
156
+ await this.photoApiService.commitDraftPhoto(nodeRevisionDraft.nodeRevisionUid, nodeCommitCrypto, photo);
157
+ await this.notifyNodeUploaded(nodeRevisionDraft);
158
+ }
159
+ }
160
+
161
+ export class PhotoUploadCryptoService extends UploadCryptoService {
162
+ constructor(
163
+ driveCrypto: DriveCrypto,
164
+ nodesService: NodesService,
165
+ ) {
166
+ super(driveCrypto, nodesService);
167
+ }
168
+
169
+ async generateContentHash(sha1: string, parentHashKey: Uint8Array): Promise<string> {
170
+ return this.driveCrypto.generateLookupHash(sha1, parentHashKey);
171
+ }
172
+ }
173
+
174
+ export class PhotoUploadAPIService extends UploadAPIService {
175
+ constructor(apiService: DriveAPIService, clientUid: string | undefined) {
176
+ super(apiService, clientUid);
177
+ }
178
+
179
+ async commitDraftPhoto(
180
+ draftNodeRevisionUid: string,
181
+ options: {
182
+ armoredManifestSignature: string;
183
+ signatureEmail: string;
184
+ armoredExtendedAttributes?: string;
185
+ },
186
+ photo: {
187
+ contentHash: string;
188
+ captureTime?: Date;
189
+ mainPhotoLinkID?: string;
190
+ // TODO: handle tags enum in the SDK
191
+ tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
192
+ },
193
+ ): Promise<void> {
194
+ const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
195
+ await this.apiService.put<
196
+ // TODO: Deprected fields but not properly marked in the types.
197
+ Omit<PostCommitRevisionRequest, 'BlockNumber' | 'BlockList' | 'ThumbnailToken' | 'State'>,
198
+ PostCommitRevisionResponse
199
+ >(`drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/${revisionId}`, {
200
+ ManifestSignature: options.armoredManifestSignature,
201
+ SignatureAddress: options.signatureEmail,
202
+ XAttr: options.armoredExtendedAttributes || null,
203
+ Photo: {
204
+ ContentHash: photo.contentHash,
205
+ CaptureTime: photo.captureTime?.getTime() || 0,
206
+ MainPhotoLinkID: photo.mainPhotoLinkID || null,
207
+ Tags: photo.tags || [],
208
+ Exif: null, // Deprecated field, not used.
209
+ },
210
+ });
211
+ }
212
+ }