@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.
- package/dist/crypto/driveCrypto.d.ts +20 -2
- package/dist/crypto/driveCrypto.js +72 -14
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/driveCrypto.test.js +3 -2
- package/dist/crypto/driveCrypto.test.js.map +1 -1
- package/dist/crypto/interface.d.ts +22 -8
- package/dist/crypto/openPGPCrypto.d.ts +30 -7
- package/dist/crypto/openPGPCrypto.js +46 -8
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/diagnostic/telemetry.js +3 -0
- package/dist/diagnostic/telemetry.js.map +1 -1
- package/dist/interface/featureFlags.d.ts +4 -1
- package/dist/interface/featureFlags.js +5 -0
- package/dist/interface/featureFlags.js.map +1 -1
- package/dist/interface/index.d.ts +2 -0
- package/dist/interface/index.js +5 -1
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/photos.d.ts +13 -1
- package/dist/interface/photos.js +14 -0
- package/dist/interface/photos.js.map +1 -1
- package/dist/interface/telemetry.d.ts +12 -1
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/internal/apiService/apiService.d.ts +1 -1
- package/dist/internal/apiService/apiService.js +2 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.js +1 -1
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/photos/addToAlbum.d.ts +1 -5
- package/dist/internal/photos/addToAlbum.js +8 -87
- package/dist/internal/photos/addToAlbum.js.map +1 -1
- package/dist/internal/photos/{albums.d.ts → albumsManager.d.ts} +1 -1
- package/dist/internal/photos/{albums.js → albumsManager.js} +4 -4
- package/dist/internal/photos/albumsManager.js.map +1 -0
- package/dist/internal/photos/{albums.test.js → albumsManager.test.js} +3 -3
- package/dist/internal/photos/albumsManager.test.js.map +1 -0
- package/dist/internal/photos/apiService.d.ts +8 -4
- package/dist/internal/photos/apiService.js +38 -6
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/index.d.ts +7 -5
- package/dist/internal/photos/index.js +7 -4
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +3 -26
- package/dist/internal/photos/interface.js +0 -14
- package/dist/internal/photos/interface.js.map +1 -1
- package/dist/internal/photos/nodes.js +1 -1
- package/dist/internal/photos/nodes.js.map +1 -1
- package/dist/internal/photos/photosManager.d.ts +22 -0
- package/dist/internal/photos/photosManager.js +101 -0
- package/dist/internal/photos/photosManager.js.map +1 -0
- package/dist/internal/photos/photosManager.test.d.ts +1 -0
- package/dist/internal/photos/photosManager.test.js +222 -0
- package/dist/internal/photos/photosManager.test.js.map +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.d.ts +57 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.js +113 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.js.map +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.test.d.ts +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.test.js +289 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.test.js.map +1 -0
- package/dist/internal/photos/upload.d.ts +2 -2
- package/dist/internal/photos/upload.js +3 -3
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.js +11 -4
- package/dist/internal/sharingPublic/nodes.js.map +1 -1
- package/dist/internal/upload/apiService.d.ts +0 -4
- package/dist/internal/upload/apiService.js +0 -4
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +4 -2
- package/dist/internal/upload/cryptoService.js +18 -6
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/index.d.ts +2 -2
- package/dist/internal/upload/index.js +2 -2
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +1 -1
- package/dist/internal/upload/streamUploader.d.ts +1 -1
- package/dist/internal/upload/streamUploader.js +12 -13
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +28 -4
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.js +2 -2
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +20 -3
- package/dist/protonDrivePhotosClient.js +27 -3
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +3 -2
- package/dist/protonDrivePublicLinkClient.js +7 -3
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.test.ts +3 -1
- package/src/crypto/driveCrypto.ts +82 -7
- package/src/crypto/interface.ts +21 -8
- package/src/crypto/openPGPCrypto.ts +68 -8
- package/src/diagnostic/telemetry.ts +3 -0
- package/src/interface/featureFlags.ts +5 -1
- package/src/interface/index.ts +2 -0
- package/src/interface/photos.ts +14 -1
- package/src/interface/telemetry.ts +14 -1
- package/src/internal/apiService/apiService.ts +6 -2
- package/src/internal/nodes/apiService.ts +2 -2
- package/src/internal/nodes/nodesAccess.ts +1 -1
- package/src/internal/photos/addToAlbum.ts +29 -136
- package/src/internal/photos/{albums.test.ts → albumsManager.test.ts} +3 -3
- package/src/internal/photos/{albums.ts → albumsManager.ts} +1 -1
- package/src/internal/photos/apiService.ts +73 -16
- package/src/internal/photos/index.ts +9 -4
- package/src/internal/photos/interface.ts +3 -28
- package/src/internal/photos/nodes.ts +1 -1
- package/src/internal/photos/photosManager.test.ts +266 -0
- package/src/internal/photos/photosManager.ts +144 -0
- package/src/internal/photos/photosTransferPayloadBuilder.test.ts +380 -0
- package/src/internal/photos/photosTransferPayloadBuilder.ts +203 -0
- package/src/internal/photos/upload.ts +20 -7
- package/src/internal/sharingPublic/nodes.ts +11 -4
- package/src/internal/upload/apiService.ts +7 -9
- package/src/internal/upload/cryptoService.ts +28 -7
- package/src/internal/upload/index.ts +3 -2
- package/src/internal/upload/interface.ts +1 -1
- package/src/internal/upload/streamUploader.test.ts +33 -4
- package/src/internal/upload/streamUploader.ts +13 -13
- package/src/protonDriveClient.ts +2 -1
- package/src/protonDrivePhotosClient.ts +39 -2
- package/src/protonDrivePublicLinkClient.ts +9 -1
- package/dist/internal/photos/albums.js.map +0 -1
- package/dist/internal/photos/albums.test.js.map +0 -1
- /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: {
|
|
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: {
|
|
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({
|
|
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(
|
|
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(
|
|
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
|
|
|
@@ -3,5 +3,9 @@
|
|
|
3
3
|
* Applications must supply their own implementation.
|
|
4
4
|
*/
|
|
5
5
|
export interface FeatureFlagProvider {
|
|
6
|
-
isEnabled(flagName:
|
|
6
|
+
isEnabled(flagName: FeatureFlags, signal?: AbortSignal): Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export enum FeatureFlags {
|
|
10
|
+
DriveCryptoEncryptBlocksWithPgpAead = 'DriveCryptoEncryptBlocksWithPgpAead',
|
|
7
11
|
}
|
package/src/interface/index.ts
CHANGED
|
@@ -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,
|
package/src/interface/photos.ts
CHANGED
|
@@ -65,7 +65,20 @@ export type PhotoAttributes = {
|
|
|
65
65
|
/**
|
|
66
66
|
* List of tags assigned to the photo.
|
|
67
67
|
*/
|
|
68
|
-
tags:
|
|
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<
|
|
141
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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
|
|
100
|
+
albums = new AlbumsManager(getMockTelemetry(), apiService, cryptoService, photoShares, nodesService);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
describe('createAlbum', () => {
|