@protontech/drive-sdk 0.7.0 → 0.7.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 (95) hide show
  1. package/dist/interface/index.d.ts +1 -0
  2. package/dist/interface/index.js.map +1 -1
  3. package/dist/interface/nodes.d.ts +14 -10
  4. package/dist/interface/nodes.js +5 -8
  5. package/dist/interface/nodes.js.map +1 -1
  6. package/dist/interface/photos.d.ts +62 -0
  7. package/dist/interface/photos.js +3 -0
  8. package/dist/interface/photos.js.map +1 -0
  9. package/dist/internal/apiService/driveTypes.d.ts +1294 -517
  10. package/dist/internal/errors.d.ts +1 -0
  11. package/dist/internal/errors.js +4 -0
  12. package/dist/internal/errors.js.map +1 -1
  13. package/dist/internal/nodes/apiService.d.ts +60 -9
  14. package/dist/internal/nodes/apiService.js +125 -81
  15. package/dist/internal/nodes/apiService.js.map +1 -1
  16. package/dist/internal/nodes/apiService.test.js +2 -0
  17. package/dist/internal/nodes/apiService.test.js.map +1 -1
  18. package/dist/internal/nodes/cache.d.ts +16 -8
  19. package/dist/internal/nodes/cache.js +19 -5
  20. package/dist/internal/nodes/cache.js.map +1 -1
  21. package/dist/internal/nodes/cache.test.js +1 -0
  22. package/dist/internal/nodes/cache.test.js.map +1 -1
  23. package/dist/internal/nodes/cryptoService.d.ts +1 -1
  24. package/dist/internal/nodes/cryptoService.js +4 -4
  25. package/dist/internal/nodes/cryptoService.js.map +1 -1
  26. package/dist/internal/nodes/cryptoService.test.js +3 -3
  27. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  28. package/dist/internal/nodes/events.d.ts +2 -2
  29. package/dist/internal/nodes/events.js.map +1 -1
  30. package/dist/internal/nodes/index.test.js +1 -0
  31. package/dist/internal/nodes/index.test.js.map +1 -1
  32. package/dist/internal/nodes/interface.d.ts +1 -0
  33. package/dist/internal/nodes/nodesAccess.d.ts +29 -20
  34. package/dist/internal/nodes/nodesAccess.js +41 -29
  35. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  36. package/dist/internal/nodes/nodesManagement.d.ts +32 -12
  37. package/dist/internal/nodes/nodesManagement.js +30 -13
  38. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  39. package/dist/internal/nodes/nodesManagement.test.js +39 -4
  40. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  41. package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
  42. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  43. package/dist/internal/photos/albums.d.ts +2 -2
  44. package/dist/internal/photos/albums.js.map +1 -1
  45. package/dist/internal/photos/index.d.ts +19 -3
  46. package/dist/internal/photos/index.js +38 -8
  47. package/dist/internal/photos/index.js.map +1 -1
  48. package/dist/internal/photos/interface.d.ts +18 -9
  49. package/dist/internal/photos/nodes.d.ts +57 -0
  50. package/dist/internal/photos/nodes.js +165 -0
  51. package/dist/internal/photos/nodes.js.map +1 -0
  52. package/dist/internal/photos/timeline.d.ts +2 -2
  53. package/dist/internal/photos/timeline.js.map +1 -1
  54. package/dist/internal/photos/timeline.test.js.map +1 -1
  55. package/dist/protonDriveClient.d.ts +10 -1
  56. package/dist/protonDriveClient.js +18 -3
  57. package/dist/protonDriveClient.js.map +1 -1
  58. package/dist/protonDrivePhotosClient.d.ts +8 -8
  59. package/dist/protonDrivePhotosClient.js +8 -9
  60. package/dist/protonDrivePhotosClient.js.map +1 -1
  61. package/dist/protonDrivePublicLinkClient.d.ts +7 -1
  62. package/dist/protonDrivePublicLinkClient.js +9 -0
  63. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  64. package/dist/transformers.d.ts +7 -2
  65. package/dist/transformers.js +37 -0
  66. package/dist/transformers.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/interface/index.ts +1 -0
  69. package/src/interface/nodes.ts +14 -11
  70. package/src/interface/photos.ts +67 -0
  71. package/src/internal/apiService/driveTypes.ts +1294 -517
  72. package/src/internal/errors.ts +4 -0
  73. package/src/internal/nodes/apiService.test.ts +2 -0
  74. package/src/internal/nodes/apiService.ts +187 -114
  75. package/src/internal/nodes/cache.test.ts +1 -0
  76. package/src/internal/nodes/cache.ts +32 -13
  77. package/src/internal/nodes/cryptoService.test.ts +13 -3
  78. package/src/internal/nodes/cryptoService.ts +4 -4
  79. package/src/internal/nodes/events.ts +2 -2
  80. package/src/internal/nodes/index.test.ts +1 -0
  81. package/src/internal/nodes/interface.ts +1 -0
  82. package/src/internal/nodes/nodesAccess.ts +82 -54
  83. package/src/internal/nodes/nodesManagement.test.ts +48 -4
  84. package/src/internal/nodes/nodesManagement.ts +76 -26
  85. package/src/internal/nodes/nodesRevisions.ts +3 -3
  86. package/src/internal/photos/albums.ts +2 -2
  87. package/src/internal/photos/index.ts +45 -3
  88. package/src/internal/photos/interface.ts +21 -9
  89. package/src/internal/photos/nodes.ts +233 -0
  90. package/src/internal/photos/timeline.test.ts +2 -2
  91. package/src/internal/photos/timeline.ts +2 -2
  92. package/src/protonDriveClient.ts +20 -3
  93. package/src/protonDrivePhotosClient.ts +23 -23
  94. package/src/protonDrivePublicLinkClient.ts +11 -0
  95. package/src/transformers.ts +49 -2
