@protontech/drive-sdk 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/dist/diagnostic/diagnostic.d.ts +7 -4
  2. package/dist/diagnostic/diagnostic.js +16 -8
  3. package/dist/diagnostic/diagnostic.js.map +1 -1
  4. package/dist/diagnostic/index.d.ts +1 -1
  5. package/dist/diagnostic/index.js +9 -1
  6. package/dist/diagnostic/index.js.map +1 -1
  7. package/dist/diagnostic/interface.d.ts +24 -9
  8. package/dist/diagnostic/nodeUtils.d.ts +13 -0
  9. package/dist/diagnostic/nodeUtils.js +90 -0
  10. package/dist/diagnostic/nodeUtils.js.map +1 -0
  11. package/dist/diagnostic/sdkDiagnosticBase.d.ts +36 -0
  12. package/dist/diagnostic/sdkDiagnosticBase.js +305 -0
  13. package/dist/diagnostic/sdkDiagnosticBase.js.map +1 -0
  14. package/dist/diagnostic/sdkDiagnosticMain.d.ts +16 -0
  15. package/dist/diagnostic/sdkDiagnosticMain.js +79 -0
  16. package/dist/diagnostic/sdkDiagnosticMain.js.map +1 -0
  17. package/dist/diagnostic/sdkDiagnosticPhotos.d.ts +13 -0
  18. package/dist/diagnostic/sdkDiagnosticPhotos.js +65 -0
  19. package/dist/diagnostic/sdkDiagnosticPhotos.js.map +1 -0
  20. package/dist/interface/index.d.ts +1 -1
  21. package/dist/interface/upload.d.ts +1 -12
  22. package/dist/internal/devices/interface.d.ts +1 -1
  23. package/dist/internal/devices/manager.js +1 -1
  24. package/dist/internal/devices/manager.js.map +1 -1
  25. package/dist/internal/devices/manager.test.js +3 -3
  26. package/dist/internal/devices/manager.test.js.map +1 -1
  27. package/dist/internal/errors.d.ts +5 -0
  28. package/dist/internal/errors.js +23 -0
  29. package/dist/internal/errors.js.map +1 -1
  30. package/dist/internal/errors.test.js +53 -2
  31. package/dist/internal/errors.test.js.map +1 -1
  32. package/dist/internal/nodes/apiService.d.ts +11 -1
  33. package/dist/internal/nodes/apiService.js +20 -1
  34. package/dist/internal/nodes/apiService.js.map +1 -1
  35. package/dist/internal/nodes/apiService.test.js +1 -1
  36. package/dist/internal/nodes/apiService.test.js.map +1 -1
  37. package/dist/internal/nodes/cryptoReporter.js +3 -0
  38. package/dist/internal/nodes/cryptoReporter.js.map +1 -1
  39. package/dist/internal/nodes/cryptoService.d.ts +4 -0
  40. package/dist/internal/nodes/cryptoService.js +6 -0
  41. package/dist/internal/nodes/cryptoService.js.map +1 -1
  42. package/dist/internal/nodes/index.d.ts +1 -1
  43. package/dist/internal/nodes/index.js +2 -2
  44. package/dist/internal/nodes/index.js.map +1 -1
  45. package/dist/internal/nodes/index.test.js +2 -2
  46. package/dist/internal/nodes/index.test.js.map +1 -1
  47. package/dist/internal/nodes/interface.d.ts +1 -1
  48. package/dist/internal/nodes/nodeName.d.ts +8 -0
  49. package/dist/internal/nodes/nodeName.js +30 -0
  50. package/dist/internal/nodes/nodeName.js.map +1 -0
  51. package/dist/internal/nodes/nodeName.test.d.ts +1 -0
  52. package/dist/internal/nodes/nodeName.test.js +50 -0
  53. package/dist/internal/nodes/nodeName.test.js.map +1 -0
  54. package/dist/internal/nodes/nodesAccess.d.ts +1 -1
  55. package/dist/internal/nodes/nodesAccess.js +4 -4
  56. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  57. package/dist/internal/nodes/nodesAccess.test.js +2 -2
  58. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  59. package/dist/internal/nodes/nodesManagement.d.ts +1 -0
  60. package/dist/internal/nodes/nodesManagement.js +30 -1
  61. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  62. package/dist/internal/nodes/nodesManagement.test.js +61 -0
  63. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  64. package/dist/internal/photos/albums.js +1 -1
  65. package/dist/internal/photos/albums.js.map +1 -1
  66. package/dist/internal/photos/apiService.d.ts +6 -0
  67. package/dist/internal/photos/apiService.js +16 -0
  68. package/dist/internal/photos/apiService.js.map +1 -1
  69. package/dist/internal/photos/index.d.ts +3 -1
  70. package/dist/internal/photos/index.js +6 -2
  71. package/dist/internal/photos/index.js.map +1 -1
  72. package/dist/internal/photos/interface.d.ts +4 -1
  73. package/dist/internal/photos/shares.d.ts +1 -1
  74. package/dist/internal/photos/shares.js +3 -3
  75. package/dist/internal/photos/shares.js.map +1 -1
  76. package/dist/internal/photos/timeline.d.ts +8 -1
  77. package/dist/internal/photos/timeline.js +36 -2
  78. package/dist/internal/photos/timeline.js.map +1 -1
  79. package/dist/internal/photos/timeline.test.d.ts +1 -0
  80. package/dist/internal/photos/timeline.test.js +99 -0
  81. package/dist/internal/photos/timeline.test.js.map +1 -0
  82. package/dist/internal/shares/cryptoService.js +3 -0
  83. package/dist/internal/shares/cryptoService.js.map +1 -1
  84. package/dist/internal/shares/index.d.ts +1 -0
  85. package/dist/internal/shares/index.js +3 -0
  86. package/dist/internal/shares/index.js.map +1 -1
  87. package/dist/internal/shares/interface.d.ts +8 -0
  88. package/dist/internal/shares/interface.js +10 -1
  89. package/dist/internal/shares/interface.js.map +1 -1
  90. package/dist/internal/shares/manager.d.ts +1 -1
  91. package/dist/internal/shares/manager.js +4 -4
  92. package/dist/internal/shares/manager.js.map +1 -1
  93. package/dist/internal/shares/manager.test.js +7 -7
  94. package/dist/internal/shares/manager.test.js.map +1 -1
  95. package/dist/internal/sharing/apiService.d.ts +3 -1
  96. package/dist/internal/sharing/apiService.js +16 -12
  97. package/dist/internal/sharing/apiService.js.map +1 -1
  98. package/dist/internal/sharing/index.d.ts +2 -1
  99. package/dist/internal/sharing/index.js +6 -2
  100. package/dist/internal/sharing/index.js.map +1 -1
  101. package/dist/internal/sharing/interface.d.ts +1 -1
  102. package/dist/internal/sharing/sharingAccess.js +1 -1
  103. package/dist/internal/sharing/sharingAccess.js.map +1 -1
  104. package/dist/internal/sharing/sharingAccess.test.js +1 -1
  105. package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
  106. package/dist/internal/sharing/sharingManagement.js +32 -14
  107. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  108. package/dist/internal/sharing/sharingManagement.test.js +46 -1
  109. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  110. package/dist/internal/sharingPublic/cryptoReporter.js +3 -0
  111. package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -1
  112. package/dist/internal/sharingPublic/index.d.ts +3 -0
  113. package/dist/internal/sharingPublic/index.js +5 -1
  114. package/dist/internal/sharingPublic/index.js.map +1 -1
  115. package/dist/internal/sharingPublic/shares.d.ts +1 -1
  116. package/dist/internal/sharingPublic/shares.js +1 -2
  117. package/dist/internal/sharingPublic/shares.js.map +1 -1
  118. package/dist/internal/upload/apiService.d.ts +0 -9
  119. package/dist/internal/upload/apiService.js +0 -16
  120. package/dist/internal/upload/apiService.js.map +1 -1
  121. package/dist/internal/upload/cryptoService.d.ts +0 -4
  122. package/dist/internal/upload/cryptoService.js +0 -6
  123. package/dist/internal/upload/cryptoService.js.map +1 -1
  124. package/dist/internal/upload/fileUploader.d.ts +0 -1
  125. package/dist/internal/upload/fileUploader.js +0 -4
  126. package/dist/internal/upload/fileUploader.js.map +1 -1
  127. package/dist/internal/upload/manager.d.ts +0 -1
  128. package/dist/internal/upload/manager.js +0 -51
  129. package/dist/internal/upload/manager.js.map +1 -1
  130. package/dist/internal/upload/manager.test.js +0 -61
  131. package/dist/internal/upload/manager.test.js.map +1 -1
  132. package/dist/protonDriveClient.d.ts +17 -2
  133. package/dist/protonDriveClient.js +19 -1
  134. package/dist/protonDriveClient.js.map +1 -1
  135. package/dist/protonDrivePhotosClient.d.ts +119 -4
  136. package/dist/protonDrivePhotosClient.js +183 -10
  137. package/dist/protonDrivePhotosClient.js.map +1 -1
  138. package/dist/protonDrivePublicLinkClient.d.ts +33 -1
  139. package/dist/protonDrivePublicLinkClient.js +51 -2
  140. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  141. package/package.json +1 -1
  142. package/src/diagnostic/diagnostic.ts +27 -8
  143. package/src/diagnostic/index.ts +17 -2
  144. package/src/diagnostic/interface.ts +35 -9
  145. package/src/diagnostic/nodeUtils.ts +100 -0
  146. package/src/diagnostic/{sdkDiagnostic.ts → sdkDiagnosticBase.ts} +204 -204
  147. package/src/diagnostic/sdkDiagnosticMain.ts +95 -0
  148. package/src/diagnostic/sdkDiagnosticPhotos.ts +70 -0
  149. package/src/interface/index.ts +1 -1
  150. package/src/interface/upload.ts +1 -13
  151. package/src/internal/devices/interface.ts +1 -1
  152. package/src/internal/devices/manager.test.ts +3 -3
  153. package/src/internal/devices/manager.ts +1 -1
  154. package/src/internal/errors.test.ts +62 -1
  155. package/src/internal/errors.ts +27 -0
  156. package/src/internal/nodes/apiService.test.ts +1 -1
  157. package/src/internal/nodes/apiService.ts +42 -0
  158. package/src/internal/nodes/cryptoReporter.ts +6 -5
  159. package/src/internal/nodes/cryptoService.ts +9 -0
  160. package/src/internal/nodes/index.test.ts +2 -1
  161. package/src/internal/nodes/index.ts +2 -1
  162. package/src/internal/nodes/interface.ts +1 -1
  163. package/src/internal/nodes/nodeName.test.ts +57 -0
  164. package/src/internal/nodes/nodeName.ts +26 -0
  165. package/src/internal/nodes/nodesAccess.test.ts +2 -2
  166. package/src/internal/nodes/nodesAccess.ts +5 -5
  167. package/src/internal/nodes/nodesManagement.test.ts +65 -0
  168. package/src/internal/nodes/nodesManagement.ts +43 -1
  169. package/src/internal/photos/albums.ts +1 -1
  170. package/src/internal/photos/apiService.ts +40 -0
  171. package/src/internal/photos/index.ts +13 -1
  172. package/src/internal/photos/interface.ts +4 -1
  173. package/src/internal/photos/shares.ts +3 -3
  174. package/src/internal/photos/timeline.test.ts +116 -0
  175. package/src/internal/photos/timeline.ts +47 -2
  176. package/src/internal/shares/cryptoService.ts +5 -1
  177. package/src/internal/shares/index.ts +1 -0
  178. package/src/internal/shares/interface.ts +9 -0
  179. package/src/internal/shares/manager.test.ts +7 -7
  180. package/src/internal/shares/manager.ts +4 -4
  181. package/src/internal/sharing/apiService.ts +15 -12
  182. package/src/internal/sharing/index.ts +7 -1
  183. package/src/internal/sharing/interface.ts +1 -1
  184. package/src/internal/sharing/sharingAccess.test.ts +1 -1
  185. package/src/internal/sharing/sharingAccess.ts +1 -1
  186. package/src/internal/sharing/sharingManagement.test.ts +59 -1
  187. package/src/internal/sharing/sharingManagement.ts +33 -14
  188. package/src/internal/sharingPublic/cryptoReporter.ts +5 -1
  189. package/src/internal/sharingPublic/index.ts +5 -1
  190. package/src/internal/sharingPublic/shares.ts +1 -2
  191. package/src/internal/upload/apiService.ts +0 -39
  192. package/src/internal/upload/cryptoService.ts +0 -9
  193. package/src/internal/upload/fileUploader.ts +0 -5
  194. package/src/internal/upload/manager.test.ts +0 -65
  195. package/src/internal/upload/manager.ts +0 -64
  196. package/src/protonDriveClient.ts +21 -2
  197. package/src/protonDrivePhotosClient.ts +217 -9
  198. package/src/protonDrivePublicLinkClient.ts +77 -2
  199. package/dist/diagnostic/sdkDiagnostic.d.ts +0 -23
  200. package/dist/diagnostic/sdkDiagnostic.js +0 -320
  201. package/dist/diagnostic/sdkDiagnostic.js.map +0 -1
