@protontech/drive-sdk 0.10.0 → 0.12.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 (125) hide show
  1. package/dist/crypto/driveCrypto.d.ts +20 -2
  2. package/dist/crypto/driveCrypto.js +72 -14
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/crypto/driveCrypto.test.js +3 -2
  5. package/dist/crypto/driveCrypto.test.js.map +1 -1
  6. package/dist/crypto/interface.d.ts +22 -8
  7. package/dist/crypto/openPGPCrypto.d.ts +30 -7
  8. package/dist/crypto/openPGPCrypto.js +46 -8
  9. package/dist/crypto/openPGPCrypto.js.map +1 -1
  10. package/dist/diagnostic/telemetry.js +3 -0
  11. package/dist/diagnostic/telemetry.js.map +1 -1
  12. package/dist/interface/featureFlags.d.ts +4 -1
  13. package/dist/interface/featureFlags.js +5 -0
  14. package/dist/interface/featureFlags.js.map +1 -1
  15. package/dist/interface/index.d.ts +2 -0
  16. package/dist/interface/index.js +5 -1
  17. package/dist/interface/index.js.map +1 -1
  18. package/dist/interface/photos.d.ts +13 -1
  19. package/dist/interface/photos.js +14 -0
  20. package/dist/interface/photos.js.map +1 -1
  21. package/dist/interface/telemetry.d.ts +12 -1
  22. package/dist/interface/telemetry.js.map +1 -1
  23. package/dist/internal/apiService/apiService.d.ts +1 -1
  24. package/dist/internal/apiService/apiService.js +2 -2
  25. package/dist/internal/apiService/apiService.js.map +1 -1
  26. package/dist/internal/nodes/apiService.js.map +1 -1
  27. package/dist/internal/nodes/nodesAccess.js +1 -1
  28. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  29. package/dist/internal/photos/addToAlbum.d.ts +1 -5
  30. package/dist/internal/photos/addToAlbum.js +8 -87
  31. package/dist/internal/photos/addToAlbum.js.map +1 -1
  32. package/dist/internal/photos/{albums.d.ts → albumsManager.d.ts} +1 -1
  33. package/dist/internal/photos/{albums.js → albumsManager.js} +4 -4
  34. package/dist/internal/photos/albumsManager.js.map +1 -0
  35. package/dist/internal/photos/{albums.test.js → albumsManager.test.js} +3 -3
  36. package/dist/internal/photos/albumsManager.test.js.map +1 -0
  37. package/dist/internal/photos/apiService.d.ts +8 -4
  38. package/dist/internal/photos/apiService.js +38 -6
  39. package/dist/internal/photos/apiService.js.map +1 -1
  40. package/dist/internal/photos/index.d.ts +7 -5
  41. package/dist/internal/photos/index.js +7 -4
  42. package/dist/internal/photos/index.js.map +1 -1
  43. package/dist/internal/photos/interface.d.ts +3 -26
  44. package/dist/internal/photos/interface.js +0 -14
  45. package/dist/internal/photos/interface.js.map +1 -1
  46. package/dist/internal/photos/nodes.js +1 -1
  47. package/dist/internal/photos/nodes.js.map +1 -1
  48. package/dist/internal/photos/photosManager.d.ts +22 -0
  49. package/dist/internal/photos/photosManager.js +101 -0
  50. package/dist/internal/photos/photosManager.js.map +1 -0
  51. package/dist/internal/photos/photosManager.test.d.ts +1 -0
  52. package/dist/internal/photos/photosManager.test.js +222 -0
  53. package/dist/internal/photos/photosManager.test.js.map +1 -0
  54. package/dist/internal/photos/photosTransferPayloadBuilder.d.ts +57 -0
  55. package/dist/internal/photos/photosTransferPayloadBuilder.js +113 -0
  56. package/dist/internal/photos/photosTransferPayloadBuilder.js.map +1 -0
  57. package/dist/internal/photos/photosTransferPayloadBuilder.test.d.ts +1 -0
  58. package/dist/internal/photos/photosTransferPayloadBuilder.test.js +289 -0
  59. package/dist/internal/photos/photosTransferPayloadBuilder.test.js.map +1 -0
  60. package/dist/internal/photos/upload.d.ts +2 -2
  61. package/dist/internal/photos/upload.js +3 -3
  62. package/dist/internal/photos/upload.js.map +1 -1
  63. package/dist/internal/sharingPublic/nodes.js +11 -4
  64. package/dist/internal/sharingPublic/nodes.js.map +1 -1
  65. package/dist/internal/upload/apiService.d.ts +0 -4
  66. package/dist/internal/upload/apiService.js +0 -4
  67. package/dist/internal/upload/apiService.js.map +1 -1
  68. package/dist/internal/upload/cryptoService.d.ts +4 -2
  69. package/dist/internal/upload/cryptoService.js +18 -6
  70. package/dist/internal/upload/cryptoService.js.map +1 -1
  71. package/dist/internal/upload/index.d.ts +2 -2
  72. package/dist/internal/upload/index.js +2 -2
  73. package/dist/internal/upload/index.js.map +1 -1
  74. package/dist/internal/upload/interface.d.ts +1 -1
  75. package/dist/internal/upload/streamUploader.d.ts +1 -1
  76. package/dist/internal/upload/streamUploader.js +12 -13
  77. package/dist/internal/upload/streamUploader.js.map +1 -1
  78. package/dist/internal/upload/streamUploader.test.js +28 -4
  79. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  80. package/dist/protonDriveClient.js +2 -2
  81. package/dist/protonDriveClient.js.map +1 -1
  82. package/dist/protonDrivePhotosClient.d.ts +20 -3
  83. package/dist/protonDrivePhotosClient.js +27 -3
  84. package/dist/protonDrivePhotosClient.js.map +1 -1
  85. package/dist/protonDrivePublicLinkClient.d.ts +3 -2
  86. package/dist/protonDrivePublicLinkClient.js +7 -3
  87. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/crypto/driveCrypto.test.ts +3 -1
  90. package/src/crypto/driveCrypto.ts +82 -7
  91. package/src/crypto/interface.ts +21 -8
  92. package/src/crypto/openPGPCrypto.ts +68 -8
  93. package/src/diagnostic/telemetry.ts +3 -0
  94. package/src/interface/featureFlags.ts +5 -1
  95. package/src/interface/index.ts +2 -0
  96. package/src/interface/photos.ts +14 -1
  97. package/src/interface/telemetry.ts +14 -1
  98. package/src/internal/apiService/apiService.ts +6 -2
  99. package/src/internal/nodes/apiService.ts +2 -2
  100. package/src/internal/nodes/nodesAccess.ts +1 -1
  101. package/src/internal/photos/addToAlbum.ts +29 -136
  102. package/src/internal/photos/{albums.test.ts → albumsManager.test.ts} +3 -3
  103. package/src/internal/photos/{albums.ts → albumsManager.ts} +1 -1
  104. package/src/internal/photos/apiService.ts +73 -16
  105. package/src/internal/photos/index.ts +9 -4
  106. package/src/internal/photos/interface.ts +3 -28
  107. package/src/internal/photos/nodes.ts +1 -1
  108. package/src/internal/photos/photosManager.test.ts +266 -0
  109. package/src/internal/photos/photosManager.ts +144 -0
  110. package/src/internal/photos/photosTransferPayloadBuilder.test.ts +380 -0
  111. package/src/internal/photos/photosTransferPayloadBuilder.ts +203 -0
  112. package/src/internal/photos/upload.ts +20 -7
  113. package/src/internal/sharingPublic/nodes.ts +11 -4
  114. package/src/internal/upload/apiService.ts +7 -9
  115. package/src/internal/upload/cryptoService.ts +28 -7
  116. package/src/internal/upload/index.ts +3 -2
  117. package/src/internal/upload/interface.ts +1 -1
  118. package/src/internal/upload/streamUploader.test.ts +33 -4
  119. package/src/internal/upload/streamUploader.ts +13 -13
  120. package/src/protonDriveClient.ts +2 -1
  121. package/src/protonDrivePhotosClient.ts +39 -2
  122. package/src/protonDrivePublicLinkClient.ts +9 -1
  123. package/dist/internal/photos/albums.js.map +0 -1
  124. package/dist/internal/photos/albums.test.js.map +0 -1
  125. /package/dist/internal/photos/{albums.test.d.ts → albumsManager.test.d.ts} +0 -0