@@ -6,6 +6,10 @@ import {
6
6
  ProtonDriveEntitiesCache,
7
7
  ProtonDriveTelemetry,
8
8
  } from '../../interface';
9
+ import { NodesCryptoService } from '../nodes/cryptoService';
10
+ import { NodesCryptoReporter } from '../nodes/cryptoReporter';
11
+ import { NodesCryptoCache } from '../nodes/cryptoCache';
12
+ import { ShareTargetType } from '../shares';
9
13
  import { SharesCache } from '../shares/cache';
10
14
  import { SharesCryptoCache } from '../shares/cryptoCache';
11
15
  import { SharesCryptoService } from '../shares/cryptoService';
@@ -14,7 +18,8 @@ import { UploadTelemetry } from '../upload/telemetry';
14
18
  import { UploadQueue } from '../upload/queue';
15
19
  import { Albums } from './albums';
16
20
  import { PhotosAPIService } from './apiService';
17
- import { NodesService, SharesService } from './interface';
21
+ import { SharesService } from './interface';
22
+ import { PhotosNodesAPIService, PhotosNodesAccess, PhotosNodesCache, PhotosNodesManagement } from './nodes';
18
23
  import { PhotoSharesManager } from './shares';
19
24
  import { PhotosTimeline } from './timeline';
