@protontech/drive-sdk 0.4.1 → 0.5.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/diagnostic/{sdkDiagnosticFull.d.ts → diagnostic.d.ts} +5 -4
- package/dist/diagnostic/{sdkDiagnosticFull.js → diagnostic.js} +13 -10
- package/dist/diagnostic/diagnostic.js.map +1 -0
- package/dist/diagnostic/index.js +2 -4
- package/dist/diagnostic/index.js.map +1 -1
- package/dist/diagnostic/interface.d.ts +22 -1
- package/dist/diagnostic/sdkDiagnostic.d.ts +3 -2
- package/dist/diagnostic/sdkDiagnostic.js +80 -8
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/interface/download.d.ts +4 -4
- package/dist/interface/index.d.ts +1 -1
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +9 -0
- package/dist/interface/telemetry.d.ts +4 -1
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/interface/upload.d.ts +6 -3
- package/dist/internal/apiService/apiService.d.ts +3 -0
- package/dist/internal/apiService/apiService.js +25 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +38 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +2595 -2397
- package/dist/internal/apiService/errors.js +3 -0
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/errors.test.js +15 -7
- package/dist/internal/apiService/errors.test.js.map +1 -1
- package/dist/internal/asyncIteratorMap.d.ts +1 -1
- package/dist/internal/asyncIteratorMap.js +6 -1
- package/dist/internal/asyncIteratorMap.js.map +1 -1
- package/dist/internal/asyncIteratorMap.test.js +9 -0
- package/dist/internal/asyncIteratorMap.test.js.map +1 -1
- package/dist/internal/download/controller.d.ts +2 -0
- package/dist/internal/download/controller.js +15 -1
- package/dist/internal/download/controller.js.map +1 -1
- package/dist/internal/download/fileDownloader.d.ts +3 -3
- package/dist/internal/download/fileDownloader.js +11 -6
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +8 -8
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +6 -1
- package/dist/internal/nodes/apiService.js +71 -44
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +204 -15
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/debouncer.d.ts +24 -0
- package/dist/internal/nodes/debouncer.js +92 -0
- package/dist/internal/nodes/debouncer.js.map +1 -0
- package/dist/internal/nodes/debouncer.test.d.ts +1 -0
- package/dist/internal/nodes/debouncer.test.js +108 -0
- package/dist/internal/nodes/debouncer.test.js.map +1 -0
- package/dist/internal/nodes/extendedAttributes.js +2 -2
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/index.js +1 -1
- package/dist/internal/nodes/index.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.d.ts +6 -4
- package/dist/internal/nodes/nodesAccess.js +29 -9
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +19 -7
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +2 -2
- package/dist/internal/nodes/nodesManagement.js +5 -3
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +3 -1
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/photos/apiService.js +9 -20
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +2 -1
- package/dist/internal/photos/upload.js +9 -3
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharing/apiService.d.ts +1 -1
- package/dist/internal/sharing/apiService.js +2 -2
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.d.ts +4 -1
- package/dist/internal/sharing/sharingManagement.js +7 -4
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharingPublic/apiService.d.ts +8 -10
- package/dist/internal/sharingPublic/apiService.js +9 -125
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
- package/dist/internal/sharingPublic/{cryptoService.js → cryptoReporter.js} +3 -16
- package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
- package/dist/internal/sharingPublic/index.d.ts +22 -4
- package/dist/internal/sharingPublic/index.js +37 -12
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +18 -0
- package/dist/internal/sharingPublic/nodes.js +46 -0
- package/dist/internal/sharingPublic/nodes.js.map +1 -0
- package/dist/internal/sharingPublic/session/apiService.d.ts +7 -5
- package/dist/internal/sharingPublic/session/apiService.js +25 -4
- package/dist/internal/sharingPublic/session/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/session/interface.d.ts +17 -0
- package/dist/internal/sharingPublic/session/manager.d.ts +12 -4
- package/dist/internal/sharingPublic/session/manager.js +14 -4
- package/dist/internal/sharingPublic/session/manager.js.map +1 -1
- package/dist/internal/sharingPublic/session/session.d.ts +7 -4
- package/dist/internal/sharingPublic/session/session.js +7 -3
- package/dist/internal/sharingPublic/session/session.js.map +1 -1
- package/dist/internal/sharingPublic/session/url.test.js +3 -3
- package/dist/internal/sharingPublic/shares.d.ts +27 -0
- package/dist/internal/sharingPublic/shares.js +46 -0
- package/dist/internal/sharingPublic/shares.js.map +1 -0
- package/dist/internal/upload/apiService.js +10 -1
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/controller.d.ts +11 -3
- package/dist/internal/upload/controller.js +16 -2
- package/dist/internal/upload/controller.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +6 -3
- package/dist/internal/upload/fileUploader.js +4 -4
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +23 -11
- package/dist/internal/upload/fileUploader.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +9 -4
- package/dist/internal/upload/streamUploader.js +67 -20
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +43 -13
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +11 -6
- package/dist/protonDriveClient.js +11 -10
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +34 -6
- package/dist/protonDrivePublicLinkClient.js +52 -9
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/tests/telemetry.d.ts +4 -2
- package/dist/tests/telemetry.js +3 -1
- package/dist/tests/telemetry.js.map +1 -1
- package/dist/transformers.d.ts +3 -2
- package/dist/transformers.js +6 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/diagnostic/{sdkDiagnosticFull.ts → diagnostic.ts} +10 -6
- package/src/diagnostic/index.ts +3 -5
- package/src/diagnostic/interface.ts +39 -0
- package/src/diagnostic/sdkDiagnostic.ts +111 -10
- package/src/interface/download.ts +4 -4
- package/src/interface/index.ts +1 -0
- package/src/interface/nodes.ts +3 -0
- package/src/interface/telemetry.ts +5 -0
- package/src/interface/upload.ts +3 -3
- package/src/internal/apiService/apiService.test.ts +50 -0
- package/src/internal/apiService/apiService.ts +33 -2
- package/src/internal/apiService/driveTypes.ts +2713 -2561
- package/src/internal/apiService/errors.test.ts +10 -0
- package/src/internal/apiService/errors.ts +5 -1
- package/src/internal/asyncIteratorMap.test.ts +12 -0
- package/src/internal/asyncIteratorMap.ts +8 -0
- package/src/internal/download/controller.ts +13 -1
- package/src/internal/download/fileDownloader.test.ts +8 -8
- package/src/internal/download/fileDownloader.ts +13 -6
- package/src/internal/nodes/apiService.test.ts +261 -14
- package/src/internal/nodes/apiService.ts +99 -65
- package/src/internal/nodes/debouncer.test.ts +141 -0
- package/src/internal/nodes/debouncer.ts +109 -0
- package/src/internal/nodes/extendedAttributes.ts +2 -2
- package/src/internal/nodes/index.ts +1 -8
- package/src/internal/nodes/nodesAccess.test.ts +19 -7
- package/src/internal/nodes/nodesAccess.ts +44 -9
- package/src/internal/nodes/nodesManagement.test.ts +3 -1
- package/src/internal/nodes/nodesManagement.ts +11 -5
- package/src/internal/photos/apiService.ts +12 -29
- package/src/internal/photos/upload.ts +22 -1
- package/src/internal/sharing/apiService.ts +2 -2
- package/src/internal/sharing/sharingManagement.ts +7 -4
- package/src/internal/sharingPublic/apiService.ts +23 -160
- package/src/internal/sharingPublic/{cryptoService.ts → cryptoReporter.ts} +2 -27
- package/src/internal/sharingPublic/index.ts +76 -13
- package/src/internal/sharingPublic/nodes.ts +59 -0
- package/src/internal/sharingPublic/session/apiService.ts +32 -10
- package/src/internal/sharingPublic/session/interface.ts +20 -0
- package/src/internal/sharingPublic/session/manager.ts +31 -8
- package/src/internal/sharingPublic/session/session.ts +12 -7
- package/src/internal/sharingPublic/session/url.test.ts +3 -3
- package/src/internal/sharingPublic/shares.ts +50 -0
- package/src/internal/upload/apiService.ts +12 -1
- package/src/internal/upload/controller.ts +16 -4
- package/src/internal/upload/fileUploader.test.ts +25 -11
- package/src/internal/upload/fileUploader.ts +6 -5
- package/src/internal/upload/streamUploader.test.ts +56 -12
- package/src/internal/upload/streamUploader.ts +78 -20
- package/src/protonDriveClient.ts +29 -11
- package/src/protonDrivePublicLinkClient.ts +100 -16
- package/src/tests/telemetry.ts +6 -3
- package/src/transformers.ts +8 -0
- package/dist/diagnostic/sdkDiagnosticFull.js.map +0 -1
- package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -19
- package/dist/internal/sharingPublic/cryptoCache.js +0 -72
- package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
- package/dist/internal/sharingPublic/cryptoService.d.ts +0 -9
- package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
- package/dist/internal/sharingPublic/interface.d.ts +0 -6
- package/dist/internal/sharingPublic/interface.js +0 -3
- package/dist/internal/sharingPublic/interface.js.map +0 -1
- package/dist/internal/sharingPublic/manager.d.ts +0 -19
- package/dist/internal/sharingPublic/manager.js +0 -81
- package/dist/internal/sharingPublic/manager.js.map +0 -1
- package/src/internal/sharingPublic/cryptoCache.ts +0 -79
- package/src/internal/sharingPublic/interface.ts +0 -14
- package/src/internal/sharingPublic/manager.ts +0 -86
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
|
-
import { ProtonDriveError, ValidationError } from '../../errors';
|
|
3
|
+
import { NodeWithSameNameExistsValidationError, ProtonDriveError, ValidationError } from '../../errors';
|
|
4
4
|
import { Logger, NodeResult } from '../../interface';
|
|
5
5
|
import { MemberRole, RevisionState } from '../../interface/nodes';
|
|
6
6
|
import {
|
|
7
7
|
DriveAPIService,
|
|
8
8
|
drivePaths,
|
|
9
|
+
ErrorCode,
|
|
9
10
|
InvalidRequirementsAPIError,
|
|
10
11
|
isCodeOk,
|
|
11
12
|
nodeTypeNumberToNodeType,
|
|
@@ -128,7 +129,7 @@ export class NodeAPIService {
|
|
|
128
129
|
|
|
129
130
|
async *iterateNodes(
|
|
130
131
|
nodeUids: string[],
|
|
131
|
-
ownVolumeId: string,
|
|
132
|
+
ownVolumeId: string | undefined,
|
|
132
133
|
filterOptions?: FilterOptions,
|
|
133
134
|
signal?: AbortSignal,
|
|
134
135
|
): AsyncGenerator<EncryptedNode> {
|
|
@@ -389,55 +390,49 @@ export class NodeAPIService {
|
|
|
389
390
|
return makeNodeUid(volumeId, response.LinkID);
|
|
390
391
|
}
|
|
391
392
|
|
|
392
|
-
// Improvement requested: split into multiple calls for many nodes.
|
|
393
393
|
async *trashNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
signal,
|
|
403
|
-
);
|
|
394
|
+
for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
|
|
395
|
+
const response = await this.apiService.post<PostTrashNodesRequest, PostTrashNodesResponse>(
|
|
396
|
+
`drive/v2/volumes/${volumeId}/trash_multiple`,
|
|
397
|
+
{
|
|
398
|
+
LinkIDs: batchNodeIds,
|
|
399
|
+
},
|
|
400
|
+
signal,
|
|
401
|
+
);
|
|
404
402
|
|
|
405
|
-
|
|
406
|
-
|
|
403
|
+
// TODO: remove `as` when backend fixes OpenAPI schema.
|
|
404
|
+
yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
|
|
405
|
+
}
|
|
407
406
|
}
|
|
408
407
|
|
|
409
|
-
// Improvement requested: split into multiple calls for many nodes.
|
|
410
408
|
async *restoreNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
signal,
|
|
420
|
-
);
|
|
409
|
+
for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
|
|
410
|
+
const response = await this.apiService.put<PutRestoreNodesRequest, PutRestoreNodesResponse>(
|
|
411
|
+
`drive/v2/volumes/${volumeId}/trash/restore_multiple`,
|
|
412
|
+
{
|
|
413
|
+
LinkIDs: batchNodeIds,
|
|
414
|
+
},
|
|
415
|
+
signal,
|
|
416
|
+
);
|
|
421
417
|
|
|
422
|
-
|
|
423
|
-
|
|
418
|
+
// TODO: remove `as` when backend fixes OpenAPI schema.
|
|
419
|
+
yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
|
|
420
|
+
}
|
|
424
421
|
}
|
|
425
422
|
|
|
426
|
-
// Improvement requested: split into multiple calls for many nodes.
|
|
427
423
|
async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
signal,
|
|
437
|
-
);
|
|
424
|
+
for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
|
|
425
|
+
const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
|
|
426
|
+
`drive/v2/volumes/${volumeId}/trash/delete_multiple`,
|
|
427
|
+
{
|
|
428
|
+
LinkIDs: batchNodeIds,
|
|
429
|
+
},
|
|
430
|
+
signal,
|
|
431
|
+
);
|
|
438
432
|
|
|
439
|
-
|
|
440
|
-
|
|
433
|
+
// TODO: remove `as` when backend fixes OpenAPI schema.
|
|
434
|
+
yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
|
|
435
|
+
}
|
|
441
436
|
}
|
|
442
437
|
|
|
443
438
|
async createFolder(
|
|
@@ -455,21 +450,41 @@ export class NodeAPIService {
|
|
|
455
450
|
): Promise<string> {
|
|
456
451
|
const { volumeId, nodeId: parentId } = splitNodeUid(parentUid);
|
|
457
452
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
453
|
+
let response: PostCreateFolderResponse;
|
|
454
|
+
try {
|
|
455
|
+
response = await this.apiService.post<PostCreateFolderRequest, PostCreateFolderResponse>(
|
|
456
|
+
`drive/v2/volumes/${volumeId}/folders`,
|
|
457
|
+
{
|
|
458
|
+
ParentLinkID: parentId,
|
|
459
|
+
NodeKey: newNode.armoredKey,
|
|
460
|
+
NodeHashKey: newNode.armoredHashKey,
|
|
461
|
+
NodePassphrase: newNode.armoredNodePassphrase,
|
|
462
|
+
NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
|
|
463
|
+
SignatureEmail: newNode.signatureEmail,
|
|
464
|
+
Name: newNode.encryptedName,
|
|
465
|
+
Hash: newNode.hash,
|
|
466
|
+
// @ts-expect-error: XAttr is optional as undefined.
|
|
467
|
+
XAttr: newNode.armoredExtendedAttributes,
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
} catch (error: unknown) {
|
|
471
|
+
if (error instanceof ValidationError) {
|
|
472
|
+
if (error.code === ErrorCode.ALREADY_EXISTS) {
|
|
473
|
+
const typedDetails = error.details as
|
|
474
|
+
| {
|
|
475
|
+
ConflictLinkID: string;
|
|
476
|
+
}
|
|
477
|
+
| undefined;
|
|
478
|
+
|
|
479
|
+
const existingNodeUid = typedDetails?.ConflictLinkID
|
|
480
|
+
? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
|
|
481
|
+
: undefined;
|
|
482
|
+
|
|
483
|
+
throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
473
488
|
|
|
474
489
|
return makeNodeUid(volumeId, response.Folder.ID);
|
|
475
490
|
}
|
|
@@ -513,15 +528,6 @@ export class NodeAPIService {
|
|
|
513
528
|
}
|
|
514
529
|
}
|
|
515
530
|
|
|
516
|
-
function assertAndGetSingleVolumeId(operationForErrorMessage: string, nodeIds: { volumeId: string }[]): string {
|
|
517
|
-
const uniqueVolumeIds = new Set(nodeIds.map(({ volumeId }) => volumeId));
|
|
518
|
-
if (uniqueVolumeIds.size !== 1) {
|
|
519
|
-
throw new ValidationError(c('Error').t`${operationForErrorMessage} from multiple sections is not allowed`);
|
|
520
|
-
}
|
|
521
|
-
const volumeId = nodeIds[0].volumeId;
|
|
522
|
-
return volumeId;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
531
|
type LinkResponse = {
|
|
526
532
|
LinkID: string;
|
|
527
533
|
Response: {
|
|
@@ -657,6 +663,34 @@ function linkToEncryptedNode(
|
|
|
657
663
|
throw new Error(`Unknown node type: ${link.Link.Type}`);
|
|
658
664
|
}
|
|
659
665
|
|
|
666
|
+
export function* groupNodeUidsByVolumeAndIteratePerBatch(
|
|
667
|
+
nodeUids: string[],
|
|
668
|
+
): Generator<{ volumeId: string; batchNodeIds: string[]; batchNodeUids: string[] }> {
|
|
669
|
+
const allNodeIds = nodeUids.map((nodeUid: string) => {
|
|
670
|
+
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
671
|
+
return { volumeId, nodeIds: { nodeId, nodeUid } };
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const nodeIdsByVolumeId = new Map<string, { nodeId: string; nodeUid: string }[]>();
|
|
675
|
+
for (const { volumeId, nodeIds } of allNodeIds) {
|
|
676
|
+
if (!nodeIdsByVolumeId.has(volumeId)) {
|
|
677
|
+
nodeIdsByVolumeId.set(volumeId, []);
|
|
678
|
+
}
|
|
679
|
+
nodeIdsByVolumeId.get(volumeId)?.push(nodeIds);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for (const [volumeId, nodeIds] of nodeIdsByVolumeId.entries()) {
|
|
683
|
+
for (const nodeIdsBatch of batch(nodeIds, API_NODES_BATCH_SIZE)) {
|
|
684
|
+
yield {
|
|
685
|
+
volumeId,
|
|
686
|
+
batchNodeIds: nodeIdsBatch.map(({ nodeId }) => nodeId),
|
|
687
|
+
batchNodeUids: nodeIdsBatch.map(({ nodeUid }) => nodeUid),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
|
|
660
694
|
function transformRevisionResponse(
|
|
661
695
|
volumeId: string,
|
|
662
696
|
nodeId: string,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ProtonDriveTelemetry } from '../../interface';
|
|
2
|
+
import { getMockTelemetry } from '../../tests/telemetry';
|
|
3
|
+
import { NodesDebouncer } from './debouncer';
|
|
4
|
+
|
|
5
|
+
describe('NodesDebouncer', () => {
|
|
6
|
+
let debouncer: NodesDebouncer;
|
|
7
|
+
let mockTelemetry: ReturnType<typeof getMockTelemetry>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockTelemetry = getMockTelemetry();
|
|
11
|
+
debouncer = new NodesDebouncer(mockTelemetry);
|
|
12
|
+
|
|
13
|
+
jest.useFakeTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
jest.useRealTimers();
|
|
18
|
+
debouncer.clear();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should register a node for loading and wait for it to finish', async () => {
|
|
22
|
+
const nodeUid = 'test-node-1';
|
|
23
|
+
debouncer.loadingNode(nodeUid);
|
|
24
|
+
|
|
25
|
+
// Verify that the node is registered by checking if waitForLoadingNode works
|
|
26
|
+
const waitPromise = debouncer.waitForLoadingNode(nodeUid);
|
|
27
|
+
expect(waitPromise).toBeInstanceOf(Promise);
|
|
28
|
+
|
|
29
|
+
// Finish loading to clean up
|
|
30
|
+
debouncer.finishedLoadingNode(nodeUid);
|
|
31
|
+
await waitPromise;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should allow multiple nodes to be registered', async () => {
|
|
35
|
+
const nodeUid1 = 'test-node-1';
|
|
36
|
+
const nodeUid2 = 'test-node-2';
|
|
37
|
+
|
|
38
|
+
debouncer.loadingNode(nodeUid1);
|
|
39
|
+
debouncer.loadingNode(nodeUid2);
|
|
40
|
+
|
|
41
|
+
const wait1 = debouncer.waitForLoadingNode(nodeUid1);
|
|
42
|
+
const wait2 = debouncer.waitForLoadingNode(nodeUid2);
|
|
43
|
+
|
|
44
|
+
expect(wait1).toBeInstanceOf(Promise);
|
|
45
|
+
expect(wait2).toBeInstanceOf(Promise);
|
|
46
|
+
|
|
47
|
+
debouncer.finishedLoadingNode(nodeUid1);
|
|
48
|
+
debouncer.finishedLoadingNode(nodeUid2);
|
|
49
|
+
await Promise.all([wait1, wait2]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should register multiple nodes at once', async () => {
|
|
53
|
+
const nodeUid1 = 'test-node-1';
|
|
54
|
+
const nodeUid2 = 'test-node-2';
|
|
55
|
+
|
|
56
|
+
debouncer.loadingNodes([nodeUid1, nodeUid2]);
|
|
57
|
+
|
|
58
|
+
const wait1 = debouncer.waitForLoadingNode(nodeUid1);
|
|
59
|
+
const wait2 = debouncer.waitForLoadingNode(nodeUid2);
|
|
60
|
+
|
|
61
|
+
expect(wait1).toBeInstanceOf(Promise);
|
|
62
|
+
expect(wait2).toBeInstanceOf(Promise);
|
|
63
|
+
|
|
64
|
+
debouncer.finishedLoadingNode(nodeUid1);
|
|
65
|
+
debouncer.finishedLoadingNode(nodeUid2);
|
|
66
|
+
await Promise.all([wait1, wait2]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should warn about registering the same node twice', async () => {
|
|
70
|
+
const nodeUid = 'test-node-1';
|
|
71
|
+
|
|
72
|
+
// Register the same node twice
|
|
73
|
+
debouncer.loadingNode(nodeUid);
|
|
74
|
+
debouncer.loadingNode(nodeUid);
|
|
75
|
+
|
|
76
|
+
expect(mockTelemetry.mockLogger.warn).toHaveBeenCalledWith(`Loading twice for: ${nodeUid}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should send metric when waiting for a long time', async () => {
|
|
80
|
+
const nodeUid = 'test-node-1';
|
|
81
|
+
debouncer.loadingNode(nodeUid);
|
|
82
|
+
|
|
83
|
+
const waitPromise = debouncer.waitForLoadingNode(nodeUid);
|
|
84
|
+
expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
|
|
85
|
+
jest.advanceTimersByTime(1500);
|
|
86
|
+
expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
|
|
87
|
+
eventName: 'debounceLongWait',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
debouncer.finishedLoadingNode(nodeUid);
|
|
91
|
+
await waitPromise;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should timeout', async () => {
|
|
95
|
+
const nodeUid = 'test-node-1';
|
|
96
|
+
debouncer.loadingNode(nodeUid);
|
|
97
|
+
|
|
98
|
+
jest.advanceTimersByTime(6000);
|
|
99
|
+
expect(mockTelemetry.mockLogger.warn).toHaveBeenCalledWith(`Timeout for: ${nodeUid}`);
|
|
100
|
+
await expect(debouncer.waitForLoadingNode(nodeUid)).resolves.toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('finishedLoadingNode', () => {
|
|
104
|
+
it('should handle non-existent node gracefully', async () => {
|
|
105
|
+
const nodeUid = 'non-existent-node';
|
|
106
|
+
|
|
107
|
+
expect(() => debouncer.finishedLoadingNode(nodeUid)).not.toThrow();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should remove node from internal map after finishing', async () => {
|
|
111
|
+
const nodeUid = 'test-node-1';
|
|
112
|
+
debouncer.loadingNode(nodeUid);
|
|
113
|
+
debouncer.finishedLoadingNode(nodeUid);
|
|
114
|
+
|
|
115
|
+
const waitPromise = debouncer.waitForLoadingNode(nodeUid);
|
|
116
|
+
await expect(waitPromise).resolves.toBe(undefined);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('waitForLoadingNode', () => {
|
|
121
|
+
it('should return immediately for non-registered node', async () => {
|
|
122
|
+
const nodeUid = 'non-existent-node';
|
|
123
|
+
|
|
124
|
+
const result = await debouncer.waitForLoadingNode(nodeUid);
|
|
125
|
+
expect(result).toBeUndefined();
|
|
126
|
+
expect(mockTelemetry.mockLogger.debug).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should wait for registered node and log debug message', async () => {
|
|
130
|
+
const nodeUid = 'test-node-1';
|
|
131
|
+
debouncer.loadingNode(nodeUid);
|
|
132
|
+
|
|
133
|
+
const waitPromise = debouncer.waitForLoadingNode(nodeUid);
|
|
134
|
+
|
|
135
|
+
expect(mockTelemetry.mockLogger.debug).toHaveBeenCalledWith(`Wait for: ${nodeUid}`);
|
|
136
|
+
|
|
137
|
+
debouncer.finishedLoadingNode(nodeUid);
|
|
138
|
+
await waitPromise;
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Logger, ProtonDriveTelemetry } from '../../interface';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The timeout for which the node is considered to be loading.
|
|
5
|
+
* If the node is not loaded after this timeout, it is considered to be
|
|
6
|
+
* loaded or failed to be loaded, and allowed other places to proceed.
|
|
7
|
+
*
|
|
8
|
+
* Decrypting many nodes in parallel can take a lot of time, so we allow
|
|
9
|
+
* more time for this.
|
|
10
|
+
*/
|
|
11
|
+
const DEBOUNCE_TIMEOUT = 5000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The timeout for which the node is considered to be waiting for a long time.
|
|
15
|
+
* After this timeout the metric is sent.
|
|
16
|
+
*/
|
|
17
|
+
const DEBOUNCE_LONG_WAIT_TIMEOUT = 1000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper to avoid loading the same node twice.
|
|
21
|
+
*
|
|
22
|
+
* Each place that loads a node should report it is being loaded,
|
|
23
|
+
* and when it is finished, it should report it is finished.
|
|
24
|
+
* The finish must be called even if the node fails to be loaded
|
|
25
|
+
* to clear the promise.
|
|
26
|
+
*
|
|
27
|
+
* Each place that loads a node from cache should first wait for
|
|
28
|
+
* the node to be loaded if that is the case.
|
|
29
|
+
*/
|
|
30
|
+
export class NodesDebouncer {
|
|
31
|
+
private logger: Logger;
|
|
32
|
+
|
|
33
|
+
private promises: Map<
|
|
34
|
+
string,
|
|
35
|
+
{
|
|
36
|
+
promise: Promise<void>;
|
|
37
|
+
resolve: () => void;
|
|
38
|
+
timeout: NodeJS.Timeout;
|
|
39
|
+
}
|
|
40
|
+
> = new Map();
|
|
41
|
+
|
|
42
|
+
constructor(private telemetry: ProtonDriveTelemetry) {
|
|
43
|
+
this.logger = telemetry.getLogger('nodes-debouncer');
|
|
44
|
+
this.telemetry = telemetry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loadingNodes(nodeUids: string[]) {
|
|
48
|
+
for (const nodeUid of nodeUids) {
|
|
49
|
+
this.loadingNode(nodeUid);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
loadingNode(nodeUid: string) {
|
|
54
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
55
|
+
if (this.promises.has(nodeUid)) {
|
|
56
|
+
this.logger.warn(`Loading twice for: ${nodeUid}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const timeout = setTimeout(() => {
|
|
61
|
+
this.logger.warn(`Timeout for: ${nodeUid}`);
|
|
62
|
+
this.finishedLoadingNode(nodeUid);
|
|
63
|
+
}, DEBOUNCE_TIMEOUT);
|
|
64
|
+
this.promises.set(nodeUid, { promise, resolve, timeout });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
finishedLoadingNodes(nodeUids: string[]) {
|
|
68
|
+
for (const nodeUid of nodeUids) {
|
|
69
|
+
this.finishedLoadingNode(nodeUid);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
finishedLoadingNode(nodeUid: string) {
|
|
74
|
+
const result = this.promises.get(nodeUid);
|
|
75
|
+
if (!result) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clearTimeout(result.timeout);
|
|
80
|
+
result.resolve();
|
|
81
|
+
this.promises.delete(nodeUid);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async waitForLoadingNode(nodeUid: string) {
|
|
85
|
+
const result = this.promises.get(nodeUid);
|
|
86
|
+
if (!result) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const metricTimeout = setTimeout(() => {
|
|
91
|
+
this.telemetry.recordMetric({
|
|
92
|
+
eventName: 'debounceLongWait',
|
|
93
|
+
});
|
|
94
|
+
}, DEBOUNCE_LONG_WAIT_TIMEOUT);
|
|
95
|
+
|
|
96
|
+
this.logger.debug(`Wait for: ${nodeUid}`);
|
|
97
|
+
await result.promise;
|
|
98
|
+
|
|
99
|
+
clearTimeout(metricTimeout);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
clear() {
|
|
103
|
+
for (const result of this.promises.values()) {
|
|
104
|
+
clearTimeout(result.timeout);
|
|
105
|
+
result.resolve();
|
|
106
|
+
}
|
|
107
|
+
this.promises.clear();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -208,11 +208,11 @@ function parseBlockSizes(
|
|
|
208
208
|
return undefined;
|
|
209
209
|
}
|
|
210
210
|
if (!Array.isArray(blockSizes)) {
|
|
211
|
-
logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
|
|
211
|
+
logger.warn(`XAttr block sizes "${JSON.stringify(blockSizes)}" is not valid`);
|
|
212
212
|
return undefined;
|
|
213
213
|
}
|
|
214
214
|
if (blockSizes.some((size) => typeof size !== 'number' || size <= 0)) {
|
|
215
|
-
logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
|
|
215
|
+
logger.warn(`XAttr block sizes "${JSON.stringify(blockSizes)}" is not valid`);
|
|
216
216
|
return undefined;
|
|
217
217
|
}
|
|
218
218
|
if (blockSizes.length === 0) {
|
|
@@ -43,14 +43,7 @@ export function initNodesModule(
|
|
|
43
43
|
const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
|
|
44
44
|
const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
|
|
45
45
|
const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
|
|
46
|
-
const nodesAccess = new NodesAccess(
|
|
47
|
-
telemetry.getLogger('nodes'),
|
|
48
|
-
api,
|
|
49
|
-
cache,
|
|
50
|
-
cryptoCache,
|
|
51
|
-
cryptoService,
|
|
52
|
-
sharesService,
|
|
53
|
-
);
|
|
46
|
+
const nodesAccess = new NodesAccess(telemetry, api, cache, cryptoCache, cryptoService, sharesService);
|
|
54
47
|
const nodesEventHandler = new NodesEventsHandler(telemetry.getLogger('nodes-events'), cache);
|
|
55
48
|
const nodesManagement = new NodesManagement(api, cryptoCache, cryptoService, nodesAccess);
|
|
56
49
|
const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getMockTelemetry } from '../../tests/telemetry';
|
|
2
2
|
import { PrivateKey } from '../../crypto';
|
|
3
3
|
import { DecryptionError } from '../../errors';
|
|
4
4
|
import { NodeAPIService } from './apiService';
|
|
@@ -50,7 +50,7 @@ describe('nodesAccess', () => {
|
|
|
50
50
|
getSharePrivateKey: jest.fn(),
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
access = new NodesAccess(
|
|
53
|
+
access = new NodesAccess(getMockTelemetry(), apiService, cache, cryptoCache, cryptoService, shareService);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
describe('getNode', () => {
|
|
@@ -352,7 +352,7 @@ describe('nodesAccess', () => {
|
|
|
352
352
|
expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
it
|
|
355
|
+
it('should return only filtered nodes from API', async () => {
|
|
356
356
|
cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(false);
|
|
357
357
|
cache.getNode = jest.fn().mockImplementation((uid: string) => {
|
|
358
358
|
if (uid === parentNode.uid) {
|
|
@@ -444,7 +444,7 @@ describe('nodesAccess', () => {
|
|
|
444
444
|
const node1 = { uid: 'volumeId~node1', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
445
445
|
const node2 = { uid: 'volumeId~node2', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
446
446
|
const node3 = { uid: 'volumeId~node3', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
447
|
-
const node4 = { uid: '
|
|
447
|
+
const node4 = { uid: 'volumeId~node4', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
448
448
|
|
|
449
449
|
it('should serve fully from cache', async () => {
|
|
450
450
|
cache.iterateNodes = jest.fn().mockImplementation(async function* () {
|
|
@@ -569,7 +569,11 @@ describe('nodesAccess', () => {
|
|
|
569
569
|
it('should get share parent keys', async () => {
|
|
570
570
|
shareService.getSharePrivateKey = jest.fn(() => Promise.resolve('shareKey' as any as PrivateKey));
|
|
571
571
|
|
|
572
|
-
const result = await access.getParentKeys({
|
|
572
|
+
const result = await access.getParentKeys({
|
|
573
|
+
uid: 'volumeId~nodeId',
|
|
574
|
+
shareId: 'shareId',
|
|
575
|
+
parentUid: undefined,
|
|
576
|
+
});
|
|
573
577
|
expect(result).toEqual({ key: 'shareKey' });
|
|
574
578
|
expect(cryptoCache.getNodeKeys).not.toHaveBeenCalled();
|
|
575
579
|
});
|
|
@@ -577,7 +581,11 @@ describe('nodesAccess', () => {
|
|
|
577
581
|
it('should get node parent keys', async () => {
|
|
578
582
|
cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
|
|
579
583
|
|
|
580
|
-
const result = await access.getParentKeys({
|
|
584
|
+
const result = await access.getParentKeys({
|
|
585
|
+
uid: 'volumeId~nodeId',
|
|
586
|
+
shareId: undefined,
|
|
587
|
+
parentUid: 'volumeId~parentNodeid',
|
|
588
|
+
});
|
|
581
589
|
expect(result).toEqual({ key: 'parentKey' });
|
|
582
590
|
expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
|
|
583
591
|
});
|
|
@@ -585,7 +593,11 @@ describe('nodesAccess', () => {
|
|
|
585
593
|
it('should get node parent keys even if share is set', async () => {
|
|
586
594
|
cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
|
|
587
595
|
|
|
588
|
-
const result = await access.getParentKeys({
|
|
596
|
+
const result = await access.getParentKeys({
|
|
597
|
+
uid: 'volume1~nodeId',
|
|
598
|
+
shareId: 'shareId',
|
|
599
|
+
parentUid: 'volume1~parentNodeid',
|
|
600
|
+
});
|
|
589
601
|
expect(result).toEqual({ key: 'parentKey' });
|
|
590
602
|
expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
|
|
591
603
|
});
|