@protontech/drive-sdk 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interface/index.d.ts +1 -0
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +14 -10
- package/dist/interface/nodes.js +5 -8
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/photos.d.ts +62 -0
- package/dist/interface/photos.js +3 -0
- package/dist/interface/photos.js.map +1 -0
- package/dist/internal/apiService/apiService.d.ts +2 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +1294 -517
- package/dist/internal/apiService/errors.js +4 -3
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/download/cryptoService.js +8 -6
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.d.ts +2 -1
- package/dist/internal/download/fileDownloader.js +6 -3
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/index.d.ts +1 -1
- package/dist/internal/download/index.js +3 -3
- package/dist/internal/download/index.js.map +1 -1
- package/dist/internal/errors.d.ts +1 -0
- package/dist/internal/errors.js +4 -0
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +68 -16
- package/dist/internal/nodes/apiService.js +138 -85
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +7 -5
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.d.ts +16 -8
- package/dist/internal/nodes/cache.js +19 -5
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +1 -0
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoReporter.d.ts +3 -3
- package/dist/internal/nodes/cryptoReporter.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +13 -22
- package/dist/internal/nodes/cryptoService.js +47 -16
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +262 -17
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/events.d.ts +2 -2
- package/dist/internal/nodes/events.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -0
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +14 -3
- package/dist/internal/nodes/nodesAccess.d.ts +36 -20
- package/dist/internal/nodes/nodesAccess.js +54 -29
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +34 -14
- package/dist/internal/nodes/nodesManagement.js +44 -31
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +60 -14
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
- package/dist/internal/nodes/nodesRevisions.js.map +1 -1
- package/dist/internal/photos/albums.d.ts +2 -2
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/index.d.ts +19 -3
- package/dist/internal/photos/index.js +38 -8
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +18 -9
- package/dist/internal/photos/nodes.d.ts +57 -0
- package/dist/internal/photos/nodes.js +165 -0
- package/dist/internal/photos/nodes.js.map +1 -0
- package/dist/internal/photos/timeline.d.ts +2 -2
- package/dist/internal/photos/timeline.js.map +1 -1
- package/dist/internal/photos/timeline.test.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +2 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +6 -6
- package/dist/internal/sharingPublic/index.js +8 -7
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +16 -3
- package/dist/internal/sharingPublic/nodes.js +34 -2
- package/dist/internal/sharingPublic/nodes.js.map +1 -1
- package/dist/internal/sharingPublic/unauthApiService.d.ts +17 -0
- package/dist/internal/sharingPublic/unauthApiService.js +31 -0
- package/dist/internal/sharingPublic/unauthApiService.js.map +1 -0
- package/dist/internal/sharingPublic/unauthApiService.test.d.ts +1 -0
- package/dist/internal/sharingPublic/unauthApiService.test.js +27 -0
- package/dist/internal/sharingPublic/unauthApiService.test.js.map +1 -0
- package/dist/internal/upload/apiService.d.ts +4 -3
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +8 -3
- package/dist/internal/upload/cryptoService.js +45 -9
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +1 -1
- package/dist/internal/upload/fileUploader.test.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +25 -13
- package/dist/internal/upload/manager.js +7 -4
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +5 -4
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +9 -4
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +8 -5
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +11 -2
- package/dist/protonDriveClient.js +20 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +8 -8
- package/dist/protonDrivePhotosClient.js +8 -9
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +9 -2
- package/dist/protonDrivePublicLinkClient.js +16 -5
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.d.ts +7 -2
- package/dist/transformers.js +37 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/interface/index.ts +1 -0
- package/src/interface/nodes.ts +14 -11
- package/src/interface/photos.ts +67 -0
- package/src/internal/apiService/apiService.ts +2 -2
- package/src/internal/apiService/driveTypes.ts +1294 -517
- package/src/internal/apiService/errors.ts +5 -4
- package/src/internal/download/cryptoService.ts +13 -6
- package/src/internal/download/fileDownloader.ts +4 -2
- package/src/internal/download/index.ts +3 -0
- package/src/internal/errors.ts +4 -0
- package/src/internal/nodes/apiService.test.ts +7 -5
- package/src/internal/nodes/apiService.ts +210 -124
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/cache.ts +32 -13
- package/src/internal/nodes/cryptoReporter.ts +3 -3
- package/src/internal/nodes/cryptoService.test.ts +380 -18
- package/src/internal/nodes/cryptoService.ts +77 -36
- package/src/internal/nodes/events.ts +2 -2
- package/src/internal/nodes/index.test.ts +1 -0
- package/src/internal/nodes/interface.ts +17 -2
- package/src/internal/nodes/nodesAccess.ts +99 -54
- package/src/internal/nodes/nodesManagement.test.ts +69 -14
- package/src/internal/nodes/nodesManagement.ts +94 -48
- package/src/internal/nodes/nodesRevisions.ts +3 -3
- package/src/internal/photos/albums.ts +2 -2
- package/src/internal/photos/index.ts +45 -3
- package/src/internal/photos/interface.ts +21 -9
- package/src/internal/photos/nodes.ts +233 -0
- package/src/internal/photos/timeline.test.ts +2 -2
- package/src/internal/photos/timeline.ts +2 -2
- package/src/internal/photos/upload.ts +3 -3
- package/src/internal/sharingPublic/index.ts +7 -3
- package/src/internal/sharingPublic/nodes.ts +43 -2
- package/src/internal/sharingPublic/unauthApiService.test.ts +29 -0
- package/src/internal/sharingPublic/unauthApiService.ts +32 -0
- package/src/internal/upload/apiService.ts +4 -3
- package/src/internal/upload/cryptoService.ts +73 -12
- package/src/internal/upload/fileUploader.test.ts +1 -1
- package/src/internal/upload/interface.ts +24 -13
- package/src/internal/upload/manager.test.ts +5 -4
- package/src/internal/upload/manager.ts +7 -4
- package/src/internal/upload/streamUploader.test.ts +8 -5
- package/src/internal/upload/streamUploader.ts +10 -4
- package/src/protonDriveClient.ts +27 -5
- package/src/protonDrivePhotosClient.ts +23 -23
- package/src/protonDrivePublicLinkClient.ts +19 -3
- package/src/transformers.ts +49 -2
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { NodeWithSameNameExistsValidationError, ProtonDriveError, ValidationError } from '../../errors';
|
|
4
|
-
import { Logger, NodeResult } from '../../interface';
|
|
5
|
-
import { MemberRole, RevisionState } from '../../interface/nodes';
|
|
4
|
+
import { Logger, NodeResult, MemberRole, RevisionState, AnonymousUser } from '../../interface';
|
|
6
5
|
import {
|
|
7
6
|
DriveAPIService,
|
|
8
7
|
drivePaths,
|
|
@@ -102,7 +101,6 @@ type PostRestoreRevisionResponse =
|
|
|
102
101
|
type DeleteRevisionResponse =
|
|
103
102
|
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['delete']['responses']['200']['content']['application/json'];
|
|
104
103
|
|
|
105
|
-
|
|
106
104
|
type PostCheckAvailableHashesRequest = Extract<
|
|
107
105
|
drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/checkAvailableHashes']['post']['requestBody'],
|
|
108
106
|
{ content: object }
|
|
@@ -116,18 +114,21 @@ type PostCheckAvailableHashesResponse =
|
|
|
116
114
|
* The service is responsible for transforming local objects to API payloads
|
|
117
115
|
* and vice versa. It should not contain any business logic.
|
|
118
116
|
*/
|
|
119
|
-
export class
|
|
117
|
+
export abstract class NodeAPIServiceBase<
|
|
118
|
+
T extends EncryptedNode = EncryptedNode,
|
|
119
|
+
TMetadataResponseLink extends { Link: { LinkID: string } } = { Link: { LinkID: string } },
|
|
120
|
+
> {
|
|
120
121
|
constructor(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
protected logger: Logger,
|
|
123
|
+
protected apiService: DriveAPIService,
|
|
124
|
+
protected clientUid: string | undefined,
|
|
124
125
|
) {
|
|
125
126
|
this.logger = logger;
|
|
126
127
|
this.apiService = apiService;
|
|
127
128
|
this.clientUid = clientUid;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<
|
|
131
|
+
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<T> {
|
|
131
132
|
const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, undefined, signal);
|
|
132
133
|
const result = await nodesGenerator.next();
|
|
133
134
|
if (!result.value) {
|
|
@@ -142,7 +143,7 @@ export class NodeAPIService {
|
|
|
142
143
|
ownVolumeId: string | undefined,
|
|
143
144
|
filterOptions?: FilterOptions,
|
|
144
145
|
signal?: AbortSignal,
|
|
145
|
-
): AsyncGenerator<
|
|
146
|
+
): AsyncGenerator<T> {
|
|
146
147
|
const allNodeIds = nodeUids.map(splitNodeUid);
|
|
147
148
|
|
|
148
149
|
const nodeIdsByVolumeId = new Map<string, string[]>();
|
|
@@ -186,27 +187,21 @@ export class NodeAPIService {
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
protected async *iterateNodesPerVolume(
|
|
190
191
|
volumeId: string,
|
|
191
192
|
nodeIds: string[],
|
|
192
193
|
isOwnVolumeId: boolean,
|
|
193
194
|
filterOptions?: FilterOptions,
|
|
194
195
|
signal?: AbortSignal,
|
|
195
|
-
): AsyncGenerator<
|
|
196
|
+
): AsyncGenerator<T, unknown[]> {
|
|
196
197
|
const errors: unknown[] = [];
|
|
197
198
|
|
|
198
199
|
for (const nodeIdsBatch of batch(nodeIds, API_NODES_BATCH_SIZE)) {
|
|
199
|
-
const
|
|
200
|
-
`drive/v2/volumes/${volumeId}/links`,
|
|
201
|
-
{
|
|
202
|
-
LinkIDs: nodeIdsBatch,
|
|
203
|
-
},
|
|
204
|
-
signal,
|
|
205
|
-
);
|
|
200
|
+
const responseLinks = await this.fetchNodeMetadata(volumeId, nodeIdsBatch, signal);
|
|
206
201
|
|
|
207
|
-
for (const link of
|
|
202
|
+
for (const link of responseLinks) {
|
|
208
203
|
try {
|
|
209
|
-
const encryptedNode = linkToEncryptedNode(
|
|
204
|
+
const encryptedNode = this.linkToEncryptedNode(volumeId, link, isOwnVolumeId);
|
|
210
205
|
if (filterOptions?.type && encryptedNode.type !== filterOptions.type) {
|
|
211
206
|
continue;
|
|
212
207
|
}
|
|
@@ -221,6 +216,14 @@ export class NodeAPIService {
|
|
|
221
216
|
return errors;
|
|
222
217
|
}
|
|
223
218
|
|
|
219
|
+
protected abstract fetchNodeMetadata(
|
|
220
|
+
volumeId: string,
|
|
221
|
+
linkIds: string[],
|
|
222
|
+
signal?: AbortSignal,
|
|
223
|
+
): Promise<TMetadataResponseLink[]>;
|
|
224
|
+
|
|
225
|
+
protected abstract linkToEncryptedNode(volumeId: string, link: TMetadataResponseLink, isOwnVolumeId: boolean): T;
|
|
226
|
+
|
|
224
227
|
// Improvement requested: load next page sooner before all IDs are yielded.
|
|
225
228
|
async *iterateChildrenNodeUids(
|
|
226
229
|
parentNodeUid: string,
|
|
@@ -292,7 +295,7 @@ export class NodeAPIService {
|
|
|
292
295
|
},
|
|
293
296
|
newNode: {
|
|
294
297
|
encryptedName: string;
|
|
295
|
-
nameSignatureEmail: string;
|
|
298
|
+
nameSignatureEmail: string | AnonymousUser;
|
|
296
299
|
hash?: string;
|
|
297
300
|
},
|
|
298
301
|
signal?: AbortSignal,
|
|
@@ -332,9 +335,9 @@ export class NodeAPIService {
|
|
|
332
335
|
parentUid: string;
|
|
333
336
|
armoredNodePassphrase: string;
|
|
334
337
|
armoredNodePassphraseSignature?: string;
|
|
335
|
-
signatureEmail?: string;
|
|
338
|
+
signatureEmail?: string | AnonymousUser;
|
|
336
339
|
encryptedName: string;
|
|
337
|
-
nameSignatureEmail?: string;
|
|
340
|
+
nameSignatureEmail?: string | AnonymousUser;
|
|
338
341
|
hash: string;
|
|
339
342
|
contentHash?: string;
|
|
340
343
|
},
|
|
@@ -343,24 +346,29 @@ export class NodeAPIService {
|
|
|
343
346
|
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
344
347
|
const { nodeId: newParentNodeId } = splitNodeUid(newNode.parentUid);
|
|
345
348
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
349
|
+
try {
|
|
350
|
+
await this.apiService.put<Omit<PutMoveNodeRequest, 'SignatureAddress' | 'MIMEType'>, PutMoveNodeResponse>(
|
|
351
|
+
`drive/v2/volumes/${volumeId}/links/${nodeId}/move`,
|
|
352
|
+
{
|
|
353
|
+
ParentLinkID: newParentNodeId,
|
|
354
|
+
NodePassphrase: newNode.armoredNodePassphrase,
|
|
355
|
+
// @ts-expect-error: API accepts NodePassphraseSignature as optional.
|
|
356
|
+
NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
|
|
357
|
+
// @ts-expect-error: API accepts SignatureEmail as optional.
|
|
358
|
+
SignatureEmail: newNode.signatureEmail,
|
|
359
|
+
Name: newNode.encryptedName,
|
|
360
|
+
// @ts-expect-error: API accepts NameSignatureEmail as optional.
|
|
361
|
+
NameSignatureEmail: newNode.nameSignatureEmail,
|
|
362
|
+
Hash: newNode.hash,
|
|
363
|
+
OriginalHash: oldNode.hash,
|
|
364
|
+
ContentHash: newNode.contentHash || null,
|
|
365
|
+
},
|
|
366
|
+
signal,
|
|
367
|
+
);
|
|
368
|
+
} catch (error: unknown) {
|
|
369
|
+
handleNodeWithSameNameExistsValidationError(volumeId, error);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
364
372
|
}
|
|
365
373
|
|
|
366
374
|
async copyNode(
|
|
@@ -369,9 +377,9 @@ export class NodeAPIService {
|
|
|
369
377
|
parentUid: string;
|
|
370
378
|
armoredNodePassphrase: string;
|
|
371
379
|
armoredNodePassphraseSignature?: string;
|
|
372
|
-
signatureEmail?: string;
|
|
380
|
+
signatureEmail?: string | AnonymousUser;
|
|
373
381
|
encryptedName: string;
|
|
374
|
-
nameSignatureEmail?: string;
|
|
382
|
+
nameSignatureEmail?: string | AnonymousUser;
|
|
375
383
|
hash: string;
|
|
376
384
|
},
|
|
377
385
|
signal?: AbortSignal,
|
|
@@ -379,23 +387,29 @@ export class NodeAPIService {
|
|
|
379
387
|
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
380
388
|
const { volumeId: parentVolumeId, nodeId: parentNodeId } = splitNodeUid(newNode.parentUid);
|
|
381
389
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
390
|
+
let response: PostCopyNodeResponse;
|
|
391
|
+
try {
|
|
392
|
+
response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
|
|
393
|
+
`drive/volumes/${volumeId}/links/${nodeId}/copy`,
|
|
394
|
+
{
|
|
395
|
+
TargetVolumeID: parentVolumeId,
|
|
396
|
+
TargetParentLinkID: parentNodeId,
|
|
397
|
+
NodePassphrase: newNode.armoredNodePassphrase,
|
|
398
|
+
// @ts-expect-error: API accepts NodePassphraseSignature as optional.
|
|
399
|
+
NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
|
|
400
|
+
// @ts-expect-error: API accepts SignatureEmail as optional.
|
|
401
|
+
SignatureEmail: newNode.signatureEmail,
|
|
402
|
+
Name: newNode.encryptedName,
|
|
403
|
+
// @ts-expect-error: API accepts NameSignatureEmail as optional.
|
|
404
|
+
NameSignatureEmail: newNode.nameSignatureEmail,
|
|
405
|
+
Hash: newNode.hash,
|
|
406
|
+
},
|
|
407
|
+
signal,
|
|
408
|
+
);
|
|
409
|
+
} catch (error: unknown) {
|
|
410
|
+
handleNodeWithSameNameExistsValidationError(volumeId, error);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
399
413
|
|
|
400
414
|
return makeNodeUid(volumeId, response.LinkID);
|
|
401
415
|
}
|
|
@@ -430,7 +444,7 @@ export class NodeAPIService {
|
|
|
430
444
|
}
|
|
431
445
|
}
|
|
432
446
|
|
|
433
|
-
async *
|
|
447
|
+
async *deleteTrashedNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
434
448
|
for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
|
|
435
449
|
const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
|
|
436
450
|
`drive/v2/volumes/${volumeId}/trash/delete_multiple`,
|
|
@@ -445,6 +459,21 @@ export class NodeAPIService {
|
|
|
445
459
|
}
|
|
446
460
|
}
|
|
447
461
|
|
|
462
|
+
async *deleteExistingNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
463
|
+
for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
|
|
464
|
+
const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
|
|
465
|
+
`drive/v2/volumes/${volumeId}/delete_multiple`,
|
|
466
|
+
{
|
|
467
|
+
LinkIDs: batchNodeIds,
|
|
468
|
+
},
|
|
469
|
+
signal,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
// TODO: remove `as` when backend fixes OpenAPI schema.
|
|
473
|
+
yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
448
477
|
async createFolder(
|
|
449
478
|
parentUid: string,
|
|
450
479
|
newNode: {
|
|
@@ -452,7 +481,7 @@ export class NodeAPIService {
|
|
|
452
481
|
armoredHashKey: string;
|
|
453
482
|
armoredNodePassphrase: string;
|
|
454
483
|
armoredNodePassphraseSignature: string;
|
|
455
|
-
signatureEmail: string;
|
|
484
|
+
signatureEmail: string | AnonymousUser;
|
|
456
485
|
encryptedName: string;
|
|
457
486
|
hash: string;
|
|
458
487
|
armoredExtendedAttributes?: string;
|
|
@@ -478,21 +507,7 @@ export class NodeAPIService {
|
|
|
478
507
|
},
|
|
479
508
|
);
|
|
480
509
|
} catch (error: unknown) {
|
|
481
|
-
|
|
482
|
-
if (error.code === ErrorCode.ALREADY_EXISTS) {
|
|
483
|
-
const typedDetails = error.details as
|
|
484
|
-
| {
|
|
485
|
-
ConflictLinkID: string;
|
|
486
|
-
}
|
|
487
|
-
| undefined;
|
|
488
|
-
|
|
489
|
-
const existingNodeUid = typedDetails?.ConflictLinkID
|
|
490
|
-
? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
|
|
491
|
-
: undefined;
|
|
492
|
-
|
|
493
|
-
throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
510
|
+
handleNodeWithSameNameExistsValidationError(volumeId, error);
|
|
496
511
|
throw error;
|
|
497
512
|
}
|
|
498
513
|
|
|
@@ -570,6 +585,35 @@ export class NodeAPIService {
|
|
|
570
585
|
}
|
|
571
586
|
}
|
|
572
587
|
|
|
588
|
+
export class NodeAPIService extends NodeAPIServiceBase {
|
|
589
|
+
constructor(logger: Logger, apiService: DriveAPIService, clientUid: string | undefined) {
|
|
590
|
+
super(logger, apiService, clientUid);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
protected async fetchNodeMetadata(
|
|
594
|
+
volumeId: string,
|
|
595
|
+
linkIds: string[],
|
|
596
|
+
signal?: AbortSignal,
|
|
597
|
+
): Promise<PostLoadLinksMetadataResponse['Links']> {
|
|
598
|
+
const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
|
|
599
|
+
`drive/v2/volumes/${volumeId}/links`,
|
|
600
|
+
{
|
|
601
|
+
LinkIDs: linkIds,
|
|
602
|
+
},
|
|
603
|
+
signal,
|
|
604
|
+
);
|
|
605
|
+
return response.Links;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
protected linkToEncryptedNode(
|
|
609
|
+
volumeId: string,
|
|
610
|
+
link: PostLoadLinksMetadataResponse['Links'][0],
|
|
611
|
+
isOwnVolumeId: boolean,
|
|
612
|
+
): EncryptedNode {
|
|
613
|
+
return linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
573
617
|
type LinkResponse = {
|
|
574
618
|
LinkID: string;
|
|
575
619
|
Response: {
|
|
@@ -602,56 +646,36 @@ function* handleResponseErrors(
|
|
|
602
646
|
}
|
|
603
647
|
}
|
|
604
648
|
|
|
605
|
-
function
|
|
649
|
+
function handleNodeWithSameNameExistsValidationError(volumeId: string, error: unknown): void {
|
|
650
|
+
if (error instanceof ValidationError) {
|
|
651
|
+
if (error.code === ErrorCode.ALREADY_EXISTS) {
|
|
652
|
+
const typedDetails = error.details as
|
|
653
|
+
| {
|
|
654
|
+
ConflictLinkID: string;
|
|
655
|
+
}
|
|
656
|
+
| undefined;
|
|
657
|
+
|
|
658
|
+
const existingNodeUid = typedDetails?.ConflictLinkID
|
|
659
|
+
? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
|
|
660
|
+
: undefined;
|
|
661
|
+
|
|
662
|
+
throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function linkToEncryptedNode(
|
|
606
668
|
logger: Logger,
|
|
607
669
|
volumeId: string,
|
|
608
|
-
link: PostLoadLinksMetadataResponse['Links'][0],
|
|
670
|
+
link: Pick<PostLoadLinksMetadataResponse['Links'][0], 'Link' | 'Membership' | 'Sharing' | 'Folder' | 'File'>,
|
|
609
671
|
isAdmin: boolean,
|
|
610
672
|
): EncryptedNode {
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
// Basic node metadata
|
|
619
|
-
uid: makeNodeUid(volumeId, link.Link.LinkID),
|
|
620
|
-
parentUid: link.Link.ParentLinkID ? makeNodeUid(volumeId, link.Link.ParentLinkID) : undefined,
|
|
621
|
-
type: nodeTypeNumberToNodeType(logger, link.Link.Type),
|
|
622
|
-
creationTime: new Date(link.Link.CreateTime * 1000),
|
|
623
|
-
trashTime: link.Link.TrashTime ? new Date(link.Link.TrashTime * 1000) : undefined,
|
|
624
|
-
|
|
625
|
-
// Sharing node metadata
|
|
626
|
-
shareId: link.Sharing?.ShareID || undefined,
|
|
627
|
-
isShared: !!link.Sharing,
|
|
628
|
-
isSharedPublicly: !!link.Sharing?.ShareURLID,
|
|
629
|
-
directRole: isAdmin ? MemberRole.Admin : membershipRole,
|
|
630
|
-
membership: link.Membership
|
|
631
|
-
? {
|
|
632
|
-
role: membershipRole,
|
|
633
|
-
inviteTime: new Date(link.Membership.InviteTime * 1000),
|
|
634
|
-
}
|
|
635
|
-
: undefined,
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
const baseCryptoNodeMetadata = {
|
|
639
|
-
signatureEmail: link.Link.SignatureEmail || undefined,
|
|
640
|
-
nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
|
|
641
|
-
armoredKey: link.Link.NodeKey,
|
|
642
|
-
armoredNodePassphrase: link.Link.NodePassphrase,
|
|
643
|
-
armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
|
|
644
|
-
membership: link.Membership
|
|
645
|
-
? {
|
|
646
|
-
inviterEmail: link.Membership.InviterEmail,
|
|
647
|
-
base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
|
|
648
|
-
armoredInviterSharePassphraseKeyPacketSignature:
|
|
649
|
-
link.Membership.InviterSharePassphraseKeyPacketSignature,
|
|
650
|
-
armoredInviteeSharePassphraseSessionKeySignature:
|
|
651
|
-
link.Membership.InviteeSharePassphraseSessionKeySignature,
|
|
652
|
-
}
|
|
653
|
-
: undefined,
|
|
654
|
-
};
|
|
673
|
+
const { baseNodeMetadata, baseCryptoNodeMetadata } = linkToEncryptedNodeBaseMetadata(
|
|
674
|
+
logger,
|
|
675
|
+
volumeId,
|
|
676
|
+
link,
|
|
677
|
+
isAdmin,
|
|
678
|
+
);
|
|
655
679
|
|
|
656
680
|
if (link.Link.Type === 1 && link.Folder) {
|
|
657
681
|
return {
|
|
@@ -693,6 +717,11 @@ function linkToEncryptedNode(
|
|
|
693
717
|
};
|
|
694
718
|
}
|
|
695
719
|
|
|
720
|
+
// TODO: Remove this once client do not use main SDK for photos.
|
|
721
|
+
// At the beginning, the client used main SDK for some photo actions.
|
|
722
|
+
// This was a temporary solution before the Photos SDK was implemented.
|
|
723
|
+
// Now the client must use Photos SDK for all photo-related actions.
|
|
724
|
+
// Knowledge of albums in main SDK is deprecated and will be removed.
|
|
696
725
|
if (link.Link.Type === 3) {
|
|
697
726
|
return {
|
|
698
727
|
...baseNodeMetadata,
|
|
@@ -705,6 +734,64 @@ function linkToEncryptedNode(
|
|
|
705
734
|
throw new Error(`Unknown node type: ${link.Link.Type}`);
|
|
706
735
|
}
|
|
707
736
|
|
|
737
|
+
export function linkToEncryptedNodeBaseMetadata(
|
|
738
|
+
logger: Logger,
|
|
739
|
+
volumeId: string,
|
|
740
|
+
link: Pick<PostLoadLinksMetadataResponse['Links'][0], 'Link' | 'Membership' | 'Sharing'>,
|
|
741
|
+
isAdmin: boolean,
|
|
742
|
+
) {
|
|
743
|
+
const membershipRole = permissionsToMemberRole(logger, link.Membership?.Permissions);
|
|
744
|
+
|
|
745
|
+
const baseNodeMetadata = {
|
|
746
|
+
// Internal metadata
|
|
747
|
+
hash: link.Link.NameHash || undefined,
|
|
748
|
+
encryptedName: link.Link.Name,
|
|
749
|
+
|
|
750
|
+
// Basic node metadata
|
|
751
|
+
uid: makeNodeUid(volumeId, link.Link.LinkID),
|
|
752
|
+
parentUid: link.Link.ParentLinkID ? makeNodeUid(volumeId, link.Link.ParentLinkID) : undefined,
|
|
753
|
+
type: nodeTypeNumberToNodeType(logger, link.Link.Type),
|
|
754
|
+
creationTime: new Date(link.Link.CreateTime * 1000),
|
|
755
|
+
modificationTime: new Date(link.Link.ModifyTime * 1000),
|
|
756
|
+
trashTime: link.Link.TrashTime ? new Date(link.Link.TrashTime * 1000) : undefined,
|
|
757
|
+
|
|
758
|
+
// Sharing node metadata
|
|
759
|
+
shareId: link.Sharing?.ShareID || undefined,
|
|
760
|
+
isShared: !!link.Sharing,
|
|
761
|
+
isSharedPublicly: !!link.Sharing?.ShareURLID,
|
|
762
|
+
directRole: isAdmin ? MemberRole.Admin : membershipRole,
|
|
763
|
+
membership: link.Membership
|
|
764
|
+
? {
|
|
765
|
+
role: membershipRole,
|
|
766
|
+
inviteTime: new Date(link.Membership.InviteTime * 1000),
|
|
767
|
+
}
|
|
768
|
+
: undefined,
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const baseCryptoNodeMetadata = {
|
|
772
|
+
signatureEmail: link.Link.SignatureEmail || undefined,
|
|
773
|
+
nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
|
|
774
|
+
armoredKey: link.Link.NodeKey,
|
|
775
|
+
armoredNodePassphrase: link.Link.NodePassphrase,
|
|
776
|
+
armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
|
|
777
|
+
membership: link.Membership
|
|
778
|
+
? {
|
|
779
|
+
inviterEmail: link.Membership.InviterEmail,
|
|
780
|
+
base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
|
|
781
|
+
armoredInviterSharePassphraseKeyPacketSignature:
|
|
782
|
+
link.Membership.InviterSharePassphraseKeyPacketSignature,
|
|
783
|
+
armoredInviteeSharePassphraseSessionKeySignature:
|
|
784
|
+
link.Membership.InviteeSharePassphraseSessionKeySignature,
|
|
785
|
+
}
|
|
786
|
+
: undefined,
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
baseNodeMetadata,
|
|
791
|
+
baseCryptoNodeMetadata,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
708
795
|
export function* groupNodeUidsByVolumeAndIteratePerBatch(
|
|
709
796
|
nodeUids: string[],
|
|
710
797
|
): Generator<{ volumeId: string; batchNodeIds: string[]; batchNodeUids: string[] }> {
|
|
@@ -732,7 +819,6 @@ export function* groupNodeUidsByVolumeAndIteratePerBatch(
|
|
|
732
819
|
}
|
|
733
820
|
}
|
|
734
821
|
|
|
735
|
-
|
|
736
822
|
function transformRevisionResponse(
|
|
737
823
|
volumeId: string,
|
|
738
824
|
nodeId: string,
|
|
@@ -9,7 +9,9 @@ export enum CACHE_TAG_KEYS {
|
|
|
9
9
|
Roots = 'nodeRoot',
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
type DecryptedNodeResult
|
|
12
|
+
type DecryptedNodeResult<T extends DecryptedNode> =
|
|
13
|
+
| { uid: string; ok: true; node: T }
|
|
14
|
+
| { uid: string; ok: false; error: string };
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Provides caching for nodes metadata.
|
|
@@ -19,7 +21,7 @@ type DecryptedNodeResult = { uid: string; ok: true; node: DecryptedNode } | { ui
|
|
|
19
21
|
*
|
|
20
22
|
* The cache of node metadata should not contain any crypto material.
|
|
21
23
|
*/
|
|
22
|
-
export class
|
|
24
|
+
export abstract class NodesCacheBase<T extends DecryptedNode = DecryptedNode> {
|
|
23
25
|
constructor(
|
|
24
26
|
private logger: Logger,
|
|
25
27
|
private driveCache: ProtonDriveEntitiesCache,
|
|
@@ -28,9 +30,9 @@ export class NodesCache {
|
|
|
28
30
|
this.driveCache = driveCache;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
async setNode(node:
|
|
33
|
+
async setNode(node: T): Promise<void> {
|
|
32
34
|
const key = getCacheUid(node.uid);
|
|
33
|
-
const nodeData = serialiseNode(node);
|
|
35
|
+
const nodeData = this.serialiseNode(node);
|
|
34
36
|
const { volumeId } = splitNodeUid(node.uid);
|
|
35
37
|
|
|
36
38
|
const tags = [`volume:${volumeId}`];
|
|
@@ -46,11 +48,11 @@ export class NodesCache {
|
|
|
46
48
|
await this.driveCache.setEntity(key, nodeData, tags);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
async getNode(nodeUid: string): Promise<
|
|
51
|
+
async getNode(nodeUid: string): Promise<T> {
|
|
50
52
|
const key = getCacheUid(nodeUid);
|
|
51
53
|
const nodeData = await this.driveCache.getEntity(key);
|
|
52
54
|
try {
|
|
53
|
-
return deserialiseNode(nodeData);
|
|
55
|
+
return this.deserialiseNode(nodeData);
|
|
54
56
|
} catch (error: unknown) {
|
|
55
57
|
await this.removeCorruptedNode({ nodeUid }, error);
|
|
56
58
|
throw new Error(`Failed to deserialise node: ${error instanceof Error ? error.message : error}`, {
|
|
@@ -59,6 +61,10 @@ export class NodesCache {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
protected abstract serialiseNode(node: T): string;
|
|
65
|
+
|
|
66
|
+
protected abstract deserialiseNode(nodeData: string): T;
|
|
67
|
+
|
|
62
68
|
/**
|
|
63
69
|
* Set all nodes on given node as stale. This is useful when we
|
|
64
70
|
* get refresh event from the server and we thus don't know
|
|
@@ -146,7 +152,7 @@ export class NodesCache {
|
|
|
146
152
|
return cacheUids;
|
|
147
153
|
}
|
|
148
154
|
|
|
149
|
-
async *iterateNodes(nodeUids: string[]): AsyncGenerator<DecryptedNodeResult
|
|
155
|
+
async *iterateNodes(nodeUids: string[]): AsyncGenerator<DecryptedNodeResult<T>> {
|
|
150
156
|
const cacheUids = nodeUids.map(getCacheUid);
|
|
151
157
|
for await (const result of this.driveCache.iterateEntities(cacheUids)) {
|
|
152
158
|
const node = await this.convertCacheResult(result);
|
|
@@ -156,7 +162,7 @@ export class NodesCache {
|
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
async *iterateChildren(parentNodeUid: string): AsyncGenerator<DecryptedNodeResult
|
|
165
|
+
async *iterateChildren(parentNodeUid: string): AsyncGenerator<DecryptedNodeResult<T>> {
|
|
160
166
|
for await (const result of this.driveCache.iterateEntitiesByTag(
|
|
161
167
|
`${CACHE_TAG_KEYS.ParentUid}:${parentNodeUid}`,
|
|
162
168
|
)) {
|
|
@@ -171,7 +177,7 @@ export class NodesCache {
|
|
|
171
177
|
yield* this.driveCache.iterateEntitiesByTag(`${CACHE_TAG_KEYS.Roots}:${volumeId}`);
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
async *iterateTrashedNodes(): AsyncGenerator<DecryptedNodeResult
|
|
180
|
+
async *iterateTrashedNodes(): AsyncGenerator<DecryptedNodeResult<T>> {
|
|
175
181
|
for await (const result of this.driveCache.iterateEntitiesByTag(CACHE_TAG_KEYS.Trashed)) {
|
|
176
182
|
const node = await this.convertCacheResult(result);
|
|
177
183
|
if (node) {
|
|
@@ -184,7 +190,7 @@ export class NodesCache {
|
|
|
184
190
|
* Converts result from the cache with cache UID and data to result of node
|
|
185
191
|
* with node UID and DecryptedNode.
|
|
186
192
|
*/
|
|
187
|
-
private async convertCacheResult(result: EntityResult<string>): Promise<DecryptedNodeResult | null> {
|
|
193
|
+
private async convertCacheResult(result: EntityResult<string>): Promise<DecryptedNodeResult<T> | null> {
|
|
188
194
|
let nodeUid;
|
|
189
195
|
try {
|
|
190
196
|
nodeUid = getNodeUid(result.key);
|
|
@@ -195,7 +201,7 @@ export class NodesCache {
|
|
|
195
201
|
if (result.ok) {
|
|
196
202
|
let node;
|
|
197
203
|
try {
|
|
198
|
-
node = deserialiseNode(result.value);
|
|
204
|
+
node = this.deserialiseNode(result.value);
|
|
199
205
|
} catch (error: unknown) {
|
|
200
206
|
await this.removeCorruptedNode({ nodeUid }, error);
|
|
201
207
|
return null;
|
|
@@ -232,6 +238,16 @@ export class NodesCache {
|
|
|
232
238
|
}
|
|
233
239
|
}
|
|
234
240
|
|
|
241
|
+
export class NodesCache extends NodesCacheBase<DecryptedNode> {
|
|
242
|
+
protected serialiseNode(node: DecryptedNode): string {
|
|
243
|
+
return serialiseNode(node);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
protected deserialiseNode(nodeData: string): DecryptedNode {
|
|
247
|
+
return deserialiseNode(nodeData);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
235
251
|
function getCacheUid(nodeUid: string) {
|
|
236
252
|
return `node-${nodeUid}`;
|
|
237
253
|
}
|
|
@@ -243,11 +259,12 @@ function getNodeUid(cacheUid: string) {
|
|
|
243
259
|
return cacheUid.substring(5);
|
|
244
260
|
}
|
|
245
261
|
|
|
246
|
-
function serialiseNode(node: DecryptedNode) {
|
|
262
|
+
export function serialiseNode(node: DecryptedNode) {
|
|
247
263
|
return JSON.stringify(node);
|
|
248
264
|
}
|
|
249
265
|
|
|
250
|
-
|
|
266
|
+
// TODO: use better deserialisation with validation
|
|
267
|
+
export function deserialiseNode(nodeData: string): DecryptedNode {
|
|
251
268
|
const node = JSON.parse(nodeData);
|
|
252
269
|
if (
|
|
253
270
|
!node ||
|
|
@@ -263,6 +280,7 @@ function deserialiseNode(nodeData: string): DecryptedNode {
|
|
|
263
280
|
typeof node.isShared !== 'boolean' ||
|
|
264
281
|
!node.creationTime ||
|
|
265
282
|
typeof node.creationTime !== 'string' ||
|
|
283
|
+
typeof node.modificationTime !== 'string' ||
|
|
266
284
|
(typeof node.trashTime !== 'string' && node.trashTime !== undefined) ||
|
|
267
285
|
(typeof node.folder !== 'object' && node.folder !== undefined) ||
|
|
268
286
|
(typeof node.folder?.claimedModificationTime !== 'string' && node.folder?.claimedModificationTime !== undefined)
|
|
@@ -272,6 +290,7 @@ function deserialiseNode(nodeData: string): DecryptedNode {
|
|
|
272
290
|
return {
|
|
273
291
|
...node,
|
|
274
292
|
creationTime: new Date(node.creationTime),
|
|
293
|
+
modificationTime: new Date(node.modificationTime),
|
|
275
294
|
trashTime: node.trashTime ? new Date(node.trashTime) : undefined,
|
|
276
295
|
activeRevision: node.activeRevision ? deserialiseRevision(node.activeRevision) : undefined,
|
|
277
296
|
membership: node.membership
|
|
@@ -34,7 +34,7 @@ export class NodesCryptoReporter {
|
|
|
34
34
|
signatureType: string,
|
|
35
35
|
verified: VERIFICATION_STATUS,
|
|
36
36
|
verificationErrors?: Error[],
|
|
37
|
-
claimedAuthor?: string,
|
|
37
|
+
claimedAuthor?: string | AnonymousUser,
|
|
38
38
|
notAvailableVerificationKeys = false,
|
|
39
39
|
): Promise<Author> {
|
|
40
40
|
const author = handleClaimedAuthor(
|
|
@@ -54,7 +54,7 @@ export class NodesCryptoReporter {
|
|
|
54
54
|
node: { uid: string; creationTime: Date },
|
|
55
55
|
field: MetricVerificationErrorField,
|
|
56
56
|
verificationErrors?: Error[],
|
|
57
|
-
claimedAuthor?: string,
|
|
57
|
+
claimedAuthor?: string | AnonymousUser,
|
|
58
58
|
) {
|
|
59
59
|
if (this.reportedVerificationErrors.has(node.uid)) {
|
|
60
60
|
return;
|
|
@@ -128,7 +128,7 @@ function handleClaimedAuthor(
|
|
|
128
128
|
signatureType: string,
|
|
129
129
|
verified: VERIFICATION_STATUS,
|
|
130
130
|
verificationErrors?: Error[],
|
|
131
|
-
claimedAuthor?: string,
|
|
131
|
+
claimedAuthor?: string | AnonymousUser,
|
|
132
132
|
notAvailableVerificationKeys = false,
|
|
133
133
|
): Author {
|
|
134
134
|
if (!claimedAuthor && notAvailableVerificationKeys) {
|