@protontech/drive-sdk 0.7.0 → 0.7.2
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/driveTypes.d.ts +1294 -517
- 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 +60 -9
- package/dist/internal/nodes/apiService.js +125 -81
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +2 -0
- 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/cryptoService.d.ts +1 -1
- package/dist/internal/nodes/cryptoService.js +4 -4
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +3 -3
- 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 +1 -0
- package/dist/internal/nodes/nodesAccess.d.ts +29 -20
- package/dist/internal/nodes/nodesAccess.js +41 -29
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +32 -12
- package/dist/internal/nodes/nodesManagement.js +30 -13
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +39 -4
- 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 +166 -0
- package/dist/internal/photos/nodes.js.map +1 -0
- package/dist/internal/photos/nodes.test.d.ts +1 -0
- package/dist/internal/photos/nodes.test.js +233 -0
- package/dist/internal/photos/nodes.test.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/protonDriveClient.d.ts +10 -1
- package/dist/protonDriveClient.js +18 -3
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +12 -8
- package/dist/protonDrivePhotosClient.js +15 -9
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +7 -1
- package/dist/protonDrivePublicLinkClient.js +9 -0
- 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/driveTypes.ts +1294 -517
- package/src/internal/errors.ts +4 -0
- package/src/internal/nodes/apiService.test.ts +2 -0
- package/src/internal/nodes/apiService.ts +187 -114
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/cache.ts +32 -13
- package/src/internal/nodes/cryptoService.test.ts +13 -3
- package/src/internal/nodes/cryptoService.ts +4 -4
- package/src/internal/nodes/events.ts +2 -2
- package/src/internal/nodes/index.test.ts +1 -0
- package/src/internal/nodes/interface.ts +1 -0
- package/src/internal/nodes/nodesAccess.ts +82 -54
- package/src/internal/nodes/nodesManagement.test.ts +48 -4
- package/src/internal/nodes/nodesManagement.ts +76 -26
- 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.test.ts +258 -0
- package/src/internal/photos/nodes.ts +234 -0
- package/src/internal/photos/timeline.test.ts +2 -2
- package/src/internal/photos/timeline.ts +2 -2
- package/src/protonDriveClient.ts +20 -3
- package/src/protonDrivePhotosClient.ts +31 -23
- package/src/protonDrivePublicLinkClient.ts +11 -0
- package/src/transformers.ts +49 -2
package/src/internal/errors.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { c } from 'ttag';
|
|
|
3
3
|
import { VERIFICATION_STATUS } from '../crypto';
|
|
4
4
|
import { AbortError, ConnectionError, RateLimitedError, ValidationError } from '../errors';
|
|
5
5
|
|
|
6
|
+
export function createErrorFromUnknown(error: unknown): Error {
|
|
7
|
+
return error instanceof Error ? error : new Error(getErrorMessage(error), { cause: error });
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export function getErrorMessage(error: unknown): string {
|
|
7
11
|
return error instanceof Error ? error.message : c('Error').t`Unknown error`;
|
|
8
12
|
}
|
|
@@ -65,6 +65,7 @@ function generateAPINode() {
|
|
|
65
65
|
ParentLinkID: 'parentLinkId',
|
|
66
66
|
NameHash: 'nameHash',
|
|
67
67
|
CreateTime: 123456789,
|
|
68
|
+
ModifyTime: 1234567890,
|
|
68
69
|
TrashTime: 0,
|
|
69
70
|
|
|
70
71
|
Name: 'encName',
|
|
@@ -140,6 +141,7 @@ function generateNode() {
|
|
|
140
141
|
uid: 'volumeId~linkId',
|
|
141
142
|
parentUid: 'volumeId~parentLinkId',
|
|
142
143
|
creationTime: new Date(123456789000),
|
|
144
|
+
modificationTime: new Date(1234567890000),
|
|
143
145
|
trashTime: undefined,
|
|
144
146
|
|
|
145
147
|
shareId: undefined,
|
|
@@ -114,18 +114,21 @@ type PostCheckAvailableHashesResponse =
|
|
|
114
114
|
* The service is responsible for transforming local objects to API payloads
|
|
115
115
|
* and vice versa. It should not contain any business logic.
|
|
116
116
|
*/
|
|
117
|
-
export class
|
|
117
|
+
export abstract class NodeAPIServiceBase<
|
|
118
|
+
T extends EncryptedNode = EncryptedNode,
|
|
119
|
+
TMetadataResponseLink extends { Link: { LinkID: string } } = { Link: { LinkID: string } },
|
|
120
|
+
> {
|
|
118
121
|
constructor(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
protected logger: Logger,
|
|
123
|
+
protected apiService: DriveAPIService,
|
|
124
|
+
protected clientUid: string | undefined,
|
|
122
125
|
) {
|
|
123
126
|
this.logger = logger;
|
|
124
127
|
this.apiService = apiService;
|
|
125
128
|
this.clientUid = clientUid;
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<
|
|
131
|
+
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<T> {
|
|
129
132
|
const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, undefined, signal);
|
|
130
133
|
const result = await nodesGenerator.next();
|
|
131
134
|
if (!result.value) {
|
|
@@ -140,7 +143,7 @@ export class NodeAPIService {
|
|
|
140
143
|
ownVolumeId: string | undefined,
|
|
141
144
|
filterOptions?: FilterOptions,
|
|
142
145
|
signal?: AbortSignal,
|
|
143
|
-
): AsyncGenerator<
|
|
146
|
+
): AsyncGenerator<T> {
|
|
144
147
|
const allNodeIds = nodeUids.map(splitNodeUid);
|
|
145
148
|
|
|
146
149
|
const nodeIdsByVolumeId = new Map<string, string[]>();
|
|
@@ -184,27 +187,21 @@ export class NodeAPIService {
|
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
|
|
190
|
+
protected async *iterateNodesPerVolume(
|
|
188
191
|
volumeId: string,
|
|
189
192
|
nodeIds: string[],
|
|
190
193
|
isOwnVolumeId: boolean,
|
|
191
194
|
filterOptions?: FilterOptions,
|
|
192
195
|
signal?: AbortSignal,
|
|
193
|
-
): AsyncGenerator<
|
|
196
|
+
): AsyncGenerator<T, unknown[]> {
|
|
194
197
|
const errors: unknown[] = [];
|
|
195
198
|
|
|
196
199
|
for (const nodeIdsBatch of batch(nodeIds, API_NODES_BATCH_SIZE)) {
|
|
197
|
-
const
|
|
198
|
-
`drive/v2/volumes/${volumeId}/links`,
|
|
199
|
-
{
|
|
200
|
-
LinkIDs: nodeIdsBatch,
|
|
201
|
-
},
|
|
202
|
-
signal,
|
|
203
|
-
);
|
|
200
|
+
const responseLinks = await this.fetchNodeMetadata(volumeId, nodeIdsBatch, signal);
|
|
204
201
|
|
|
205
|
-
for (const link of
|
|
202
|
+
for (const link of responseLinks) {
|
|
206
203
|
try {
|
|
207
|
-
const encryptedNode = linkToEncryptedNode(
|
|
204
|
+
const encryptedNode = this.linkToEncryptedNode(volumeId, link, isOwnVolumeId);
|
|
208
205
|
if (filterOptions?.type && encryptedNode.type !== filterOptions.type) {
|
|
209
206
|
continue;
|
|
210
207
|
}
|
|
@@ -219,6 +216,14 @@ export class NodeAPIService {
|
|
|
219
216
|
return errors;
|
|
220
217
|
}
|
|
221
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
|
+
|
|
222
227
|
// Improvement requested: load next page sooner before all IDs are yielded.
|
|
223
228
|
async *iterateChildrenNodeUids(
|
|
224
229
|
parentNodeUid: string,
|
|
@@ -341,24 +346,29 @@ export class NodeAPIService {
|
|
|
341
346
|
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
342
347
|
const { nodeId: newParentNodeId } = splitNodeUid(newNode.parentUid);
|
|
343
348
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
372
|
}
|
|
363
373
|
|
|
364
374
|
async copyNode(
|
|
@@ -377,23 +387,29 @@ export class NodeAPIService {
|
|
|
377
387
|
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
378
388
|
const { volumeId: parentVolumeId, nodeId: parentNodeId } = splitNodeUid(newNode.parentUid);
|
|
379
389
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
+
}
|
|
397
413
|
|
|
398
414
|
return makeNodeUid(volumeId, response.LinkID);
|
|
399
415
|
}
|
|
@@ -491,21 +507,7 @@ export class NodeAPIService {
|
|
|
491
507
|
},
|
|
492
508
|
);
|
|
493
509
|
} catch (error: unknown) {
|
|
494
|
-
|
|
495
|
-
if (error.code === ErrorCode.ALREADY_EXISTS) {
|
|
496
|
-
const typedDetails = error.details as
|
|
497
|
-
| {
|
|
498
|
-
ConflictLinkID: string;
|
|
499
|
-
}
|
|
500
|
-
| undefined;
|
|
501
|
-
|
|
502
|
-
const existingNodeUid = typedDetails?.ConflictLinkID
|
|
503
|
-
? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
|
|
504
|
-
: undefined;
|
|
505
|
-
|
|
506
|
-
throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
510
|
+
handleNodeWithSameNameExistsValidationError(volumeId, error);
|
|
509
511
|
throw error;
|
|
510
512
|
}
|
|
511
513
|
|
|
@@ -583,6 +585,35 @@ export class NodeAPIService {
|
|
|
583
585
|
}
|
|
584
586
|
}
|
|
585
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
|
+
|
|
586
617
|
type LinkResponse = {
|
|
587
618
|
LinkID: string;
|
|
588
619
|
Response: {
|
|
@@ -615,56 +646,36 @@ function* handleResponseErrors(
|
|
|
615
646
|
}
|
|
616
647
|
}
|
|
617
648
|
|
|
618
|
-
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(
|
|
619
668
|
logger: Logger,
|
|
620
669
|
volumeId: string,
|
|
621
|
-
link: PostLoadLinksMetadataResponse['Links'][0],
|
|
670
|
+
link: Pick<PostLoadLinksMetadataResponse['Links'][0], 'Link' | 'Membership' | 'Sharing' | 'Folder' | 'File'>,
|
|
622
671
|
isAdmin: boolean,
|
|
623
672
|
): EncryptedNode {
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// Basic node metadata
|
|
632
|
-
uid: makeNodeUid(volumeId, link.Link.LinkID),
|
|
633
|
-
parentUid: link.Link.ParentLinkID ? makeNodeUid(volumeId, link.Link.ParentLinkID) : undefined,
|
|
634
|
-
type: nodeTypeNumberToNodeType(logger, link.Link.Type),
|
|
635
|
-
creationTime: new Date(link.Link.CreateTime * 1000),
|
|
636
|
-
trashTime: link.Link.TrashTime ? new Date(link.Link.TrashTime * 1000) : undefined,
|
|
637
|
-
|
|
638
|
-
// Sharing node metadata
|
|
639
|
-
shareId: link.Sharing?.ShareID || undefined,
|
|
640
|
-
isShared: !!link.Sharing,
|
|
641
|
-
isSharedPublicly: !!link.Sharing?.ShareURLID,
|
|
642
|
-
directRole: isAdmin ? MemberRole.Admin : membershipRole,
|
|
643
|
-
membership: link.Membership
|
|
644
|
-
? {
|
|
645
|
-
role: membershipRole,
|
|
646
|
-
inviteTime: new Date(link.Membership.InviteTime * 1000),
|
|
647
|
-
}
|
|
648
|
-
: undefined,
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
const baseCryptoNodeMetadata = {
|
|
652
|
-
signatureEmail: link.Link.SignatureEmail || undefined,
|
|
653
|
-
nameSignatureEmail: link.Link.NameSignatureEmail || undefined,
|
|
654
|
-
armoredKey: link.Link.NodeKey,
|
|
655
|
-
armoredNodePassphrase: link.Link.NodePassphrase,
|
|
656
|
-
armoredNodePassphraseSignature: link.Link.NodePassphraseSignature,
|
|
657
|
-
membership: link.Membership
|
|
658
|
-
? {
|
|
659
|
-
inviterEmail: link.Membership.InviterEmail,
|
|
660
|
-
base64MemberSharePassphraseKeyPacket: link.Membership.MemberSharePassphraseKeyPacket,
|
|
661
|
-
armoredInviterSharePassphraseKeyPacketSignature:
|
|
662
|
-
link.Membership.InviterSharePassphraseKeyPacketSignature,
|
|
663
|
-
armoredInviteeSharePassphraseSessionKeySignature:
|
|
664
|
-
link.Membership.InviteeSharePassphraseSessionKeySignature,
|
|
665
|
-
}
|
|
666
|
-
: undefined,
|
|
667
|
-
};
|
|
673
|
+
const { baseNodeMetadata, baseCryptoNodeMetadata } = linkToEncryptedNodeBaseMetadata(
|
|
674
|
+
logger,
|
|
675
|
+
volumeId,
|
|
676
|
+
link,
|
|
677
|
+
isAdmin,
|
|
678
|
+
);
|
|
668
679
|
|
|
669
680
|
if (link.Link.Type === 1 && link.Folder) {
|
|
670
681
|
return {
|
|
@@ -706,6 +717,11 @@ function linkToEncryptedNode(
|
|
|
706
717
|
};
|
|
707
718
|
}
|
|
708
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.
|
|
709
725
|
if (link.Link.Type === 3) {
|
|
710
726
|
return {
|
|
711
727
|
...baseNodeMetadata,
|
|
@@ -718,6 +734,64 @@ function linkToEncryptedNode(
|
|
|
718
734
|
throw new Error(`Unknown node type: ${link.Link.Type}`);
|
|
719
735
|
}
|
|
720
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
|
+
|
|
721
795
|
export function* groupNodeUidsByVolumeAndIteratePerBatch(
|
|
722
796
|
nodeUids: string[],
|
|
723
797
|
): Generator<{ volumeId: string; batchNodeIds: string[]; batchNodeUids: string[] }> {
|
|
@@ -745,7 +819,6 @@ export function* groupNodeUidsByVolumeAndIteratePerBatch(
|
|
|
745
819
|
}
|
|
746
820
|
}
|
|
747
821
|
|
|
748
|
-
|
|
749
822
|
function transformRevisionResponse(
|
|
750
823
|
volumeId: string,
|
|
751
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
|
|
@@ -1327,7 +1327,12 @@ describe('nodesCryptoService', () => {
|
|
|
1327
1327
|
key: 'addressKey' as any,
|
|
1328
1328
|
};
|
|
1329
1329
|
|
|
1330
|
-
const result = await cryptoService.encryptNodeWithNewParent(
|
|
1330
|
+
const result = await cryptoService.encryptNodeWithNewParent(
|
|
1331
|
+
node.name,
|
|
1332
|
+
keys as any,
|
|
1333
|
+
parentKeys,
|
|
1334
|
+
signingKeys,
|
|
1335
|
+
);
|
|
1331
1336
|
|
|
1332
1337
|
expect(result).toEqual({
|
|
1333
1338
|
encryptedName: 'encryptedNodeName',
|
|
@@ -1360,7 +1365,12 @@ describe('nodesCryptoService', () => {
|
|
|
1360
1365
|
parentNodeKey: 'parentNodeKey' as any,
|
|
1361
1366
|
};
|
|
1362
1367
|
|
|
1363
|
-
const result = await cryptoService.encryptNodeWithNewParent(
|
|
1368
|
+
const result = await cryptoService.encryptNodeWithNewParent(
|
|
1369
|
+
node.name,
|
|
1370
|
+
keys as any,
|
|
1371
|
+
parentKeys,
|
|
1372
|
+
signingKeys,
|
|
1373
|
+
);
|
|
1364
1374
|
|
|
1365
1375
|
expect(result).toEqual({
|
|
1366
1376
|
encryptedName: 'encryptedNodeName',
|
|
@@ -1407,7 +1417,7 @@ describe('nodesCryptoService', () => {
|
|
|
1407
1417
|
};
|
|
1408
1418
|
|
|
1409
1419
|
await expect(
|
|
1410
|
-
cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, signingKeys),
|
|
1420
|
+
cryptoService.encryptNodeWithNewParent(node.name, keys as any, parentKeys, signingKeys),
|
|
1411
1421
|
).rejects.toThrow('Moving item to a non-folder is not allowed');
|
|
1412
1422
|
});
|
|
1413
1423
|
|
|
@@ -637,7 +637,7 @@ export class NodesCryptoService {
|
|
|
637
637
|
}
|
|
638
638
|
|
|
639
639
|
async encryptNodeWithNewParent(
|
|
640
|
-
|
|
640
|
+
nodeName: DecryptedNode['name'],
|
|
641
641
|
keys: { passphrase: string; passphraseSessionKey: SessionKey; nameSessionKey: SessionKey },
|
|
642
642
|
parentKeys: { key: PrivateKey; hashKey: Uint8Array },
|
|
643
643
|
signingKeys: NodeSigningKeys,
|
|
@@ -652,7 +652,7 @@ export class NodesCryptoService {
|
|
|
652
652
|
if (!parentKeys.hashKey) {
|
|
653
653
|
throw new ValidationError('Moving item to a non-folder is not allowed');
|
|
654
654
|
}
|
|
655
|
-
if (!
|
|
655
|
+
if (!nodeName.ok) {
|
|
656
656
|
throw new ValidationError('Cannot move item without a valid name, please rename the item first');
|
|
657
657
|
}
|
|
658
658
|
|
|
@@ -664,12 +664,12 @@ export class NodesCryptoService {
|
|
|
664
664
|
}
|
|
665
665
|
|
|
666
666
|
const { armoredNodeName } = await this.driveCrypto.encryptNodeName(
|
|
667
|
-
|
|
667
|
+
nodeName.value,
|
|
668
668
|
keys.nameSessionKey,
|
|
669
669
|
parentKeys.key,
|
|
670
670
|
nameAndPassprhaseSigningKey,
|
|
671
671
|
);
|
|
672
|
-
const hash = await this.driveCrypto.generateLookupHash(
|
|
672
|
+
const hash = await this.driveCrypto.generateLookupHash(nodeName.value, parentKeys.hashKey);
|
|
673
673
|
const { armoredPassphrase, armoredPassphraseSignature } = await this.driveCrypto.encryptPassphrase(
|
|
674
674
|
keys.passphrase,
|
|
675
675
|
keys.passphraseSessionKey,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Logger } from '../../interface';
|
|
2
2
|
import { DriveEvent, DriveEventType } from '../events';
|
|
3
|
-
import {
|
|
3
|
+
import { NodesCacheBase } from './cache';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Provides internal event handling.
|
|
@@ -11,7 +11,7 @@ import { NodesCache } from './cache';
|
|
|
11
11
|
export class NodesEventsHandler {
|
|
12
12
|
constructor(
|
|
13
13
|
private logger: Logger,
|
|
14
|
-
private cache:
|
|
14
|
+
private cache: NodesCacheBase,
|
|
15
15
|
) {}
|
|
16
16
|
|
|
17
17
|
async updateNodesCacheOnEvent(event: DriveEvent): Promise<void> {
|