@protontech/drive-sdk 0.10.0 → 0.11.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 +4 -1
- package/dist/crypto/driveCrypto.js +23 -1
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/driveCrypto.test.js +2 -1
- package/dist/crypto/driveCrypto.test.js.map +1 -1
- package/dist/diagnostic/telemetry.js +3 -0
- package/dist/diagnostic/telemetry.js.map +1 -1
- package/dist/interface/index.d.ts +1 -0
- package/dist/interface/index.js +3 -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 +5 -3
- package/dist/internal/photos/index.js +5 -2
- 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.js +1 -1
- package/dist/internal/photos/upload.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.js +4 -4
- package/dist/internal/upload/cryptoService.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 +1 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +19 -2
- package/dist/protonDrivePhotosClient.js +21 -1
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.js +1 -1
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.test.ts +2 -1
- package/src/crypto/driveCrypto.ts +36 -2
- package/src/diagnostic/telemetry.ts +3 -0
- package/src/interface/index.ts +1 -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 +6 -3
- 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 +6 -1
- package/src/internal/upload/apiService.ts +7 -9
- package/src/internal/upload/cryptoService.ts +4 -5
- 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 +1 -1
- package/src/protonDrivePhotosClient.ts +34 -2
- package/src/protonDrivePublicLinkClient.ts +1 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProtonDriveTelemetry } from '../interface';
|
|
1
2
|
import {
|
|
2
3
|
OpenPGPCrypto,
|
|
3
4
|
PrivateKey,
|
|
@@ -29,9 +30,11 @@ enum SIGNING_CONTEXTS {
|
|
|
29
30
|
*/
|
|
30
31
|
export class DriveCrypto {
|
|
31
32
|
constructor(
|
|
33
|
+
private telemetry: ProtonDriveTelemetry,
|
|
32
34
|
private openPGPCrypto: OpenPGPCrypto,
|
|
33
35
|
private srpModule: SRPModule,
|
|
34
36
|
) {
|
|
37
|
+
this.telemetry = telemetry;
|
|
35
38
|
this.openPGPCrypto = openPGPCrypto;
|
|
36
39
|
this.srpModule = srpModule;
|
|
37
40
|
}
|
|
@@ -617,12 +620,14 @@ export class DriveCrypto {
|
|
|
617
620
|
): Promise<{
|
|
618
621
|
encryptedData: Uint8Array<ArrayBuffer>;
|
|
619
622
|
}> {
|
|
623
|
+
const start = performance.now();
|
|
620
624
|
const { encryptedData } = await this.openPGPCrypto.encryptAndSign(
|
|
621
625
|
thumbnailData,
|
|
622
626
|
sessionKey,
|
|
623
627
|
[], // Thumbnails use the session key so we do not send encryption key.
|
|
624
628
|
signingKey,
|
|
625
629
|
);
|
|
630
|
+
this.recordPerformance('content_encryption', thumbnailData.length, start);
|
|
626
631
|
|
|
627
632
|
return {
|
|
628
633
|
encryptedData,
|
|
@@ -638,11 +643,13 @@ export class DriveCrypto {
|
|
|
638
643
|
verified: VERIFICATION_STATUS;
|
|
639
644
|
verificationErrors?: Error[];
|
|
640
645
|
}> {
|
|
646
|
+
const start = performance.now();
|
|
641
647
|
const {
|
|
642
648
|
data: decryptedThumbnail,
|
|
643
649
|
verified,
|
|
644
650
|
verificationErrors,
|
|
645
651
|
} = await this.openPGPCrypto.decryptAndVerify(encryptedThumbnail, sessionKey, verificationKeys);
|
|
652
|
+
this.recordPerformance('content_decryption', decryptedThumbnail.length, start);
|
|
646
653
|
return {
|
|
647
654
|
decryptedThumbnail,
|
|
648
655
|
verified,
|
|
@@ -659,12 +666,14 @@ export class DriveCrypto {
|
|
|
659
666
|
encryptedData: Uint8Array<ArrayBuffer>;
|
|
660
667
|
armoredSignature: string;
|
|
661
668
|
}> {
|
|
669
|
+
const start = performance.now();
|
|
662
670
|
const { encryptedData, signature } = await this.openPGPCrypto.encryptAndSignDetached(
|
|
663
671
|
blockData,
|
|
664
672
|
sessionKey,
|
|
665
673
|
[], // Blocks use the session key so we do not send encryption key.
|
|
666
674
|
signingKey,
|
|
667
675
|
);
|
|
676
|
+
this.recordPerformance('content_encryption', blockData.length, start);
|
|
668
677
|
|
|
669
678
|
const { armoredSignature } = await this.encryptSignature(signature, encryptionKey, sessionKey);
|
|
670
679
|
|
|
@@ -674,8 +683,13 @@ export class DriveCrypto {
|
|
|
674
683
|
};
|
|
675
684
|
}
|
|
676
685
|
|
|
677
|
-
async decryptBlock(
|
|
686
|
+
async decryptBlock(
|
|
687
|
+
encryptedBlock: Uint8Array<ArrayBuffer>,
|
|
688
|
+
sessionKey: SessionKey,
|
|
689
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
690
|
+
const start = performance.now();
|
|
678
691
|
const { data: decryptedBlock } = await this.openPGPCrypto.decryptAndVerify(encryptedBlock, sessionKey, []);
|
|
692
|
+
this.recordPerformance('content_decryption', decryptedBlock.length, start);
|
|
679
693
|
|
|
680
694
|
return decryptedBlock;
|
|
681
695
|
}
|
|
@@ -716,7 +730,11 @@ export class DriveCrypto {
|
|
|
716
730
|
return uint8ArrayToUtf8(password);
|
|
717
731
|
}
|
|
718
732
|
|
|
719
|
-
async encryptShareUrlPassword(
|
|
733
|
+
async encryptShareUrlPassword(
|
|
734
|
+
password: string,
|
|
735
|
+
encryptionKey: PrivateKey,
|
|
736
|
+
signingKey: PrivateKey,
|
|
737
|
+
): Promise<string> {
|
|
720
738
|
const { armoredData } = await this.openPGPCrypto.encryptAndSignArmored(
|
|
721
739
|
new TextEncoder().encode(password),
|
|
722
740
|
undefined,
|
|
@@ -725,6 +743,22 @@ export class DriveCrypto {
|
|
|
725
743
|
);
|
|
726
744
|
return armoredData;
|
|
727
745
|
}
|
|
746
|
+
|
|
747
|
+
private recordPerformance(
|
|
748
|
+
type: 'content_encryption' | 'content_decryption',
|
|
749
|
+
bytesProcessed: number,
|
|
750
|
+
start: number,
|
|
751
|
+
) {
|
|
752
|
+
const end = performance.now();
|
|
753
|
+
const duration = end - start;
|
|
754
|
+
this.telemetry.recordMetric({
|
|
755
|
+
eventName: 'performance',
|
|
756
|
+
type,
|
|
757
|
+
cryptoModel: 'v1',
|
|
758
|
+
bytesProcessed,
|
|
759
|
+
milliseconds: Math.round(duration),
|
|
760
|
+
});
|
|
761
|
+
}
|
|
728
762
|
}
|
|
729
763
|
|
|
730
764
|
export function uint8ArrayToUtf8(input: Uint8Array<ArrayBuffer>): string {
|
package/src/interface/index.ts
CHANGED
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', () => {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { ValidationError } from '../../errors';
|
|
4
|
-
import { NodeResultWithError } from '../../interface';
|
|
4
|
+
import { NodeResultWithError, PhotoTag } from '../../interface';
|
|
5
5
|
import { APICodeError, DriveAPIService, drivePaths, InvalidRequirementsAPIError, isCodeOk } from '../apiService';
|
|
6
6
|
import { batch } from '../batch';
|
|
7
7
|
import { EncryptedRootShare, EncryptedShareCrypto, ShareType } from '../shares/interface';
|
|
8
8
|
import { makeNodeUid, splitNodeUid } from '../uids';
|
|
9
9
|
import { MissingRelatedPhotosError } from './errors';
|
|
10
|
-
import {
|
|
10
|
+
import { AlbumItem } from './interface';
|
|
11
|
+
import { TransferEncryptedPhotoPayload } from './photosTransferPayloadBuilder';
|
|
11
12
|
|
|
12
13
|
type GetPhotoShareResponse =
|
|
13
14
|
drivePaths['/drive/v2/shares/photos']['get']['responses']['200']['content']['application/json'];
|
|
@@ -68,6 +69,19 @@ type PostRemovePhotosFromAlbumRequest = Extract<
|
|
|
68
69
|
type PostRemovePhotosFromAlbumResponse =
|
|
69
70
|
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/remove-multiple']['post']['responses']['200']['content']['application/json'];
|
|
70
71
|
|
|
72
|
+
type PostAddPhotoTagsRequest = Extract<
|
|
73
|
+
drivePaths['/drive/photos/volumes/{volumeID}/links/{linkID}/tags']['post']['requestBody'],
|
|
74
|
+
{ content: object }
|
|
75
|
+
>['content']['application/json'];
|
|
76
|
+
type PostRemovePhotoTagsRequest = Extract<
|
|
77
|
+
drivePaths['/drive/photos/volumes/{volumeID}/links/{linkID}/tags']['delete']['requestBody'],
|
|
78
|
+
{ content: object }
|
|
79
|
+
>['content']['application/json'];
|
|
80
|
+
type PostFavoritePhotoRequest = Extract<
|
|
81
|
+
drivePaths['/drive/photos/volumes/{volumeID}/links/{linkID}/favorite']['post']['requestBody'],
|
|
82
|
+
{ content: object }
|
|
83
|
+
>['content']['application/json'];
|
|
84
|
+
|
|
71
85
|
const ALBUM_CONTAINS_PHOTOS_NOT_IN_TIMELINE_ERROR_CODE = 200302;
|
|
72
86
|
|
|
73
87
|
/**
|
|
@@ -337,15 +351,12 @@ export class PhotosAPIService {
|
|
|
337
351
|
*/
|
|
338
352
|
async *addPhotosToAlbum(
|
|
339
353
|
albumNodeUid: string,
|
|
340
|
-
photoPayloads:
|
|
354
|
+
photoPayloads: TransferEncryptedPhotoPayload[],
|
|
341
355
|
signal?: AbortSignal,
|
|
342
356
|
): AsyncGenerator<NodeResultWithError> {
|
|
343
357
|
const { volumeId, nodeId: albumLinkId } = splitNodeUid(albumNodeUid);
|
|
344
358
|
|
|
345
|
-
const allPhotoPayloads = photoPayloads.flatMap((photoPayload) => [
|
|
346
|
-
photoPayload,
|
|
347
|
-
...(photoPayload.relatedPhotos || []),
|
|
348
|
-
]);
|
|
359
|
+
const allPhotoPayloads = photoPayloads.flatMap((photoPayload) => [photoPayload, ...photoPayload.relatedPhotos]);
|
|
349
360
|
const allPhotoData = allPhotoPayloads.map((photoPayload) => {
|
|
350
361
|
const { nodeId } = splitNodeUid(photoPayload.nodeUid);
|
|
351
362
|
return {
|
|
@@ -416,7 +427,7 @@ export class PhotosAPIService {
|
|
|
416
427
|
*/
|
|
417
428
|
async copyPhotoToAlbum(
|
|
418
429
|
albumNodeUid: string,
|
|
419
|
-
payload:
|
|
430
|
+
payload: TransferEncryptedPhotoPayload,
|
|
420
431
|
signal?: AbortSignal,
|
|
421
432
|
): Promise<string> {
|
|
422
433
|
const { volumeId: sourceVolumeId, nodeId: sourceLinkId } = splitNodeUid(payload.nodeUid);
|
|
@@ -438,14 +449,13 @@ export class PhotosAPIService {
|
|
|
438
449
|
SignatureEmail: payload.signatureEmail,
|
|
439
450
|
Photos: {
|
|
440
451
|
ContentHash: payload.contentHash,
|
|
441
|
-
RelatedPhotos:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
})) || [],
|
|
452
|
+
RelatedPhotos: payload.relatedPhotos.map((related) => ({
|
|
453
|
+
LinkID: splitNodeUid(related.nodeUid).nodeId,
|
|
454
|
+
Hash: related.nameHash,
|
|
455
|
+
Name: related.encryptedName,
|
|
456
|
+
NodePassphrase: related.nodePassphrase,
|
|
457
|
+
ContentHash: related.contentHash,
|
|
458
|
+
})),
|
|
449
459
|
},
|
|
450
460
|
},
|
|
451
461
|
signal,
|
|
@@ -499,4 +509,51 @@ export class PhotosAPIService {
|
|
|
499
509
|
}
|
|
500
510
|
}
|
|
501
511
|
}
|
|
512
|
+
|
|
513
|
+
async addPhotoTags(nodeUid: string, tags: PhotoTag[]): Promise<void> {
|
|
514
|
+
const { volumeId, nodeId: linkId } = splitNodeUid(nodeUid);
|
|
515
|
+
await this.apiService.post<PostAddPhotoTagsRequest, { Code: number }>(
|
|
516
|
+
`drive/photos/volumes/${volumeId}/links/${linkId}/tags`,
|
|
517
|
+
{ Tags: tags },
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async removePhotoTags(nodeUid: string, tags: PhotoTag[]): Promise<void> {
|
|
522
|
+
const { volumeId, nodeId: linkId } = splitNodeUid(nodeUid);
|
|
523
|
+
await this.apiService.delete<PostRemovePhotoTagsRequest, { Code: number }>(
|
|
524
|
+
`drive/photos/volumes/${volumeId}/links/${linkId}/tags`,
|
|
525
|
+
{ Tags: tags },
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async setPhotoFavorite(nodeUid: string, payload?: TransferEncryptedPhotoPayload): Promise<void> {
|
|
530
|
+
const { volumeId, nodeId: linkId } = splitNodeUid(nodeUid);
|
|
531
|
+
const requestBody = payload
|
|
532
|
+
? {
|
|
533
|
+
PhotoData: {
|
|
534
|
+
Hash: payload.nameHash,
|
|
535
|
+
Name: payload.encryptedName,
|
|
536
|
+
NameSignatureEmail: payload.nameSignatureEmail,
|
|
537
|
+
NodePassphrase: payload.nodePassphrase,
|
|
538
|
+
ContentHash: payload.contentHash,
|
|
539
|
+
NodePassphraseSignature: payload.nodePassphraseSignature ?? null,
|
|
540
|
+
SignatureEmail: payload.signatureEmail ?? null,
|
|
541
|
+
RelatedPhotos: payload.relatedPhotos.map((related) => ({
|
|
542
|
+
LinkID: splitNodeUid(related.nodeUid).nodeId,
|
|
543
|
+
Hash: related.nameHash,
|
|
544
|
+
Name: related.encryptedName,
|
|
545
|
+
NameSignatureEmail: related.nameSignatureEmail,
|
|
546
|
+
NodePassphrase: related.nodePassphrase,
|
|
547
|
+
ContentHash: related.contentHash,
|
|
548
|
+
NodePassphraseSignature: related.nodePassphraseSignature ?? null,
|
|
549
|
+
SignatureEmail: related.signatureEmail ?? null,
|
|
550
|
+
})),
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
: undefined;
|
|
554
|
+
await this.apiService.post<PostFavoritePhotoRequest, { Code: number }>(
|
|
555
|
+
`drive/photos/volumes/${volumeId}/links/${linkId}/favorite`,
|
|
556
|
+
requestBody,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
502
559
|
}
|