20
25
  import {
@@ -24,7 +29,10 @@ import {
24
29
  PhotoUploadManager,
25
30
  PhotoUploadMetadata,
26
31
  } from './upload';
27
- import { ShareTargetType } from '../shares';
32
+ import { NodesRevisons } from '../nodes/nodesRevisions';
33
+ import { NodesEventsHandler } from '../nodes/events';
34
+
35
+ export type { DecryptedPhotoNode } from './interface';
28
36
 
29
37
  // Only photos and albums can be shared in photos volume.
30
38
  export const PHOTOS_SHARE_TARGET_TYPES = [ShareTargetType.Photo, ShareTargetType.Album];
@@ -40,7 +48,7 @@ export function initPhotosModule(
40
48
  apiService: DriveAPIService,
41
49
  driveCrypto: DriveCrypto,
42
50
  photoShares: PhotoSharesManager,
43
- nodesService: NodesService,
51
+ nodesService: PhotosNodesAccess,
44
52
  ) {
45
53
  const api = new PhotosAPIService(apiService);
46
54
  const timeline = new PhotosTimeline(
@@ -89,6 +97,40 @@ export function initPhotoSharesModule(
89
97
  );
90
98
  }
91
99
 
100
+ /**
101
+ * Provides facade for the photo nodes module.
102
+ *
103
+ * The photo nodes module wraps the core nodes module and adds photo specific
104
+ * metadata. It provides the same interface so it can be used in the same way.
105
+ */
106
+ export function initPhotosNodesModule(
107
+ telemetry: ProtonDriveTelemetry,
108
+ apiService: DriveAPIService,
109
+ driveEntitiesCache: ProtonDriveEntitiesCache,
110
+ driveCryptoCache: ProtonDriveCryptoCache,
111
+ account: ProtonDriveAccount,
112
+ driveCrypto: DriveCrypto,
113
+ sharesService: PhotoSharesManager,
114
+ clientUid: string | undefined,
115
+ ) {
116
+ const api = new PhotosNodesAPIService(telemetry.getLogger('nodes-api'), apiService, clientUid);
117
+ const cache = new PhotosNodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
118
+ const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
119
+ const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
120
+ const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
121
+ const nodesAccess = new PhotosNodesAccess(telemetry, api, cache, cryptoCache, cryptoService, sharesService);
122
+ const nodesEventHandler = new NodesEventsHandler(telemetry.getLogger('nodes-events'), cache);
123
+ const nodesManagement = new PhotosNodesManagement(api, cryptoCache, cryptoService, nodesAccess);
124
+ const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
125
+
126
+ return {
127
+ access: nodesAccess,
128
+ management: nodesManagement,
129
+ revisions: nodesRevisions,
130
+ eventHandler: nodesEventHandler,
131
+ };
132
+ }
133
+
92
134
  /**
93
135
  * Provides facade for the photo upload module.
94
136
  *
@@ -1,6 +1,6 @@
1
1
  import { PrivateKey } from '../../crypto';
2
- import { MissingNode, MetricVolumeType } from '../../interface';
3
- import { DecryptedNode } from '../nodes';
2
+ import { MetricVolumeType, PhotoAttributes } from '../../interface';
3
+ import { DecryptedNode, EncryptedNode, DecryptedUnparsedNode } from '../nodes/interface';
4
4
  import { EncryptedShare } from '../shares';
5
5
 
6
6
  export interface SharesService {
@@ -23,10 +23,22 @@ export interface SharesService {
23
23
  getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType>;
24
24
  }
25
25
 
26
- export interface NodesService {
27
- getNode(nodeUid: string): Promise<DecryptedNode>;
28
- iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode>;
29
- getNodeKeys(nodeUid: string): Promise<{
30
- hashKey?: Uint8Array;
31
- }>;
32
- }
26
+ export type EncryptedPhotoNode = EncryptedNode & {
27
+ photo?: EcnryptedPhotoAttributes;
28
+ };
29
+
30
+ export type DecryptedUnparsedPhotoNode = DecryptedUnparsedNode & {
31
+ photo?: PhotoAttributes;
32
+ };
33
+
34
+ export type DecryptedPhotoNode = DecryptedNode & {
35
+ photo?: PhotoAttributes;
36
+ };
37
+
38
+ export type EcnryptedPhotoAttributes = Omit<PhotoAttributes, 'albums'> & {
39
+ contentHash?: string;
40
+ albums: (PhotoAttributes['albums'][0] & {
41
+ nameHash?: string;
42
+ contentHash?: string;
43
+ })[];
44
+ };
@@ -0,0 +1,233 @@
1
+ import { PrivateKey } from '../../crypto';
2
+ import { DecryptionError } from '../../errors';
3
+ import { NodeType } from '../../interface';
4
+ import { drivePaths } from '../apiService';
5
+ import { NodeAPIServiceBase, linkToEncryptedNode, linkToEncryptedNodeBaseMetadata } from '../nodes/apiService';
6
+ import { NodesCacheBase, serialiseNode, deserialiseNode } from '../nodes/cache';
7
+ import { NodesCryptoService } from '../nodes/cryptoService';
8
+ import { DecryptedNodeKeys } from '../nodes/interface';
9
+ import { NodesAccessBase, parseNode as parseNodeBase } from '../nodes/nodesAccess';
10
+ import { NodesManagementBase } from '../nodes/nodesManagement';
11
+ import { makeNodeUid } from '../uids';
12
+ import { EncryptedPhotoNode, DecryptedPhotoNode, DecryptedUnparsedPhotoNode } from './interface';
13
+
14
+ type PostLoadLinksMetadataRequest = Extract<
15
+ drivePaths['/drive/photos/volumes/{volumeID}/links']['post']['requestBody'],
16
+ { content: object }
17
+ >['content']['application/json'];
18
+ type PostLoadLinksMetadataResponse =
19
+ drivePaths['/drive/photos/volumes/{volumeID}/links']['post']['responses']['200']['content']['application/json'];
20
+
21
+ export class PhotosNodesAPIService extends NodeAPIServiceBase<
22
+ EncryptedPhotoNode,
23
+ PostLoadLinksMetadataResponse['Links'][0]
24
+ > {
25
+ protected async fetchNodeMetadata(volumeId: string, linkIds: string[], signal?: AbortSignal) {
26
+ const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
27
+ `drive/photos/volumes/${volumeId}/links`,
28
+ {
29
+ LinkIDs: linkIds,
30
+ },
31
+ signal,
32
+ );
33
+ return response.Links;
34
+ }
35
+
36
+ protected linkToEncryptedNode(
37
+ volumeId: string,
38
+ link: PostLoadLinksMetadataResponse['Links'][0],
39
+ isOwnVolumeId: boolean,
40
+ ): EncryptedPhotoNode {
41
+ const { baseNodeMetadata, baseCryptoNodeMetadata } = linkToEncryptedNodeBaseMetadata(
42
+ this.logger,
43
+ volumeId,
44
+ link,
45
+ isOwnVolumeId,
46
+ );
47
+
48
+ if (link.Link.Type === 2 && link.Photo && link.Photo.ActiveRevision) {
49
+ const node = linkToEncryptedNode(
50
+ this.logger,
51
+ volumeId,
52
+ { ...link, File: link.Photo, Folder: null },
53
+ isOwnVolumeId,
54
+ );
55
+ return {
56
+ ...node,
57
+ type: NodeType.Photo,
58
+ photo: {
59
+ captureTime: new Date(link.Photo.CaptureTime * 1000),
60
+ mainPhotoNodeUid: link.Photo.MainPhotoLinkID
61
+ ? makeNodeUid(volumeId, link.Photo.MainPhotoLinkID)
62
+ : undefined,
63
+ relatedPhotoNodeUids: link.Photo.RelatedPhotosLinkIDs.map((relatedLinkId) =>
64
+ makeNodeUid(volumeId, relatedLinkId),
65
+ ),
66
+ contentHash: link.Photo.ContentHash || undefined,
67
+ tags: link.Photo.Tags,
68
+ albums: link.Photo.Albums.map((album) => ({
69
+ nodeUid: makeNodeUid(volumeId, album.AlbumLinkID),
70
+ additionTime: new Date(album.AddedTime * 1000),
71
+ nameHash: album.Hash,
72
+ contentHash: album.ContentHash,
73
+ })),
74
+ },
75
+ };
76
+ }
77
+
78
+ if (link.Link.Type === 3) {
79
+ return {
80
+ ...baseNodeMetadata,
81
+ encryptedCrypto: {
82
+ ...baseCryptoNodeMetadata,
83
+ },
84
+ };
85
+ }
86
+
87
+ const baseLink = {
88
+ Link: link.Link,
89
+ Membership: link.Membership,
90
+ Sharing: link.Sharing,
91
+ // @ts-expect-error The photo link can have a folder type, but not always. If not set, it will use other paths.
92
+ Folder: link.Folder,
93
+ File: null, // The photo link metadata never returns a file type.
94
+ };
95
+ return linkToEncryptedNode(this.logger, volumeId, baseLink, isOwnVolumeId);
96
+ }
97
+ }
98
+
99
+ export class PhotosNodesCache extends NodesCacheBase<DecryptedPhotoNode> {
100
+ serialiseNode(node: DecryptedPhotoNode): string {
101
+ return serialiseNode(node);
102
+ }
103
+
104
+ // TODO: use better deserialisation with validation
105
+ deserialiseNode(nodeData: string): DecryptedPhotoNode {
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ const node = deserialiseNode(nodeData) as any;
108
+
109
+ if (
110
+ !node ||
111
+ typeof node !== 'object' ||
112
+ (typeof node.photo !== 'object' && node.photo !== undefined) ||
113
+ (typeof node.photo?.captureTime !== 'string' && node.folder?.captureTime !== undefined) ||
114
+ (typeof node.photo?.albums !== 'object' && node.photo?.albums !== undefined)
115
+ ) {
116
+ throw new Error(`Invalid node data: ${nodeData}`);
117
+ }
118
+
119
+ return {
120
+ ...node,
121
+ photo: !node.photo
122
+ ? undefined
123
+ : {
124
+ captureTime: new Date(node.photo.captureTime),
125
+ mainPhotoNodeUid: node.photo.mainPhotoNodeUid,
126
+ relatedPhotoNodeUids: node.photo.relatedPhotoNodeUids,
127
+ contentHash: node.photo.contentHash,
128
+ tags: node.photo.tags,
129
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
130
+ albums: node.photo.albums?.map((album: any) => ({
131
+ nodeUid: album.nodeUid,
132
+ additionTime: new Date(album.additionTime),
133
+ })),
134
+ },
135
+ } as DecryptedPhotoNode;
136
+ }
137
+ }
138
+
139
+ export class PhotosNodesAccess extends NodesAccessBase<EncryptedPhotoNode, DecryptedPhotoNode, PhotosNodesCryptoService> {
140
+ async getParentKeys(
141
+ node: Pick<EncryptedPhotoNode, 'uid' | 'parentUid' | 'shareId' | 'photo'>,
142
+ ): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
143
+ if (node.parentUid || node.shareId) {
144
+ return super.getParentKeys(node);
145
+ }
146
+
147
+ if (node.photo?.albums.length) {
148
+ // If photo is in multiple albums, we just need to get keys for one of them.
149
+ // Prefer to find a cached key first.
150
+ for (const album of node.photo.albums) {
151
+ try {
152
+ const keys = await this.cryptoCache.getNodeKeys(album.nodeUid);
153
+ return {
154
+ key: keys.key,
155
+ hashKey: keys.hashKey,
156
+ };
157
+ } catch {
158
+ // We ignore missing or invalid keys here, its just optimization.
159
+ // If it cannot be fixed, it will bubble up later when requesting
160
+ // the node keys for one of the albums.
161
+ }
162
+ }
163
+
164
+ const albumNodeUid = node.photo.albums[0].nodeUid;
165
+ return this.getNodeKeys(albumNodeUid);
166
+ }
167
+
168
+ // This is bug that should not happen.
169
+ // API cannot provide node without parent or share or album.
170
+ throw new Error('Node has neither parent node nor share nor album');
171
+ }
172
+
173
+ protected getDegradedUndecryptableNode(
174
+ encryptedNode: EncryptedPhotoNode,
175
+ error: DecryptionError,
176
+ ): DecryptedPhotoNode {
177
+ return this.getDegradedUndecryptableNodeBase(encryptedNode, error);
178
+ }
179
+
180
+ protected parseNode(unparsedNode: DecryptedUnparsedPhotoNode): DecryptedPhotoNode {
181
+ if (unparsedNode.type === NodeType.Photo) {
182
+ const node = parseNodeBase(this.logger, {
183
+ ...unparsedNode,
184
+ type: NodeType.File,
185
+ });
186
+ return {
187
+ ...node,
188
+ photo: unparsedNode.photo,
189
+ };
190
+ }
191
+
192
+ return parseNodeBase(this.logger, unparsedNode);
193
+ }
194
+ }
195
+
196
+ export class PhotosNodesCryptoService extends NodesCryptoService {
197
+ async decryptNode(
198
+ encryptedNode: EncryptedPhotoNode,
199
+ parentKey: PrivateKey,
200
+ ): Promise<{ node: DecryptedUnparsedPhotoNode; keys?: DecryptedNodeKeys }> {
201
+ const decryptedNode = await super.decryptNode(encryptedNode, parentKey);
202
+
203
+ if (decryptedNode.node.type === NodeType.Photo) {
204
+ return {
205
+ node: {
206
+ ...decryptedNode.node,
207
+ photo: encryptedNode.photo,
208
+ },
209
+ };
210
+ }
211
+
212
+ return decryptedNode;
213
+ }
214
+ }
215
+
216
+ export class PhotosNodesManagement extends NodesManagementBase<
217
+ EncryptedPhotoNode,
218
+ DecryptedPhotoNode,
219
+ PhotosNodesCryptoService
220
+ > {
221
+ protected generateNodeFolder(
222
+ nodeUid: string,
223
+ parentNodeUid: string,
224
+ name: string,
225
+ encryptedCrypto: {
226
+ hash: string;
227
+ encryptedName: string;
228
+ signatureEmail: string | null;
229
+ },
230
+ ): DecryptedPhotoNode {
231
+ return this.generateNodeFolderBase(nodeUid, parentNodeUid, name, encryptedCrypto);
232
+ }
233
+ }
@@ -2,7 +2,7 @@ import { getMockLogger } from '../../tests/logger';
2
2
  import { DriveCrypto } from '../../crypto';
3
3
  import { makeNodeUid } from '../uids';
4
4
  import { PhotosAPIService } from './apiService';
5
- import { NodesService } from './interface';
5
+ import { PhotosNodesAccess } from './nodes';
6
6
  import { PhotoSharesManager } from './shares';
7
7
  import { PhotosTimeline } from './timeline';
8
8
 
@@ -11,7 +11,7 @@ describe('PhotosTimeline', () => {
11
11
  let apiService: PhotosAPIService;
12
12
  let driveCrypto: DriveCrypto;
13
13
  let photoShares: PhotoSharesManager;
14
- let nodesService: NodesService;
14
+ let nodesService: PhotosNodesAccess;
15
15
  let timeline: PhotosTimeline;
16
16
 
17
17
  const volumeId = 'volumeId';
@@ -2,7 +2,7 @@ import { DriveCrypto } from '../../crypto';
2
2
  import { Logger } from '../../interface';
3
3
  import { makeNodeUid } from '../uids';
4
4
  import { PhotosAPIService } from './apiService';
5
- import { NodesService } from './interface';
5
+ import { PhotosNodesAccess } from './nodes';
6
6
  import { PhotoSharesManager } from './shares';
7
7
 
8
8
  /**
@@ -14,7 +14,7 @@ export class PhotosTimeline {
14
14
  private apiService: PhotosAPIService,
15
15
  private driveCrypto: DriveCrypto,
16
16
  private photoShares: PhotoSharesManager,
17
- private nodesService: NodesService,
17
+ private nodesService: PhotosNodesAccess,
18
18
  ) {
19
19
  this.logger = logger;
20
20
  this.apiService = apiService;
@@ -424,6 +424,12 @@ export class ProtonDriveClient {
424
424
  * The operation is performed node by node and the results are yielded
425
425
  * as they are available. Order of the results is not guaranteed.
426
426
  *
427
+ * The `nodeUids` can be a list of node entities or their UIDs, or a list
428
+ * of objects with `uid` and `name` properties where the name is the new
429
+ * name of the copied node. By default, the name is the same as the
430
+ * original node. Use `getAvailableName` to get the available name for the
431
+ * new node in the target parent node in case of a name conflict.
432
+ *
427
433
  * If one of the nodes fails to copy, the operation continues with the
428
434
  * rest of the nodes. Use `NodeResult` to check the status of the action.
429
435
  *
@@ -433,12 +439,23 @@ export class ProtonDriveClient {
433
439
  * @returns An async generator of the results of the copy operation
434
440
  */
435
441
  async *copyNodes(
436
- nodeUids: NodeOrUid[],
442
+ nodesOrNodeUidsOrWithNames: (NodeOrUid | { uid: string; name: string })[],
437
443
  newParentNodeUid: NodeOrUid,
438
444
  signal?: AbortSignal,
439
445
  ): AsyncGenerator<NodeResultWithNewUid> {
440
- this.logger.info(`Copying ${nodeUids.length} nodes to ${getUid(newParentNodeUid)}`);
441
- yield* this.nodes.management.copyNodes(getUids(nodeUids), getUid(newParentNodeUid), signal);
446
+ this.logger.info(`Copying ${nodesOrNodeUidsOrWithNames.length} nodes to ${getUid(newParentNodeUid)}`);
447
+
448
+ const nodeUidsOrWithNames = nodesOrNodeUidsOrWithNames.map((param) => {
449
+ if (typeof param === 'string') {
450
+ return param;
451
+ }
452
+ if ('uid' in param && 'name' in param && typeof param.uid === 'string' && typeof param.name === 'string') {
453
+ return { uid: param.uid, name: param.name };
454
+ }
455
+ return getUid(param);
456
+ });
457
+
458
+ yield* this.nodes.management.copyNodes(nodeUidsOrWithNames, getUid(newParentNodeUid), signal);
442
459
  }
443
460
 
444
461
  /**
@@ -2,12 +2,12 @@ import {
2
2
  Logger,
3
3
  ProtonDriveClientContructorParameters,
4
4
  NodeOrUid,
5
- MaybeMissingNode,
5
+ MaybeMissingPhotoNode,
6
6
  UploadMetadata,
7
7
  FileDownloader,
8
8
  FileUploader,
9
9
  SDKEvent,
10
- MaybeNode,
10
+ MaybePhotoNode,
11
11
  ThumbnailType,
12
12
  ThumbnailResult,
13
13
  ShareNodeSettings,
@@ -22,22 +22,22 @@ import { getConfig } from './config';
22
22
  import { DriveCrypto } from './crypto';
23
23
  import { Telemetry } from './telemetry';
24
24
  import {
25
- convertInternalMissingNodeIterator,
26
- convertInternalNode,
27
- convertInternalNodeIterator,
28
- convertInternalNodePromise,
25
+ convertInternalMissingPhotoNodeIterator,
26
+ convertInternalPhotoNode,
27
+ convertInternalPhotoNodeIterator,
28
+ convertInternalPhotoNodePromise,
29
29
  getUid,
30
30
  getUids,
31
31
  } from './transformers';
32
32
  import { DriveAPIService } from './internal/apiService';
33
33
  import { initDownloadModule } from './internal/download';
34
34
  import { DriveEventsService, DriveListener, EventSubscription } from './internal/events';
35
- import { initNodesModule } from './internal/nodes';
36
35
  import {
37
36
  PHOTOS_SHARE_TARGET_TYPES,
38
37
  initPhotosModule,
39
38
  initPhotoSharesModule,
40
39
  initPhotoUploadModule,
40
+ initPhotosNodesModule,
41
41
  } from './internal/photos';
42
42
  import { SDKEvents } from './internal/sdkEvents';
43
43
  import { initSharesModule } from './internal/shares';
@@ -56,7 +56,7 @@ export class ProtonDrivePhotosClient {
56
56
  private sdkEvents: SDKEvents;
57
57
  private events: DriveEventsService;
58
58
  private photoShares: ReturnType<typeof initPhotoSharesModule>;
59
- private nodes: ReturnType<typeof initNodesModule>;
59
+ private nodes: ReturnType<typeof initPhotosNodesModule>;
60
60
  private sharing: ReturnType<typeof initSharingModule>;
61
61
  private download: ReturnType<typeof initDownloadModule>;
62
62
  private upload: ReturnType<typeof initPhotoUploadModule>;
@@ -107,7 +107,7 @@ export class ProtonDrivePhotosClient {
107
107
  cryptoModule,
108
108
  coreShares,
109
109
  );
110
- this.nodes = initNodesModule(
110
+ this.nodes = initPhotosNodesModule(
111
111
  telemetry,
112
112
  apiService,
113
113
  entitiesCache,
@@ -224,9 +224,9 @@ export class ProtonDrivePhotosClient {
224
224
  *
225
225
  * See `ProtonDriveClient.iterateTrashedNodes` for more information.
226
226
  */
227
- async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<MaybeNode> {
227
+ async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<MaybePhotoNode> {
228
228
  this.logger.info('Iterating trashed nodes');
229
- yield* convertInternalNodeIterator(this.nodes.access.iterateTrashedNodes(signal));
229
+ yield * convertInternalPhotoNodeIterator(this.nodes.access.iterateTrashedNodes(signal));
230
230
  }
231
231
 
232
232
  /**
@@ -234,10 +234,10 @@ export class ProtonDrivePhotosClient {
234
234
  *
235
235
  * See `ProtonDriveClient.iterateNodes` for more information.
236
236
  */
237
- async *iterateNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<MaybeMissingNode> {
237
+ async *iterateNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<MaybeMissingPhotoNode> {
238
238
  this.logger.info(`Iterating ${nodeUids.length} nodes`);
239
239
  // TODO: expose photo type
240
- yield* convertInternalMissingNodeIterator(this.nodes.access.iterateNodes(getUids(nodeUids), signal));
240
+ yield * convertInternalMissingPhotoNodeIterator(this.nodes.access.iterateNodes(getUids(nodeUids), signal));
241
241
  }
242
242
 
243
243
  /**
@@ -245,9 +245,9 @@ export class ProtonDrivePhotosClient {
245
245
  *
246
246
  * See `ProtonDriveClient.getNode` for more information.
247
247
  */
248
- async getNode(nodeUid: NodeOrUid): Promise<MaybeNode> {
248
+ async getNode(nodeUid: NodeOrUid): Promise<MaybePhotoNode> {
249
249
  this.logger.info(`Getting node ${getUid(nodeUid)}`);
250
- return convertInternalNodePromise(this.nodes.access.getNode(getUid(nodeUid)));
250
+ return convertInternalPhotoNodePromise(this.nodes.access.getNode(getUid(nodeUid)));
251
251
  }
252
252
 
253
253
  /**
@@ -255,9 +255,9 @@ export class ProtonDrivePhotosClient {
255
255
  *
256
256
  * See `ProtonDriveClient.renameNode` for more information.
257
257
  */
258
- async renameNode(nodeUid: NodeOrUid, newName: string): Promise<MaybeNode> {
258
+ async renameNode(nodeUid: NodeOrUid, newName: string): Promise<MaybePhotoNode> {
259
259
  this.logger.info(`Renaming node ${getUid(nodeUid)}`);
260
- return convertInternalNodePromise(this.nodes.management.renameNode(getUid(nodeUid), newName));
260
+ return convertInternalPhotoNodePromise(this.nodes.management.renameNode(getUid(nodeUid), newName));
261
261
  }
262
262
 
263
263
  /**
@@ -305,9 +305,9 @@ export class ProtonDrivePhotosClient {
305
305
  *
306
306
  * See `ProtonDriveClient.iterateSharedNodes` for more information.
307
307
  */
308
- async *iterateSharedNodes(signal?: AbortSignal): AsyncGenerator<MaybeNode> {
308
+ async *iterateSharedNodes(signal?: AbortSignal): AsyncGenerator<MaybePhotoNode> {
309
309
  this.logger.info('Iterating shared nodes by me');
310
- yield* convertInternalNodeIterator(this.sharing.access.iterateSharedNodes(signal));
310
+ yield * convertInternalPhotoNodeIterator(this.sharing.access.iterateSharedNodes(signal));
311
311
  }
312
312
 
313
313
  /**
@@ -315,11 +315,11 @@ export class ProtonDrivePhotosClient {
315
315
  *
316
316
  * See `ProtonDriveClient.iterateSharedNodesWithMe` for more information.
317
317
  */
318
- async *iterateSharedNodesWithMe(signal?: AbortSignal): AsyncGenerator<MaybeNode> {
318
+ async *iterateSharedNodesWithMe(signal?: AbortSignal): AsyncGenerator<MaybePhotoNode> {
319
319
  this.logger.info('Iterating shared nodes with me');
320
320
 
321
321
  for await (const node of this.sharing.access.iterateSharedNodesWithMe(signal)) {
322
- yield convertInternalNode(node);
322
+ yield convertInternalPhotoNode(node);
323
323
  }
324
324
  }
325
325
 
@@ -482,9 +482,9 @@ export class ProtonDrivePhotosClient {
482
482
  *
483
483
  * The output is not sorted and the order of the nodes is not guaranteed.
484
484
  */
485
- async *iterateAlbums(signal?: AbortSignal): AsyncGenerator<MaybeNode> {
485
+ async *iterateAlbums(signal?: AbortSignal): AsyncGenerator<MaybePhotoNode> {
486
486
  this.logger.info('Iterating albums');
487
487
  // TODO: expose album type
488
- yield* convertInternalNodeIterator(this.photos.albums.iterateAlbums(signal));
488
+ yield * convertInternalPhotoNodeIterator(this.photos.albums.iterateAlbums(signal));
489
489
  }
490
490
  }
@@ -18,6 +18,7 @@ import {
18
18
  UploadMetadata,
19
19
  FileUploader,
20
20
  NodeResult,
21
+ SDKEvent,
21
22
  } from './interface';
22
23
  import { Telemetry } from './telemetry';
23
24
  import {
@@ -164,6 +165,16 @@ export class ProtonDrivePublicLinkClient {
164
165
  };
165
166
  }
166
167
 
168
+ /**
169
+ * Subscribes to the general SDK events.
170
+ *
171
+ * See `ProtonDriveClient.onMessage` for more information.
172
+ */
173
+ onMessage(eventName: SDKEvent, callback: () => void): () => void {
174
+ this.logger.debug(`Subscribing to event ${eventName}`);
175
+ return this.sdkEvents.addListener(eventName, callback);
176
+ }
177
+
167
178
  /**
168
179
  * @returns The root folder to the public link.
169
180
  */