@@ -18,6 +18,13 @@ type GetTimelineResponse =
18
18
  type GetAlbumsResponse =
19
19
  drivePaths['/drive/photos/volumes/{volumeID}/albums']['get']['responses']['200']['content']['application/json'];
20
20
 
21
+ type PostPhotoDuplicateRequest = Extract<
22
+ drivePaths['/drive/volumes/{volumeID}/photos/duplicates']['post']['requestBody'],
23
+ { content: object }
24
+ >['content']['application/json'];
25
+ type PostPhotoDuplicateResponse =
26
+ drivePaths['/drive/volumes/{volumeID}/photos/duplicates']['post']['responses']['200']['content']['application/json'];
27
+
21
28
  /**
22
29
  * Provides API communication for fetching and manipulating photos and albums
23
30
  * metadata.
@@ -148,4 +155,37 @@ export class PhotosAPIService {
148
155
  anchor = response.AnchorID;
149
156
  }
150
157
  }
158
+
159
+ async checkPhotoDuplicates(
160
+ volumeId: string,
161
+ nameHashes: string[],
162
+ signal?: AbortSignal,
163
+ ): Promise<
164
+ {
165
+ nameHash: string;
166
+ contentHash: string;
167
+ nodeUid: string;
168
+ clientUid?: string;
169
+ }[]
170
+ > {
171
+ const response = await this.apiService.post<PostPhotoDuplicateRequest, PostPhotoDuplicateResponse>(
172
+ `drive/volumes/${volumeId}/photos/duplicates`,
173
+ {
174
+ NameHashes: nameHashes,
175
+ },
176
+ signal,
177
+ );
178
+
179
+ return response.DuplicateHashes.map((duplicate) => {
180
+ if (!duplicate.Hash || !duplicate.ContentHash || duplicate.LinkState !== 1 /* Active */) {
181
+ return undefined;
182
+ }
183
+ return {
184
+ nameHash: duplicate.Hash,
185
+ contentHash: duplicate.ContentHash,
186
+ nodeUid: makeNodeUid(volumeId, duplicate.LinkID),
187
+ clientUid: duplicate.ClientUID || undefined,
188
+ };
189
+ }).filter((duplicate) => duplicate !== undefined);
190
+ }
151
191
  }