@@ -8,10 +8,18 @@ import { uint8ArrayToBase64String } from './utils';
8
8
  * clients/packages/crypto/lib/proxy/proxy.ts.
9
9
  */
10
10
  export interface OpenPGPCryptoProxy {
11
- generateKey: (options: { userIDs: { name: string }[]; type: 'ecc'; curve: 'ed25519Legacy' }) => Promise<PrivateKey>;
11
+ generateKey: (options: {
12
+ userIDs: { name: string }[];
13
+ type: 'ecc';
14
+ curve: 'ed25519Legacy';
15
+ config?: { aeadProtect: boolean };
16
+ }) => Promise<PrivateKey>;
12
17
  exportPrivateKey: (options: { privateKey: PrivateKey; passphrase: string | null }) => Promise<string>;
13
18
  importPrivateKey: (options: { armoredKey: string; passphrase: string | null }) => Promise<PrivateKey>;
14
- generateSessionKey: (options: { recipientKeys: PublicKey[] }) => Promise<SessionKey>;
19
+ generateSessionKey: (options: {
20
+ recipientKeys: PublicKey[];
21
+ config?: { ignoreSEIPDv2FeatureFlag: boolean };
22
+ }) => Promise<SessionKey>;
15
23
  encryptSessionKey: (
16
24
  options: SessionKey & {
17
25
  format: 'binary';
@@ -32,6 +40,7 @@ export interface OpenPGPCryptoProxy {
32
40
  signingKeys?: PrivateKey;
33
41
  detached?: Detached;
34
42
  compress?: boolean;
43
+ config?: { ignoreSEIPDv2FeatureFlag: boolean };
35
44
  }) => Promise<
36
45
  Detached extends true
37
46
  ? {
@@ -93,8 +102,15 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
93
102
  return uint8ArrayToBase64String(value);
94
103
  }
95
104
 
96
- async generateSessionKey(encryptionKeys: PublicKey[]) {
97
- return this.cryptoProxy.generateSessionKey({ recipientKeys: encryptionKeys });
105
+ async generateSessionKey(encryptionKeys: PublicKey[], options: { enableAeadWithEncryptionKeys: boolean }) {
106
+ return this.cryptoProxy.generateSessionKey({
107
+ recipientKeys: encryptionKeys,
108
+ // `ignoreSEIPDv2FeatureFlag` means that the key preferences are
109
+ // ignored. If set to `true`, the session key will be generated
110
+ // the standard non-AEAD algorithm. If set to `false`, the session
111
+ // key will always follow the encryption key preferences.
112
+ config: { ignoreSEIPDv2FeatureFlag: !options.enableAeadWithEncryptionKeys },
113
+ });
98
114
  }
99
115
 
100
116
  async encryptSessionKey(sessionKey: SessionKey, encryptionKeys: PublicKey | PublicKey[]) {
@@ -119,11 +135,12 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
119
135
  };
120
136
  }
121
137
 
122
- async generateKey(passphrase: string) {
138
+ async generateKey(passphrase: string, options: { enableAead: boolean }) {
123
139
  const privateKey = await this.cryptoProxy.generateKey({
124
140
  userIDs: [{ name: 'Drive key' }],
125
141
  type: 'ecc',
126
142
  curve: 'ed25519Legacy',
143
+ config: { aeadProtect: options.enableAead },
127
144
  });
128
145
 
129
146
  const armoredKey = await this.cryptoProxy.exportPrivateKey({
@@ -137,11 +154,21 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
137
154
  };
138
155
  }
139
156
 
140
- async encryptArmored(data: Uint8Array<ArrayBuffer>, encryptionKeys: PublicKey[], sessionKey?: SessionKey) {
157
+ async encryptArmored(
158
+ data: Uint8Array<ArrayBuffer>,
159
+ encryptionKeys: PublicKey[],
160
+ sessionKey: SessionKey | undefined,
161
+ options: { enableAeadWithEncryptionKeys: boolean },
162
+ ) {
141
163
  const { message: armoredData } = await this.cryptoProxy.encryptMessage({
142
164
  binaryData: data,
143
165
  sessionKey,
144
166
  encryptionKeys,
167
+ // `ignoreSEIPDv2FeatureFlag` means that the key preferences are
168
+ // ignored. If set to `true`, the encrypted data will be generated
169
+ // the standard non-AEAD algorithm. If set to `false`, the session
170
+ // key will always follow the encryption key preferences.
171
+ config: { ignoreSEIPDv2FeatureFlag: !options.enableAeadWithEncryptionKeys },
145
172
  });
146
173
  return {
147
174
  armoredData: armoredData,
@@ -153,6 +180,7 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
153
180
  sessionKey: SessionKey,
154
181
  encryptionKeys: PublicKey[],
155
182
  signingKey: PrivateKey,
183
+ options: { compress?: boolean; enableAeadWithEncryptionKeys: boolean },
156
184
  ) {
157
185
  const { message: encryptedData } = await this.cryptoProxy.encryptMessage({
158
186
  binaryData: data,
@@ -161,6 +189,11 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
161
189
  encryptionKeys,
162
190
  format: 'binary',
163
191
  detached: false,
192
+ // `ignoreSEIPDv2FeatureFlag` means that the key preferences are
193
+ // ignored. If set to `true`, the encrypted data will be generated
194
+ // the standard non-AEAD algorithm. If set to `false`, the session
195
+ // key will always follow the encryption key preferences.
196
+ config: { ignoreSEIPDv2FeatureFlag: !options.enableAeadWithEncryptionKeys },
164
197
  });
165
198
  return {
166
199
  encryptedData: encryptedData,
@@ -172,7 +205,7 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
172
205
  sessionKey: SessionKey | undefined,
173
206
  encryptionKeys: PublicKey[],
174
207
  signingKey: PrivateKey,
175
- options: { compress?: boolean } = {},
208
+ options: { compress?: boolean; enableAeadWithEncryptionKeys: boolean },
176
209
  ) {
177
210
  const { message: armoredData } = await this.cryptoProxy.encryptMessage({
178
211
  binaryData: data,
@@ -181,6 +214,11 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
181
214
  signingKeys: signingKey,
182
215
  detached: false,
183
216
  compress: options.compress || false,
217
+ // `ignoreSEIPDv2FeatureFlag` means that the key preferences are
218
+ // ignored. If set to `true`, the encrypted data will be generated
219
+ // the standard non-AEAD algorithm. If set to `false`, the session
220
+ // key will always follow the encryption key preferences.
221
+ config: { ignoreSEIPDv2FeatureFlag: !options.enableAeadWithEncryptionKeys },
184
222
  });
185
223
  return {
186
224
  armoredData: armoredData,
@@ -192,6 +230,7 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
192
230
  sessionKey: SessionKey,
193
231
  encryptionKeys: PublicKey[],
194
232
  signingKey: PrivateKey,
233
+ options: { enableAeadWithEncryptionKeys: boolean },
195
234
  ) {
196
235
  const { message: encryptedData, signature } = await this.cryptoProxy.encryptMessage({
197
236
  binaryData: data,
@@ -200,6 +239,11 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
200
239
  encryptionKeys,
201
240
  format: 'binary',
202
241
  detached: true,
242
+ // `ignoreSEIPDv2FeatureFlag` means that the key preferences are
243
+ // ignored. If set to `true`, the encrypted data will be generated
244
+ // the standard non-AEAD algorithm. If set to `false`, the session
245
+ // key will always follow the encryption key preferences.
246
+ config: { ignoreSEIPDv2FeatureFlag: !options.enableAeadWithEncryptionKeys },
203
247
  });
204
248
  return {
205
249
  encryptedData: encryptedData,
@@ -212,6 +256,7 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
212
256
  sessionKey: SessionKey,
213
257
  encryptionKeys: PublicKey[],
214
258
  signingKey: PrivateKey,
259
+ options: { enableAeadWithEncryptionKeys: boolean },
215
260
  ) {
216
261
  const { message: armoredData, signature: armoredSignature } = await this.cryptoProxy.encryptMessage({
217
262
  binaryData: data,
@@ -219,6 +264,11 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
219
264
  signingKeys: signingKey,
220
265
  encryptionKeys,
221
266
  detached: true,
267
+ // `ignoreSEIPDv2FeatureFlag` means that the key preferences are
268
+ // ignored. If set to `true`, the encrypted data will be generated
269
+ // the standard non-AEAD algorithm. If set to `false`, the session
270
+ // key will always follow the encryption key preferences.
271
+ config: { ignoreSEIPDv2FeatureFlag: !options.enableAeadWithEncryptionKeys },
222
272
  });
223
273
  return {
224
274
  armoredData: armoredData,
@@ -251,7 +301,11 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
251
301
  };
252
302
  }
253
303
 
254
- async verify(data: Uint8Array<ArrayBuffer>, signature: Uint8Array<ArrayBuffer>, verificationKeys: PublicKey | PublicKey[]) {
304
+ async verify(
305
+ data: Uint8Array<ArrayBuffer>,
306
+ signature: Uint8Array<ArrayBuffer>,
307
+ verificationKeys: PublicKey | PublicKey[],
308
+ ) {
255
309
  const { verificationStatus, errors } = await this.cryptoProxy.verifyMessage({
256
310
  binaryData: data,
257
311
  binarySignature: signature,
@@ -292,6 +346,12 @@ export class OpenPGPCryptoWithCryptoProxy implements OpenPGPCrypto {
292
346
  throw new Error('Could not decrypt session key');
293
347
  }
294
348
 
349
+ // Encrypted OpenPGP v6 session keys used for AEAD do not store algorithm information, so we hardcode it
350
+ if (sessionKey.algorithm === null) {
351
+ sessionKey.algorithm = 'aes256';
352
+ sessionKey.aeadAlgorithm = 'gcm';
353
+ }
354
+
295
355
  return sessionKey;
296
356
  }
297
357
 
@@ -27,6 +27,9 @@ export class DiagnosticTelemetry extends EventsGenerator {
27
27
  if (event.eventName === 'volumeEventsSubscriptionsChanged') {
28
28
  return;
29
29
  }
30
+ if (event.eventName === 'performance') {
31
+ return;
32
+ }
30
33
 
31
34
  this.enqueueEvent({
32
35
  type: 'metric',
@@ -3,5 +3,9 @@
3
3
  * Applications must supply their own implementation.
4
4
  */
5
5
  export interface FeatureFlagProvider {
6
- isEnabled(flagName: string, signal?: AbortSignal): Promise<boolean>;
6
+ isEnabled(flagName: FeatureFlags, signal?: AbortSignal): Promise<boolean>;
7
+ }
8
+
9
+ export enum FeatureFlags {
10
+ DriveCryptoEncryptBlocksWithPgpAead = 'DriveCryptoEncryptBlocksWithPgpAead',
7
11
  }
@@ -14,6 +14,7 @@ export type { Author, UnverifiedAuthorError, AnonymousUser } from './author';
14
14
  export type { ProtonDriveConfig } from './config';
15
15
  export type { Device, DeviceOrUid } from './devices';
16
16
  export type { FeatureFlagProvider } from './featureFlags';
17
+ export { FeatureFlags } from './featureFlags';
17
18
  export { DeviceType } from './devices';
18
19
  export type { FileDownloader, DownloadController, SeekableReadableStream } from './download';
19
20
  export type {
@@ -56,6 +57,7 @@ export type {
56
57
  PhotoAttributes,
57
58
  AlbumAttributes,
58
59
  } from './photos';
60
+ export { PhotoTag } from './photos';
59
61
  export type {
60
62
  ProtonInvitation,
61
63
  ProtonInvitationWithNode,
@@ -65,7 +65,20 @@ export type PhotoAttributes = {
65
65
  /**
66
66
  * List of tags assigned to the photo.
67
67
  */
68
- tags: number[]; // TODO: enum
68
+ tags: PhotoTag[];
69
+ };
70
+
71
+ export enum PhotoTag {
72
+ Favorites = 0,
73
+ Screenshots = 1,
74
+ Videos = 2,
75
+ LivePhotos = 3,
76
+ MotionPhotos = 4,
77
+ Selfies = 5,
78
+ Portraits = 6,
79
+ Bursts = 7,
80
+ Panoramas = 8,
81
+ Raw = 9,
69
82
  }
70
83
 
71
84
  /**
@@ -18,7 +18,8 @@ export type MetricEvent =
18
18
  | MetricDecryptionErrorEvent
19
19
  | MetricVerificationErrorEvent
20
20
  | MetricBlockVerificationErrorEvent
21
- | MetricVolumeEventsSubscriptionsChangedEvent;
21
+ | MetricVolumeEventsSubscriptionsChangedEvent
22
+ | MetricPerformanceEvent;
22
23
 
23
24
  export interface MetricAPIRetrySucceededEvent {
24
25
  eventName: 'apiRetrySucceeded';
@@ -118,3 +119,15 @@ export enum MetricVolumeType {
118
119
  Shared = 'shared',
119
120
  SharedPublic = 'shared_public',
120
121
  }
122
+
123
+ /**
124
+ * Experimental metrics to track performance of encryption and decryption
125
+ * operations of the file content.
126
+ */
127
+ export interface MetricPerformanceEvent {
128
+ eventName: 'performance';
129
+ type: 'content_encryption' | 'content_decryption';
130
+ cryptoModel: 'v1' | 'v1.5';
131
+ bytesProcessed: number;
132
+ milliseconds: number;
133
+ }
@@ -137,8 +137,12 @@ export class DriveAPIService {
137
137
  return this.makeRequest(url, 'PUT', data, signal);
138
138
  }
139
139
 
140
- async delete<Response>(url: string, signal?: AbortSignal): Promise<Response> {
141
- return this.makeRequest(url, 'DELETE', undefined, signal);
140
+ async delete<RequestPayload, ResponsePayload>(
141
+ url: string,
142
+ data?: RequestPayload,
143
+ signal?: AbortSignal,
144
+ ): Promise<ResponsePayload> {
145
+ return this.makeRequest(url, 'DELETE', data, signal);
142
146
  }
143
147
 
144
148
  protected async makeRequest<RequestPayload, ResponsePayload>(
@@ -440,7 +440,7 @@ export abstract class NodeAPIServiceBase<
440
440
  }
441
441
 
442
442
  async emptyTrash(volumeId: string): Promise<void> {
443
- await this.apiService.delete<EmptyTrashResponse>(`drive/volumes/${volumeId}/trash`);
443
+ await this.apiService.delete<undefined, EmptyTrashResponse>(`drive/volumes/${volumeId}/trash`);
444
444
  }
445
445
 
446
446
  async *restoreNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
@@ -561,7 +561,7 @@ export abstract class NodeAPIServiceBase<
561
561
  async deleteRevision(nodeRevisionUid: string): Promise<void> {
562
562
  const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(nodeRevisionUid);
563
563
 
564
- await this.apiService.delete<DeleteRevisionResponse>(
564
+ await this.apiService.delete<undefined, DeleteRevisionResponse>(
565
565
  `drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/${revisionId}`,
566
566
  );
567
567
  }
@@ -412,7 +412,7 @@ export abstract class NodesAccessBase<
412
412
  }
413
413
  // This is bug that should not happen.
414
414
  // API cannot provide node without parent or share.
415
- throw new Error('Node has neither parent node nor share');
415
+ throw new Error(`Node has neither parent node nor share: ${node.uid}`);
416
416
  }
417
417
 
418
418
  async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
@@ -1,13 +1,12 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { ValidationError } from '../../errors';
4
3
  import { Logger, NodeResultWithError } from '../../interface';
5
4
  import { DecryptedNodeKeys, NodeSigningKeys } from '../nodes/interface';
6
5
  import { splitNodeUid } from '../uids';
7
6
  import { AlbumsCryptoService } from './albumsCrypto';
8
7
  import { PhotosAPIService } from './apiService';
9
8
  import { MissingRelatedPhotosError } from './errors';
10
- import { AddToAlbumEncryptedPhotoPayload, DecryptedPhotoNode } from './interface';
9
+ import { PhotoTransferPayloadBuilder, TransferEncryptedPhotoPayload } from './photosTransferPayloadBuilder';
11
10
  import { PhotosNodesAccess } from './nodes';
12
11
 
13
12
  /**
@@ -47,18 +46,20 @@ type PhotoQueueItem = {
47
46
  export class AddToAlbumProcess {
48
47
  private readonly albumVolumeId: string;
49
48
  private readonly retriedPhotoUids = new Set<string>();
49
+ private readonly payloadBuilder: PhotoTransferPayloadBuilder;
50
50
 
51
51
  constructor(
52
52
  private readonly albumNodeUid: string,
53
53
  private readonly albumKeys: DecryptedNodeKeys,
54
54
  private readonly signingKeys: NodeSigningKeys,
55
55
  private readonly apiService: PhotosAPIService,
56
- private readonly cryptoService: AlbumsCryptoService,
56
+ cryptoService: AlbumsCryptoService,
57
57
  private readonly nodesService: PhotosNodesAccess,
58
58
  private readonly logger: Logger,
59
59
  private readonly signal?: AbortSignal,
60
60
  ) {
61
61
  this.albumVolumeId = splitNodeUid(albumNodeUid).volumeId;
62
+ this.payloadBuilder = new PhotoTransferPayloadBuilder(cryptoService, nodesService);
62
63
  }
63
64
 
64
65
  async *execute(photoNodeUids: string[]): AsyncGenerator<NodeResultWithError> {
@@ -71,7 +72,13 @@ export class AddToAlbumProcess {
71
72
  private async *processSameVolumeQueue(queue: PhotoQueueItem[]): AsyncGenerator<NodeResultWithError> {
72
73
  while (queue.length > 0) {
73
74
  const items = queue.splice(0, BATCH_LOADING_SIZE);
74
- const { payloads, errors } = await this.preparePhotoPayloads(items);
75
+ const { payloads, errors } = await this.payloadBuilder.preparePhotoPayloads(
76
+ items,
77
+ this.albumNodeUid,
78
+ this.albumKeys,
79
+ this.signingKeys,
80
+ this.signal,
81
+ );
75
82
 
76
83
  for (const [uid, error] of errors) {
77
84
  yield { uid, ok: false, error };
@@ -97,7 +104,13 @@ export class AddToAlbumProcess {
97
104
  private async *processDifferentVolumeQueue(queue: PhotoQueueItem[]): AsyncGenerator<NodeResultWithError> {
98
105
  while (queue.length > 0) {
99
106
  const items = queue.splice(0, BATCH_LOADING_SIZE);
100
- const { payloads, errors } = await this.preparePhotoPayloads(items);
107
+ const { payloads, errors } = await this.payloadBuilder.preparePhotoPayloads(
108
+ items,
109
+ this.albumNodeUid,
110
+ this.albumKeys,
111
+ this.signingKeys,
112
+ this.signal,
113
+ );
101
114
 
102
115
  for (const [uid, error] of errors) {
103
116
  yield { uid, ok: false, error };
@@ -105,7 +118,11 @@ export class AddToAlbumProcess {
105
118
 
106
119
  for (const payload of payloads) {
107
120
  try {
108
- const newPhotoNodeUid = await this.apiService.copyPhotoToAlbum(this.albumNodeUid, payload, this.signal);
121
+ const newPhotoNodeUid = await this.apiService.copyPhotoToAlbum(
122
+ this.albumNodeUid,
123
+ payload,
124
+ this.signal,
125
+ );
109
126
  await this.nodesService.notifyChildCreated(newPhotoNodeUid);
110
127
  yield { uid: payload.nodeUid, ok: true };
111
128
  } catch (error) {
@@ -119,131 +136,14 @@ export class AddToAlbumProcess {
119
136
  yield {
120
137
  uid: payload.nodeUid,
121
138
  ok: false,
122
- error: error instanceof Error ? error : new Error(c('Error').t`Unknown error`, { cause: error }),
139
+ error:
140
+ error instanceof Error ? error : new Error(c('Error').t`Unknown error`, { cause: error }),
123
141
  };
124
142
  }
125
143
  }
126
144
  }
127
145
  }
128
146
 
129
- private async preparePhotoPayloads(items: PhotoQueueItem[]): Promise<{
130
- payloads: AddToAlbumEncryptedPhotoPayload[];
131
- errors: Map<string, Error>;
132
- }> {
133
- const payloads: AddToAlbumEncryptedPhotoPayload[] = [];
134
- const errors = new Map<string, Error>();
135
-
136
- const additionalRelatedMap = new Map(
137
- items.map((item) => [item.photoNodeUid, item.additionalRelatedPhotoNodeUids]),
138
- );
139
-
140
- const nodeUids = items.map((item) => item.photoNodeUid);
141
- for await (const photoNode of this.nodesService.iterateNodes(nodeUids, this.signal)) {
142
- if ('missingUid' in photoNode) {
143
- errors.set(photoNode.missingUid, new ValidationError(c('Error').t`Photo not found`));
144
- continue;
145
- }
146
-
147
- try {
148
- const additionalRelated = additionalRelatedMap.get(photoNode.uid) || [];
149
- const payload = await this.preparePhotoPayload(photoNode, additionalRelated);
150
- payloads.push(payload);
151
- } catch (error) {
152
- errors.set(
153
- photoNode.uid,
154
- error instanceof Error ? error : new Error(c('Error').t`Unknown error`, { cause: error }),
155
- );
156
- }
157
- }
158
-
159
- return { payloads, errors };
160
- }
161
-
162
- private async preparePhotoPayload(
163
- photoNode: DecryptedPhotoNode,
164
- additionalRelatedPhotoNodeUids: string[],
165
- ): Promise<AddToAlbumEncryptedPhotoPayload> {
166
- const photoData = await this.encryptPhotoForAlbum(photoNode);
167
-
168
- const relatedNodeUids = [...new Set([
169
- ...(photoNode.photo?.relatedPhotoNodeUids || []),
170
- ...additionalRelatedPhotoNodeUids,
171
- ])];
172
-
173
- const relatedPhotos =
174
- relatedNodeUids.length > 0 ? await this.prepareRelatedPhotoPayloads(relatedNodeUids) : [];
175
-
176
- return {
177
- ...photoData,
178
- relatedPhotos,
179
- };
180
- }
181
-
182
- private async prepareRelatedPhotoPayloads(
183
- nodeUids: string[],
184
- ): Promise<Omit<AddToAlbumEncryptedPhotoPayload, 'relatedPhotos'>[]> {
185
- const payloads: Omit<AddToAlbumEncryptedPhotoPayload, 'relatedPhotos'>[] = [];
186
-
187
- for await (const photoNode of this.nodesService.iterateNodes(nodeUids, this.signal)) {
188
- // Missing related photos means that the related photo was deleted
189
- // since the loading of the metadata. It can happen and should be
190
- // ignored. The backend controls all the related photos are part
191
- // of the request, thus the request will fail and be retried if
192
- // there is any other race condition.
193
- if ('missingUid' in photoNode) {
194
- continue;
195
- }
196
- const payload = await this.encryptPhotoForAlbum(photoNode);
197
- payloads.push(payload);
198
- }
199
-
200
- return payloads;
201
- }
202
-
203
- private async encryptPhotoForAlbum(
204
- photoNode: DecryptedPhotoNode,
205
- ): Promise<AddToAlbumEncryptedPhotoPayload> {
206
- const nodeKeys = await this.nodesService.getNodePrivateAndSessionKeys(photoNode.uid);
207
-
208
- const contentSha1 = photoNode.activeRevision?.ok
209
- ? photoNode.activeRevision.value.claimedDigests?.sha1
210
- : undefined;
211
-
212
- if (!contentSha1) {
213
- throw new Error('Cannot add photo to album without a content hash');
214
- }
215
-
216
- const encryptedCrypto = await this.cryptoService.encryptPhotoForAlbum(
217
- photoNode.name,
218
- contentSha1,
219
- nodeKeys,
220
- { key: this.albumKeys.key, hashKey: this.albumKeys.hashKey! },
221
- this.signingKeys,
222
- );
223
-
224
- // Node could be uploaded or renamed by anonymous user and thus have
225
- // missing signatures that must be added to the request.
226
- // Node passphrase and signature email must be passed if and only if
227
- // the signatures are missing (key author is null).
228
- const anonymousKey = photoNode.keyAuthor.ok && photoNode.keyAuthor.value === null;
229
- const keySignatureProperties = !anonymousKey
230
- ? {}
231
- : {
232
- signatureEmail: encryptedCrypto.signatureEmail,
233
- nodePassphraseSignature: encryptedCrypto.armoredNodePassphraseSignature,
234
- };
235
-
236
- return {
237
- nodeUid: photoNode.uid,
238
- contentHash: encryptedCrypto.contentHash,
239
- nameHash: encryptedCrypto.hash,
240
- encryptedName: encryptedCrypto.encryptedName,
241
- nameSignatureEmail: encryptedCrypto.nameSignatureEmail,
242
- nodePassphrase: encryptedCrypto.armoredNodePassphrase,
243
- ...keySignatureProperties,
244
- };
245
- }
246
-
247
147
  /**
248
148
  * If the result indicates a MissingRelatedPhotosError that hasn't
249
149
  * been retried, returns a retry queue item. Otherwise returns undefined.
@@ -260,19 +160,14 @@ export class AddToAlbumProcess {
260
160
  * Returns undefined if the photo has already been retried, preventing
261
161
  * infinite retry loops.
262
162
  */
263
- private createRetryQueueItem(
264
- photoNodeUid: string,
265
- error: MissingRelatedPhotosError,
266
- ): PhotoQueueItem | undefined {
163
+ private createRetryQueueItem(photoNodeUid: string, error: MissingRelatedPhotosError): PhotoQueueItem | undefined {
267
164
  if (this.retriedPhotoUids.has(photoNodeUid)) {
268
165
  this.logger.warn(`Missing related photos for ${photoNodeUid}, already retried`);
269
166
  return undefined;
270
167
  }
271
168
 
272
169
  this.retriedPhotoUids.add(photoNodeUid);
273
- this.logger.info(
274
- `Missing related photos for ${photoNodeUid}, re-queuing: ${error.missingNodeUids.join(', ')}`,
275
- );
170
+ this.logger.info(`Missing related photos for ${photoNodeUid}, re-queuing: ${error.missingNodeUids.join(', ')}`);
276
171
 
277
172
  return {
278
173
  photoNodeUid,
@@ -316,10 +211,8 @@ function splitByVolume(
316
211
  * Groups payloads into batches respecting the API limit.
317
212
  * Each payload's size counts itself plus its related photos.
318
213
  */
319
- function* createBatches(
320
- payloads: AddToAlbumEncryptedPhotoPayload[],
321
- ): Generator<AddToAlbumEncryptedPhotoPayload[]> {
322
- let batch: AddToAlbumEncryptedPhotoPayload[] = [];
214
+ function* createBatches(payloads: TransferEncryptedPhotoPayload[]): Generator<TransferEncryptedPhotoPayload[]> {
215
+ let batch: TransferEncryptedPhotoPayload[] = [];
323
216
  let batchSize = 0;
324
217
 
325
218
  for (const payload of payloads) {
@@ -1,7 +1,7 @@
1
1
  import { NodeType } from '../../interface';
2
2
  import { ValidationError } from '../../errors';
3
3
  import { getMockTelemetry } from '../../tests/telemetry';
4
- import { Albums } from './albums';
4
+ import { AlbumsManager } from './albumsManager';
5
5
  import { AlbumsCryptoService } from './albumsCrypto';
6
6
  import { PhotosAPIService } from './apiService';
7
7
  import { DecryptedPhotoNode } from './interface';
@@ -13,7 +13,7 @@ describe('Albums', () => {
13
13
  let cryptoService: AlbumsCryptoService;
14
14
  let photoShares: PhotoSharesManager;
15
15
  let nodesService: PhotosNodesAccess;
16
- let albums: Albums;
16
+ let albums: AlbumsManager;
17
17
 
18
18
  let nodes: { [uid: string]: DecryptedPhotoNode };
19
19
 
@@ -97,7 +97,7 @@ describe('Albums', () => {
97
97
  notifyChildCreated: jest.fn(),
98
98
  };
99
99
 
100
- albums = new Albums(getMockTelemetry(), apiService, cryptoService, photoShares, nodesService);
100
+ albums = new AlbumsManager(getMockTelemetry(), apiService, cryptoService, photoShares, nodesService);
101
101
  });
102
102
 
103
103
  describe('createAlbum', () => {
@@ -16,7 +16,7 @@ const BATCH_LOADING_SIZE = 10;
16
16
  /**
17
17
  * Provides access and high-level actions for managing albums.
18
18
  */
19
- export class Albums {
19
+ export class AlbumsManager {
20
20
  private logger: Logger;
21
21
 
22
22
  constructor(