@protontech/drive-sdk 0.13.1 → 0.14.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/interface/telemetry.d.ts +5 -4
- package/dist/interface/telemetry.js +1 -0
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/internal/download/telemetry.js +2 -1
- package/dist/internal/download/telemetry.js.map +1 -1
- package/dist/internal/nodes/cryptoReporter.js +2 -2
- package/dist/internal/nodes/cryptoReporter.js.map +1 -1
- package/dist/internal/photos/addToAlbum.d.ts +6 -0
- package/dist/internal/photos/addToAlbum.js +1 -0
- package/dist/internal/photos/addToAlbum.js.map +1 -1
- package/dist/internal/photos/albumsManager.d.ts +4 -1
- package/dist/internal/photos/albumsManager.js +22 -2
- package/dist/internal/photos/albumsManager.js.map +1 -1
- package/dist/internal/photos/albumsManager.test.js +41 -1
- package/dist/internal/photos/albumsManager.test.js.map +1 -1
- package/dist/internal/photos/apiService.d.ts +1 -0
- package/dist/internal/photos/apiService.js +59 -5
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/apiService.test.js +137 -0
- package/dist/internal/photos/apiService.test.js.map +1 -1
- package/dist/internal/photos/errors.d.ts +5 -0
- package/dist/internal/photos/errors.js +10 -1
- package/dist/internal/photos/errors.js.map +1 -1
- package/dist/internal/photos/index.js +1 -1
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/photosManager.d.ts +1 -0
- package/dist/internal/photos/photosManager.js +42 -4
- package/dist/internal/photos/photosManager.js.map +1 -1
- package/dist/internal/photos/photosManager.test.js +35 -0
- package/dist/internal/photos/photosManager.test.js.map +1 -1
- package/dist/internal/photos/photosTransferPayloadBuilder.d.ts +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.js +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.js.map +1 -1
- package/dist/internal/upload/telemetry.js +2 -1
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +4 -3
- package/dist/protonDrivePhotosClient.js +3 -3
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/package.json +1 -1
- package/src/interface/telemetry.ts +5 -4
- package/src/internal/download/telemetry.ts +2 -2
- package/src/internal/nodes/cryptoReporter.ts +4 -2
- package/src/internal/photos/addToAlbum.ts +1 -1
- package/src/internal/photos/albumsManager.test.ts +61 -1
- package/src/internal/photos/albumsManager.ts +23 -3
- package/src/internal/photos/apiService.test.ts +155 -0
- package/src/internal/photos/apiService.ts +92 -3
- package/src/internal/photos/errors.ts +11 -0
- package/src/internal/photos/index.ts +1 -1
- package/src/internal/photos/photosManager.test.ts +43 -1
- package/src/internal/photos/photosManager.ts +61 -3
- package/src/internal/photos/photosTransferPayloadBuilder.ts +3 -1
- package/src/internal/upload/telemetry.ts +2 -2
- package/src/protonDrivePhotosClient.ts +4 -4
|
@@ -5,6 +5,7 @@ import { PhotosAPIService } from './apiService';
|
|
|
5
5
|
import { AlbumsCryptoService } from './albumsCrypto';
|
|
6
6
|
import { PhotosNodesAccess } from './nodes';
|
|
7
7
|
import { DecryptedPhotoNode } from './interface';
|
|
8
|
+
import { MissingRelatedPhotosError } from './errors';
|
|
8
9
|
|
|
9
10
|
function createMockPhotoNode(uid: string, overrides: Partial<DecryptedPhotoNode> = {}): DecryptedPhotoNode {
|
|
10
11
|
return {
|
|
@@ -46,9 +47,19 @@ async function collectUpdateResults(manager: PhotosManager, photos: UpdatePhotoS
|
|
|
46
47
|
return results;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
async function collectSaveToTimelineResults(manager: PhotosManager, nodeUids: string[], signal?: AbortSignal) {
|
|
51
|
+
const results = [];
|
|
52
|
+
for await (const result of manager.saveToTimeline(nodeUids, signal)) {
|
|
53
|
+
results.push(result);
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
describe('PhotosManager', () => {
|
|
50
59
|
let logger: ReturnType<typeof getMockLogger>;
|
|
51
|
-
let apiService: jest.Mocked<
|
|
60
|
+
let apiService: jest.Mocked<
|
|
61
|
+
Pick<PhotosAPIService, 'addPhotoTags' | 'removePhotoTags' | 'setPhotoFavorite' | 'transferPhotos'>
|
|
62
|
+
>;
|
|
52
63
|
let cryptoService: jest.Mocked<Pick<AlbumsCryptoService, 'encryptPhotoForAlbum'>>;
|
|
53
64
|
let nodesService: jest.Mocked<
|
|
54
65
|
Pick<
|
|
@@ -80,6 +91,7 @@ describe('PhotosManager', () => {
|
|
|
80
91
|
addPhotoTags: jest.fn().mockResolvedValue(undefined),
|
|
81
92
|
removePhotoTags: jest.fn().mockResolvedValue(undefined),
|
|
82
93
|
setPhotoFavorite: jest.fn().mockResolvedValue(undefined),
|
|
94
|
+
transferPhotos: jest.fn().mockImplementation(async function* () {}),
|
|
83
95
|
};
|
|
84
96
|
|
|
85
97
|
cryptoService = {
|
|
@@ -263,4 +275,34 @@ describe('PhotosManager', () => {
|
|
|
263
275
|
});
|
|
264
276
|
});
|
|
265
277
|
});
|
|
278
|
+
|
|
279
|
+
describe('saveToTimeline', () => {
|
|
280
|
+
it('re-queues once on MissingRelatedPhotosError then succeeds without yielding the retry error', async () => {
|
|
281
|
+
const missingRelatedUid = 'volume1~related1';
|
|
282
|
+
let transferCall = 0;
|
|
283
|
+
apiService.transferPhotos.mockImplementation(async function* (_rootUid, payloads) {
|
|
284
|
+
transferCall++;
|
|
285
|
+
for (const payload of payloads) {
|
|
286
|
+
if (transferCall === 1) {
|
|
287
|
+
yield {
|
|
288
|
+
uid: payload.nodeUid,
|
|
289
|
+
ok: false,
|
|
290
|
+
error: new MissingRelatedPhotosError([missingRelatedUid]),
|
|
291
|
+
};
|
|
292
|
+
} else {
|
|
293
|
+
yield { uid: payload.nodeUid, ok: true };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const results = await collectSaveToTimelineResults(manager, ['volume1~photo1']);
|
|
299
|
+
|
|
300
|
+
expect(results).toEqual([{ uid: 'volume1~photo1', ok: true }]);
|
|
301
|
+
expect(apiService.transferPhotos).toHaveBeenCalledTimes(2);
|
|
302
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
303
|
+
`Missing related photos for saving volume1~photo1, re-queuing: ${missingRelatedUid}`,
|
|
304
|
+
);
|
|
305
|
+
expect(nodesService.notifyNodeChanged).toHaveBeenCalledWith('volume1~photo1');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
266
308
|
});
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
|
+
import { AbortError } from '../../errors';
|
|
3
4
|
import { Logger, NodeResultWithError, PhotoTag } from '../../interface';
|
|
5
|
+
import { batch } from '../batch';
|
|
4
6
|
import { PhotosAPIService } from './apiService';
|
|
5
7
|
import { PhotoAlreadyInTargetError, PhotoTransferPayloadBuilder, TransferEncryptedPhotoPayload } from './photosTransferPayloadBuilder';
|
|
6
8
|
import { PhotosNodesAccess } from './nodes';
|
|
7
9
|
import { AlbumsCryptoService } from './albumsCrypto';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
10
|
+
import { createBatches } from './addToAlbum';
|
|
11
|
+
import { MissingRelatedPhotosError } from './errors';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The number of photos that are loaded in parallel to prepare the payloads.
|
|
15
|
+
*/
|
|
16
|
+
const BATCH_LOADING_SIZE = 20;
|
|
11
17
|
|
|
12
18
|
export type UpdatePhotoSettings = {
|
|
13
19
|
nodeUid: string;
|
|
@@ -31,6 +37,58 @@ export class PhotosManager {
|
|
|
31
37
|
this.payloadBuilder = new PhotoTransferPayloadBuilder(albumsCryptoService, nodesService);
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
async *saveToTimeline(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResultWithError> {
|
|
41
|
+
const rootNode = await this.nodesService.getVolumeRootFolder();
|
|
42
|
+
const volumeRootKeys = await this.nodesService.getNodeKeys(rootNode.uid);
|
|
43
|
+
const signingKeys = await this.nodesService.getNodeSigningKeys({ nodeUid: rootNode.uid });
|
|
44
|
+
|
|
45
|
+
const queue: {
|
|
46
|
+
photoNodeUid: string;
|
|
47
|
+
additionalRelatedPhotoNodeUids: string[];
|
|
48
|
+
}[] = nodeUids.map((nodeUid) => ({ photoNodeUid: nodeUid, additionalRelatedPhotoNodeUids: [] }));
|
|
49
|
+
const retriedPhotoUids = new Set<string>();
|
|
50
|
+
|
|
51
|
+
while (queue.length > 0) {
|
|
52
|
+
const items = queue.splice(0, BATCH_LOADING_SIZE);
|
|
53
|
+
const { payloads, errors } = await this.payloadBuilder.preparePhotoPayloads(
|
|
54
|
+
items,
|
|
55
|
+
rootNode.uid,
|
|
56
|
+
volumeRootKeys,
|
|
57
|
+
signingKeys,
|
|
58
|
+
signal,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
for (const [uid, error] of errors) {
|
|
62
|
+
yield { uid, ok: false, error };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const batch of createBatches(payloads)) {
|
|
66
|
+
for await (const result of this.apiService.transferPhotos(rootNode.uid, batch, signal)) {
|
|
67
|
+
if (
|
|
68
|
+
!result.ok &&
|
|
69
|
+
result.error instanceof MissingRelatedPhotosError &&
|
|
70
|
+
!retriedPhotoUids.has(result.uid)
|
|
71
|
+
) {
|
|
72
|
+
retriedPhotoUids.add(result.uid);
|
|
73
|
+
this.logger.info(
|
|
74
|
+
`Missing related photos for saving ${result.uid}, re-queuing: ${result.error.missingNodeUids.join(', ')}`,
|
|
75
|
+
);
|
|
76
|
+
queue.push({
|
|
77
|
+
photoNodeUid: result.uid,
|
|
78
|
+
additionalRelatedPhotoNodeUids: result.error.missingNodeUids,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (result.ok) {
|
|
84
|
+
await this.nodesService.notifyNodeChanged(result.uid);
|
|
85
|
+
}
|
|
86
|
+
yield result;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
34
92
|
async *updatePhotos(photos: UpdatePhotoSettings[], signal?: AbortSignal): AsyncGenerator<NodeResultWithError> {
|
|
35
93
|
for await (const {
|
|
36
94
|
photoSettings: { nodeUid, tagsToAdd, tagsToRemove },
|
|
@@ -14,12 +14,13 @@ type TransferEncryptedRelatedPhotoPayload = {
|
|
|
14
14
|
nodeUid: string;
|
|
15
15
|
contentHash: string;
|
|
16
16
|
nameHash: string;
|
|
17
|
+
originalNameHash: string | undefined;
|
|
17
18
|
encryptedName: string;
|
|
18
19
|
nameSignatureEmail: string;
|
|
19
20
|
nodePassphrase: string;
|
|
20
21
|
nodePassphraseSignature?: string;
|
|
21
22
|
signatureEmail?: string;
|
|
22
|
-
}
|
|
23
|
+
};
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Item representing a photo to build a payload for.
|
|
@@ -186,6 +187,7 @@ export class PhotoTransferPayloadBuilder {
|
|
|
186
187
|
nodeUid: photoNode.uid,
|
|
187
188
|
contentHash: encryptedCrypto.contentHash,
|
|
188
189
|
nameHash: encryptedCrypto.hash,
|
|
190
|
+
originalNameHash: photoNode.hash,
|
|
189
191
|
encryptedName: encryptedCrypto.encryptedName,
|
|
190
192
|
nameSignatureEmail: encryptedCrypto.nameSignatureEmail,
|
|
191
193
|
nodePassphrase: encryptedCrypto.armoredNodePassphrase,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RateLimitedError, ValidationError, IntegrityError } from '../../errors';
|
|
2
|
-
import { ProtonDriveTelemetry, MetricsUploadErrorType, Logger } from '../../interface';
|
|
2
|
+
import { ProtonDriveTelemetry, MetricsUploadErrorType, Logger, MetricVolumeType } from '../../interface';
|
|
3
3
|
import { LoggerWithPrefix, reduceSizePrecision } from '../../telemetry';
|
|
4
4
|
import { APIHTTPError } from '../apiService';
|
|
5
5
|
import { splitNodeUid, splitNodeRevisionUid } from '../uids';
|
|
@@ -85,7 +85,7 @@ export class UploadTelemetry {
|
|
|
85
85
|
originalError?: unknown;
|
|
86
86
|
},
|
|
87
87
|
) {
|
|
88
|
-
let volumeType;
|
|
88
|
+
let volumeType = MetricVolumeType.Unknown;
|
|
89
89
|
try {
|
|
90
90
|
volumeType = await this.sharesService.getVolumeMetricContext(volumeId);
|
|
91
91
|
} catch (error: unknown) {
|
|
@@ -559,9 +559,9 @@ export class ProtonDrivePhotosClient {
|
|
|
559
559
|
*
|
|
560
560
|
* Photos in the timeline will not be deleted. If the album has photos
|
|
561
561
|
* that are not in the timeline (uploaded by another user), the method
|
|
562
|
-
* will throw an error.
|
|
563
|
-
*
|
|
564
|
-
* not in the timeline as well.
|
|
562
|
+
* will throw an error. Then, either the photos must be saved to the
|
|
563
|
+
* timelines with `saveToTimeline` option, or the album must be deleted
|
|
564
|
+
* with `force` option that deletes the photos not in the timeline as well.
|
|
565
565
|
*
|
|
566
566
|
* This operation is irreversible. Both the album and the photos will be
|
|
567
567
|
* permanently deleted, skipping the trash.
|
|
@@ -569,7 +569,7 @@ export class ProtonDrivePhotosClient {
|
|
|
569
569
|
* @param nodeUid - The UID of the album to delete.
|
|
570
570
|
* @param force - Whether to force the deletion.
|
|
571
571
|
*/
|
|
572
|
-
async deleteAlbum(nodeUid: NodeOrUid, options: { force?: boolean } = {}): Promise<void> {
|
|
572
|
+
async deleteAlbum(nodeUid: NodeOrUid, options: { force?: boolean; saveToTimeline?: boolean } = {}): Promise<void> {
|
|
573
573
|
this.logger.info(`Deleting album ${getUid(nodeUid)}`);
|
|
574
574
|
await this.photos.albums.deleteAlbum(getUid(nodeUid), options);
|
|
575
575
|
}
|