@@ -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.
@@ -32,12 +36,20 @@ import {
32
36
  * including API communication, crypto, caching, and event handling.
33
37
  */
34
38
  export function initPhotosModule(
39
+ telemetry: ProtonDriveTelemetry,
35
40
  apiService: DriveAPIService,
41
+ driveCrypto: DriveCrypto,
36
42
  photoShares: PhotoSharesManager,
37
43
  nodesService: NodesService,
38
44
  ) {
39
45
  const api = new PhotosAPIService(apiService);
40
- const timeline = new PhotosTimeline(api, photoShares);
46
+ const timeline = new PhotosTimeline(
47
+ telemetry.getLogger('photos-timeline'),
48
+ api,
49
+ driveCrypto,
50
+ photoShares,
51
+ nodesService,
52
+ );
41
53
  const albums = new Albums(api, photoShares, nodesService);
42
54
 
43
55
  return {
@@ -4,7 +4,7 @@ import { DecryptedNode } from '../nodes';
4
4
  import { EncryptedShare } from '../shares';
5
5
 
6
6
  export interface SharesService {
7
- getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
7
+ getRootIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
8
8
  loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
9
9
  getSharePrivateKey(shareId: string): Promise<PrivateKey>;
10
10
  getMyFilesShareMemberEmailKey(): Promise<{
@@ -26,4 +26,7 @@ export interface SharesService {
26
26
  export interface NodesService {
27
27
  getNode(nodeUid: string): Promise<DecryptedNode>;
28
28
  iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode>;
29
+ getNodeKeys(nodeUid: string): Promise<{
30
+ hashKey?: Uint8Array;
31
+ }>;
29
32
  }
@@ -34,7 +34,7 @@ export class PhotoSharesManager {
34
34
  this.sharesService = sharesService;
35
35
  }
36
36
 
37
- async getOwnVolumeIDs(): Promise<VolumeShareNodeIDs> {
37
+ async getRootIDs(): Promise<VolumeShareNodeIDs> {
38
38
  if (this.photoRootIds) {
39
39
  return this.photoRootIds;
40
40
  }
@@ -113,7 +113,7 @@ export class PhotoSharesManager {
113
113
  }
114
114
 
115
115
  async isOwnVolume(volumeId: string): Promise<boolean> {
116
- const { volumeId: myVolumeId } = await this.getOwnVolumeIDs();
116
+ const { volumeId: myVolumeId } = await this.getRootIDs();
117
117
  if (volumeId === myVolumeId) {
118
118
  return true;
119
119
  }
@@ -121,7 +121,7 @@ export class PhotoSharesManager {
121
121
  }
122
122
 
123
123
  async getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType> {
124
- const { volumeId: myVolumeId } = await this.getOwnVolumeIDs();
124
+ const { volumeId: myVolumeId } = await this.getRootIDs();
125
125
  if (volumeId === myVolumeId) {
126
126
  return MetricVolumeType.OwnVolume;
127
127
  }
@@ -0,0 +1,116 @@
1
+ import { getMockLogger } from '../../tests/logger';
2
+ import { DriveCrypto } from '../../crypto';
3
+ import { makeNodeUid } from '../uids';
4
+ import { PhotosAPIService } from './apiService';
5
+ import { NodesService } from './interface';
6
+ import { PhotoSharesManager } from './shares';
7
+ import { PhotosTimeline } from './timeline';
8
+
9
+ describe('PhotosTimeline', () => {
10
+ let logger: ReturnType<typeof getMockLogger>;
11
+ let apiService: PhotosAPIService;
12
+ let driveCrypto: DriveCrypto;
13
+ let photoShares: PhotoSharesManager;
14
+ let nodesService: NodesService;
15
+ let timeline: PhotosTimeline;
16
+
17
+ const volumeId = 'volumeId';
18
+ const rootNodeId = 'rootNodeId';
19
+ const rootNodeUid = makeNodeUid(volumeId, rootNodeId);
20
+ const hashKey = new Uint8Array([1, 2, 3]);
21
+ const name = 'photo.jpg';
22
+ const nameHash = 'nameHash123';
23
+ const sha1 = 'sha1Hash123';
24
+ const contentHash = 'contentHash123';
25
+
26
+ beforeEach(() => {
27
+ logger = getMockLogger();
28
+ // @ts-expect-error No need to implement all methods for mocking
29
+ apiService = {
30
+ checkPhotoDuplicates: jest.fn(),
31
+ };
32
+ // @ts-expect-error No need to implement all methods for mocking
33
+ driveCrypto = {
34
+ generateLookupHash: jest.fn(),
35
+ };
36
+ // @ts-expect-error No need to implement all methods for mocking
37
+ photoShares = {
38
+ getRootIDs: jest.fn().mockResolvedValue({ volumeId, rootNodeId }),
39
+ };
40
+ // @ts-expect-error No need to implement all methods for mocking
41
+ nodesService = {
42
+ getNodeKeys: jest.fn().mockResolvedValue({ hashKey }),
43
+ };
44
+
45
+ timeline = new PhotosTimeline(logger, apiService, driveCrypto, photoShares, nodesService);
46
+ });
47
+
48
+ describe('isDuplicatePhoto', () => {
49
+ it('should not call sha1 callback when there is no name hash match', async () => {
50
+ const generateSha1 = jest.fn();
51
+ apiService.checkPhotoDuplicates = jest.fn().mockResolvedValue([]);
52
+ driveCrypto.generateLookupHash = jest.fn().mockResolvedValue(nameHash);
53
+
54
+ const result = await timeline.isDuplicatePhoto(name, generateSha1);
55
+
56
+ expect(result).toBe(false);
57
+ expect(generateSha1).not.toHaveBeenCalled();
58
+ expect(photoShares.getRootIDs).toHaveBeenCalled();
59
+ expect(nodesService.getNodeKeys).toHaveBeenCalledWith(rootNodeUid);
60
+ expect(driveCrypto.generateLookupHash).toHaveBeenCalledWith(name, hashKey);
61
+ expect(apiService.checkPhotoDuplicates).toHaveBeenCalledWith(volumeId, [nameHash], undefined);
62
+ });
63
+
64
+ it('should call sha1 callback and not logger when name hash match but content hash does not', async () => {
65
+ const generateSha1 = jest.fn().mockResolvedValue(sha1);
66
+ const duplicates = [
67
+ {
68
+ nameHash: nameHash,
69
+ contentHash: 'differentContentHash',
70
+ nodeUid: 'volumeId~node1',
71
+ },
72
+ ];
73
+ apiService.checkPhotoDuplicates = jest.fn().mockResolvedValue(duplicates);
74
+ driveCrypto.generateLookupHash = jest
75
+ .fn()
76
+ .mockResolvedValueOnce(nameHash)
77
+ .mockResolvedValueOnce(contentHash);
78
+
79
+ const result = await timeline.isDuplicatePhoto(name, generateSha1);
80
+
81
+ expect(result).toBe(false);
82
+ expect(generateSha1).toHaveBeenCalledTimes(1);
83
+ expect(driveCrypto.generateLookupHash).toHaveBeenCalledTimes(2);
84
+ expect(driveCrypto.generateLookupHash).toHaveBeenNthCalledWith(1, name, hashKey);
85
+ expect(driveCrypto.generateLookupHash).toHaveBeenNthCalledWith(2, sha1, hashKey);
86
+ expect(logger.debug).not.toHaveBeenCalled();
87
+ });
88
+
89
+ it('should call sha1 and logger when name and content hashes match', async () => {
90
+ const generateSha1 = jest.fn().mockResolvedValue(sha1);
91
+ const nodeUid1 = 'volumeId~node1';
92
+ const duplicates = [
93
+ {
94
+ nameHash: nameHash,
95
+ contentHash: contentHash,
96
+ nodeUid: nodeUid1,
97
+ },
98
+ ];
99
+ apiService.checkPhotoDuplicates = jest.fn().mockResolvedValue(duplicates);
100
+ driveCrypto.generateLookupHash = jest
101
+ .fn()
102
+ .mockResolvedValueOnce(nameHash)
103
+ .mockResolvedValueOnce(contentHash);
104
+
105
+ const result = await timeline.isDuplicatePhoto(name, generateSha1);
106
+
107
+ expect(result).toBe(true);
108
+ expect(generateSha1).toHaveBeenCalledTimes(1);
109
+ expect(logger.debug).toHaveBeenCalledTimes(1);
110
+ expect(logger.debug).toHaveBeenCalledWith(
111
+ `Duplicate photo found: name hash: ${nameHash}, content hash: ${contentHash}, node uids: ${nodeUid1}`,
112
+ );
113
+ });
114
+ });
115
+ });
116
+
@@ -1,4 +1,8 @@
1
+ import { DriveCrypto } from '../../crypto';
2
+ import { Logger } from '../../interface';
3
+ import { makeNodeUid } from '../uids';
1
4
  import { PhotosAPIService } from './apiService';
5
+ import { NodesService } from './interface';
2
6
  import { PhotoSharesManager } from './shares';
3
7
 
4
8
  /**
@@ -6,19 +10,60 @@ import { PhotoSharesManager } from './shares';
6
10
  */
7
11
  export class PhotosTimeline {
8
12
  constructor(
13
+ private logger: Logger,
9
14
  private apiService: PhotosAPIService,
15
+ private driveCrypto: DriveCrypto,
10
16
  private photoShares: PhotoSharesManager,
17
+ private nodesService: NodesService,
11
18
  ) {
19
+ this.logger = logger;
12
20
  this.apiService = apiService;
21
+ this.driveCrypto = driveCrypto;
13
22
  this.photoShares = photoShares;
23
+ this.nodesService = nodesService;
14
24
  }
15
25
 
16
- async* iterateTimeline(signal?: AbortSignal): AsyncGenerator<{
26
+ async *iterateTimeline(signal?: AbortSignal): AsyncGenerator<{
17
27
  nodeUid: string;
18
28
  captureTime: Date;
19
29
  tags: number[];
20
30
  }> {
21
- const { volumeId } = await this.photoShares.getOwnVolumeIDs();
31
+ const { volumeId } = await this.photoShares.getRootIDs();
22
32
  yield* this.apiService.iterateTimeline(volumeId, signal);
23
33
  }
34
+
35
+ async isDuplicatePhoto(name: string, generateSha1: () => Promise<string>, signal?: AbortSignal): Promise<boolean> {
36
+ const { volumeId, rootNodeId } = await this.photoShares.getRootIDs();
37
+ const rootNodeUid = makeNodeUid(volumeId, rootNodeId);
38
+ const { hashKey } = await this.nodesService.getNodeKeys(rootNodeUid);
39
+ if (!hashKey) {
40
+ throw new Error('Hash key of photo root node not found');
41
+ }
42
+
43
+ const nameHash = await this.driveCrypto.generateLookupHash(name, hashKey);
44
+ const duplicates = await this.apiService.checkPhotoDuplicates(volumeId, [nameHash], signal);
45
+
46
+ if (duplicates.length === 0) {
47
+ return false;
48
+ }
49
+
50
+ // Generate the SHA1 only when there is any matching node hash to avoid
51
+ // computing it for every node as in most cases there is no match.
52
+ const sha1 = await generateSha1();
53
+ const contentHash = await this.driveCrypto.generateLookupHash(sha1, hashKey);
54
+
55
+ const matchingDuplicates = duplicates.filter(
56
+ (duplicate) => duplicate.nameHash === nameHash && duplicate.contentHash === contentHash,
57
+ );
58
+
59
+ if (matchingDuplicates.length === 0) {
60
+ return false;
61
+ }
62
+
63
+ const nodeUids = matchingDuplicates.map((duplicate) => duplicate.nodeUid);
64
+ this.logger.debug(
65
+ `Duplicate photo found: name hash: ${nameHash}, content hash: ${contentHash}, node uids: ${nodeUids}`,
66
+ );
67
+ return true;
68
+ }
24
69
  }
@@ -9,7 +9,7 @@ import {
9
9
  MetricVolumeType,
10
10
  } from '../../interface';
11
11
  import { DriveCrypto, PrivateKey, VERIFICATION_STATUS } from '../../crypto';
12
- import { getVerificationMessage } from '../errors';
12
+ import { getVerificationMessage, isNotApplicationError } from '../errors';
13
13
  import {
14
14
  EncryptedRootShare,
15
15
  DecryptedRootShare,
@@ -119,6 +119,10 @@ export class SharesCryptoService {
119
119
  }
120
120
 
121
121
  private reportDecryptionError(share: EncryptedRootShare, error?: unknown) {
122
+ if (isNotApplicationError(error)) {
123
+ return;
124
+ }
125
+
122
126
  if (this.reportedDecryptionErrors.has(share.shareId)) {
123
127
  return;
124
128
  }
@@ -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.
@@ -50,7 +50,7 @@ describe('SharesManager', () => {
50
50
  manager = new SharesManager(getMockLogger(), apiService, cache, cryptoCache, cryptoService, account);
51
51
  });
52
52
 
53
- describe('getOwnVolumeIDs', () => {
53
+ describe('getRootIDs', () => {
54
54
  const myFilesShare = {
55
55
  shareId: 'myFilesShareId',
56
56
  volumeId: 'myFilesVolumeId',
@@ -71,8 +71,8 @@ describe('SharesManager', () => {
71
71
  cryptoService.decryptRootShare = jest.fn().mockResolvedValue({ share: myFilesShare, key });
72
72
 
73
73
  // Calling twice to check if it loads only once.
74
- await manager.getOwnVolumeIDs();
75
- const result = await manager.getOwnVolumeIDs();
74
+ await manager.getRootIDs();
75
+ const result = await manager.getRootIDs();
76
76
 
77
77
  expect(result).toStrictEqual(myFilesShare);
78
78
  expect(apiService.getMyFiles).toHaveBeenCalledTimes(1);
@@ -103,7 +103,7 @@ describe('SharesManager', () => {
103
103
  });
104
104
  apiService.createVolume = jest.fn().mockResolvedValue(myFilesShare);
105
105
 
106
- const result = await manager.getOwnVolumeIDs();
106
+ const result = await manager.getRootIDs();
107
107
 
108
108
  expect(result).toStrictEqual(myFilesShare);
109
109
  expect(cryptoService.decryptRootShare).not.toHaveBeenCalled();
@@ -113,7 +113,7 @@ describe('SharesManager', () => {
113
113
  it('should throw on unknown error', async () => {
114
114
  apiService.getMyFiles = jest.fn().mockRejectedValue(new Error('Some error'));
115
115
 
116
- await expect(manager.getOwnVolumeIDs()).rejects.toThrow('Some error');
116
+ await expect(manager.getRootIDs()).rejects.toThrow('Some error');
117
117
  expect(cryptoService.decryptRootShare).not.toHaveBeenCalled();
118
118
  expect(apiService.createVolume).not.toHaveBeenCalled();
119
119
  });
@@ -142,7 +142,7 @@ describe('SharesManager', () => {
142
142
 
143
143
  describe('getMyFilesShareMemberEmailKey', () => {
144
144
  it('should return cached volume email key', async () => {
145
- jest.spyOn(manager, 'getOwnVolumeIDs').mockResolvedValue({ volumeId: 'volumeId' } as VolumeShareNodeIDs);
145
+ jest.spyOn(manager, 'getRootIDs').mockResolvedValue({ volumeId: 'volumeId' } as VolumeShareNodeIDs);
146
146
  cache.getVolume = jest.fn().mockResolvedValue({ addressId: 'addressId' });
147
147
  account.getOwnAddress = jest
148
148
  .fn()
@@ -158,7 +158,7 @@ describe('SharesManager', () => {
158
158
  });
159
159
 
160
160
  it('should load volume email key if not in cache', async () => {
161
- jest.spyOn(manager, 'getOwnVolumeIDs').mockResolvedValue({ volumeId: 'volumeId' } as VolumeShareNodeIDs);
161
+ jest.spyOn(manager, 'getRootIDs').mockResolvedValue({ volumeId: 'volumeId' } as VolumeShareNodeIDs);
162
162
  const share = {
163
163
  volumeId: 'volumeId',
164
164
  shareId: 'shareId',
@@ -46,7 +46,7 @@ export class SharesManager {
46
46
  *
47
47
  * If the default volume or My files section doesn't exist, it creates it.
48
48
  */
49
- async getOwnVolumeIDs(): Promise<VolumeShareNodeIDs> {
49
+ async getRootIDs(): Promise<VolumeShareNodeIDs> {
50
50
  if (this.myFilesIds) {
51
51
  return this.myFilesIds;
52
52
  }
@@ -140,7 +140,7 @@ export class SharesManager {
140
140
  addressKey: PrivateKey;
141
141
  addressKeyId: string;
142
142
  }> {
143
- const { volumeId } = await this.getOwnVolumeIDs();
143
+ const { volumeId } = await this.getRootIDs();
144
144
 
145
145
  try {
146
146
  const { addressId } = await this.cache.getVolume(volumeId);
@@ -196,11 +196,11 @@ export class SharesManager {
196
196
  }
197
197
 
198
198
  async isOwnVolume(volumeId: string): Promise<boolean> {
199
- return (await this.getOwnVolumeIDs()).volumeId === volumeId;
199
+ return (await this.getRootIDs()).volumeId === volumeId;
200
200
  }
201
201
 
202
202
  async getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType> {
203
- const { volumeId: myVolumeId } = await this.getOwnVolumeIDs();
203
+ const { volumeId: myVolumeId } = await this.getRootIDs();
204
204
 
205
205
  // SDK doesn't support public sharing yet, also public sharing
206
206
  // doesn't use a volume but shareURL, thus we can simplify and
@@ -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
  );
@@ -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);
@@ -142,7 +142,7 @@ export interface PublicLinkWithCreatorEmail extends PublicLink {
142
142
  * Interface describing the dependencies to the shares module.
143
143
  */
144
144
  export interface SharesService {
145
- getOwnVolumeIDs(): Promise<{ volumeId: string }>;
145
+ getRootIDs(): Promise<{ volumeId: string }>;
146
146
  loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
147
147
  getMyFilesShareMemberEmailKey(): Promise<{
148
148
  email: string;
@@ -93,7 +93,7 @@ describe('SharingAccess', () => {
93
93
 
94
94
  // @ts-expect-error No need to implement all methods for mocking
95
95
  sharesService = {
96
- getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
96
+ getRootIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
97
97
  loadEncryptedShare: jest.fn().mockResolvedValue({
98
98
  id: 'shareId',
99
99
  membership: { memberUid: 'memberUid' },
@@ -40,7 +40,7 @@ export class SharingAccess {
40
40
  const nodeUids = await this.cache.getSharedByMeNodeUids();
41
41
  yield* this.iterateSharedNodesFromCache(nodeUids, signal);
42
42
  } catch {
43
- const { volumeId } = await this.sharesService.getOwnVolumeIDs();
43
+ const { volumeId } = await this.sharesService.getRootIDs();
44
44
  const nodeUidsIterator = this.apiService.iterateSharedNodeUids(volumeId, signal);
45
45
  yield* this.iterateSharedNodesFromAPI(
46
46
  nodeUidsIterator,