@protontech/drive-sdk 0.5.0 → 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 +79 -7
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- 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/internal/apiService/driveTypes.d.ts +2571 -2356
- 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.js +6 -1
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/nodes/apiService.js +27 -12
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +60 -2
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/debouncer.d.ts +3 -2
- package/dist/internal/nodes/debouncer.js +16 -4
- package/dist/internal/nodes/debouncer.js.map +1 -1
- package/dist/internal/nodes/debouncer.test.js +20 -12
- package/dist/internal/nodes/debouncer.test.js.map +1 -1
- 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 +5 -4
- package/dist/internal/nodes/nodesAccess.js +6 -5
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +17 -5
- 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.js +7 -1
- 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 -63
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +3 -3
- package/dist/internal/sharingPublic/index.js +5 -11
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +13 -8
- package/dist/internal/sharingPublic/nodes.js +20 -2
- package/dist/internal/sharingPublic/nodes.js.map +1 -1
- 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 +5 -2
- package/dist/internal/sharingPublic/session/session.js +7 -3
- package/dist/internal/sharingPublic/session/session.js.map +1 -1
- package/dist/internal/sharingPublic/shares.d.ts +3 -10
- package/dist/internal/sharingPublic/shares.js +10 -33
- package/dist/internal/sharingPublic/shares.js.map +1 -1
- package/dist/internal/upload/controller.d.ts +3 -1
- package/dist/internal/upload/controller.js +16 -2
- package/dist/internal/upload/controller.js.map +1 -1
- package/dist/internal/upload/fileUploader.js +2 -2
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +4 -3
- package/dist/internal/upload/streamUploader.js +61 -18
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +38 -12
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +8 -3
- package/dist/protonDriveClient.js +7 -6
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +4 -3
- package/dist/protonDrivePublicLinkClient.js +2 -2
- 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 +110 -9
- package/src/interface/index.ts +1 -0
- package/src/interface/nodes.ts +3 -0
- package/src/interface/telemetry.ts +5 -0
- package/src/internal/apiService/driveTypes.ts +2698 -2529
- package/src/internal/download/controller.ts +13 -1
- package/src/internal/download/fileDownloader.ts +8 -1
- package/src/internal/nodes/apiService.test.ts +64 -0
- package/src/internal/nodes/apiService.ts +38 -17
- package/src/internal/nodes/debouncer.test.ts +25 -13
- package/src/internal/nodes/debouncer.ts +20 -4
- package/src/internal/nodes/extendedAttributes.ts +2 -2
- package/src/internal/nodes/index.ts +1 -8
- package/src/internal/nodes/nodesAccess.test.ts +17 -5
- package/src/internal/nodes/nodesAccess.ts +15 -5
- 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 +19 -1
- package/src/internal/sharing/apiService.ts +2 -2
- package/src/internal/sharing/sharingManagement.ts +7 -4
- package/src/internal/sharingPublic/apiService.ts +23 -77
- package/src/internal/sharingPublic/index.ts +11 -10
- package/src/internal/sharingPublic/nodes.ts +33 -11
- package/src/internal/sharingPublic/session/apiService.ts +31 -9
- 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 +10 -5
- package/src/internal/sharingPublic/shares.ts +7 -43
- package/src/internal/upload/controller.ts +16 -4
- package/src/internal/upload/fileUploader.ts +2 -2
- package/src/internal/upload/streamUploader.test.ts +46 -14
- package/src/internal/upload/streamUploader.ts +74 -21
- package/src/protonDriveClient.ts +25 -7
- package/src/protonDrivePublicLinkClient.ts +7 -4
- 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 -15
- package/dist/internal/sharingPublic/cryptoCache.js +0 -44
- package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
- package/dist/internal/sharingPublic/cryptoService.d.ts +0 -8
- package/dist/internal/sharingPublic/cryptoService.js +0 -19
- package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
- package/dist/internal/sharingPublic/interface.d.ts +0 -5
- package/dist/internal/sharingPublic/interface.js +0 -3
- package/dist/internal/sharingPublic/interface.js.map +0 -1
- package/src/internal/sharingPublic/cryptoCache.ts +0 -45
- package/src/internal/sharingPublic/cryptoService.ts +0 -22
- package/src/internal/sharingPublic/interface.ts +0 -5
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
import { AbortError } from '../../errors';
|
|
1
2
|
import { waitForCondition } from '../wait';
|
|
2
3
|
|
|
3
4
|
export class DownloadController {
|
|
4
5
|
private paused = false;
|
|
5
6
|
public promise?: Promise<void>;
|
|
6
7
|
|
|
8
|
+
constructor(private signal?: AbortSignal) {
|
|
9
|
+
this.signal = signal;
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
async waitWhilePaused(): Promise<void> {
|
|
8
|
-
|
|
13
|
+
try {
|
|
14
|
+
await waitForCondition(() => !this.paused, this.signal);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof AbortError) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
9
21
|
}
|
|
10
22
|
|
|
11
23
|
pause(): void {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { c } from 'ttag';
|
|
2
|
+
|
|
1
3
|
import { PrivateKey, SessionKey, base64StringToUint8Array } from '../../crypto';
|
|
4
|
+
import { AbortError } from '../../errors';
|
|
2
5
|
import { Logger } from '../../interface';
|
|
3
6
|
import { LoggerWithPrefix } from '../../telemetry';
|
|
4
7
|
import { APIHTTPError, HTTPErrorCode } from '../apiService';
|
|
@@ -48,7 +51,7 @@ export class FileDownloader {
|
|
|
48
51
|
this.revision = revision;
|
|
49
52
|
this.signal = signal;
|
|
50
53
|
this.onFinish = onFinish;
|
|
51
|
-
this.controller = new DownloadController();
|
|
54
|
+
this.controller = new DownloadController(this.signal);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
getClaimedSizeInBytes(): number | undefined {
|
|
@@ -289,6 +292,10 @@ export class FileDownloader {
|
|
|
289
292
|
logger.debug(`Decrypting`);
|
|
290
293
|
decryptedBlock = await this.cryptoService.decryptBlock(encryptedBlock, cryptoKeys);
|
|
291
294
|
} catch (error) {
|
|
295
|
+
if (this.signal?.aborted) {
|
|
296
|
+
throw new AbortError(c('Error').t`Operation aborted`);
|
|
297
|
+
}
|
|
298
|
+
|
|
292
299
|
if (blockProgress !== 0) {
|
|
293
300
|
onProgress?.(-blockProgress);
|
|
294
301
|
blockProgress = 0;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NodeWithSameNameExistsValidationError, ValidationError } from '../../errors';
|
|
1
2
|
import { MemberRole, NodeType } from '../../interface';
|
|
2
3
|
import { getMockLogger } from '../../tests/logger';
|
|
3
4
|
import { DriveAPIService, ErrorCode, InvalidRequirementsAPIError } from '../apiService';
|
|
@@ -627,6 +628,69 @@ describe('nodeAPIService', () => {
|
|
|
627
628
|
});
|
|
628
629
|
});
|
|
629
630
|
|
|
631
|
+
describe('createFolder', () => {
|
|
632
|
+
it('should create folder', async () => {
|
|
633
|
+
apiMock.post = jest.fn().mockResolvedValue({
|
|
634
|
+
Code: ErrorCode.OK,
|
|
635
|
+
Folder: {
|
|
636
|
+
ID: 'newNodeId',
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const result = await api.createFolder('volumeId~parentNodeId', {
|
|
641
|
+
armoredKey: 'armoredKey',
|
|
642
|
+
armoredHashKey: 'armoredHashKey',
|
|
643
|
+
armoredNodePassphrase: 'armoredNodePassphrase',
|
|
644
|
+
armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
|
|
645
|
+
signatureEmail: 'signatureEmail',
|
|
646
|
+
encryptedName: 'encryptedName',
|
|
647
|
+
hash: 'hash',
|
|
648
|
+
armoredExtendedAttributes: 'armoredExtendedAttributes',
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
expect(result).toEqual('volumeId~newNodeId');
|
|
652
|
+
expect(apiMock.post).toHaveBeenCalledWith('drive/v2/volumes/volumeId/folders', {
|
|
653
|
+
ParentLinkID: 'parentNodeId',
|
|
654
|
+
NodeKey: 'armoredKey',
|
|
655
|
+
NodeHashKey: 'armoredHashKey',
|
|
656
|
+
NodePassphrase: 'armoredNodePassphrase',
|
|
657
|
+
NodePassphraseSignature: 'armoredNodePassphraseSignature',
|
|
658
|
+
SignatureEmail: 'signatureEmail',
|
|
659
|
+
Name: 'encryptedName',
|
|
660
|
+
Hash: 'hash',
|
|
661
|
+
XAttr: 'armoredExtendedAttributes',
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should throw NodeWithSameNameExistsValidationError if node already exists', async () => {
|
|
666
|
+
apiMock.post = jest.fn().mockRejectedValue(
|
|
667
|
+
new ValidationError('Node already exists', ErrorCode.ALREADY_EXISTS, {
|
|
668
|
+
ConflictLinkID: 'existingNodeId',
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
await api.createFolder('volumeId~parentNodeId', {
|
|
674
|
+
armoredKey: 'armoredKey',
|
|
675
|
+
armoredHashKey: 'armoredHashKey',
|
|
676
|
+
armoredNodePassphrase: 'armoredNodePassphrase',
|
|
677
|
+
armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
|
|
678
|
+
signatureEmail: 'signatureEmail',
|
|
679
|
+
encryptedName: 'encryptedName',
|
|
680
|
+
hash: 'hash',
|
|
681
|
+
armoredExtendedAttributes: 'armoredExtendedAttributes',
|
|
682
|
+
});
|
|
683
|
+
expect(false).toBeTruthy();
|
|
684
|
+
} catch (error: unknown) {
|
|
685
|
+
expect(error).toBeInstanceOf(NodeWithSameNameExistsValidationError);
|
|
686
|
+
if (error instanceof NodeWithSameNameExistsValidationError) {
|
|
687
|
+
expect(error.code).toEqual(ErrorCode.ALREADY_EXISTS);
|
|
688
|
+
expect(error.existingNodeUid).toEqual('volumeId~existingNodeId');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
630
694
|
describe('renameNode', () => {
|
|
631
695
|
it('should rename node', async () => {
|
|
632
696
|
await api.renameNode(
|
|
@@ -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,
|
|
@@ -400,7 +401,7 @@ export class NodeAPIService {
|
|
|
400
401
|
);
|
|
401
402
|
|
|
402
403
|
// TODO: remove `as` when backend fixes OpenAPI schema.
|
|
403
|
-
yield
|
|
404
|
+
yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
|
|
404
405
|
}
|
|
405
406
|
}
|
|
406
407
|
|
|
@@ -449,21 +450,41 @@ export class NodeAPIService {
|
|
|
449
450
|
): Promise<string> {
|
|
450
451
|
const { volumeId, nodeId: parentId } = splitNodeUid(parentUid);
|
|
451
452
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
}
|
|
467
488
|
|
|
468
489
|
return makeNodeUid(volumeId, response.Folder.ID);
|
|
469
490
|
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
+
import { ProtonDriveTelemetry } from '../../interface';
|
|
2
|
+
import { getMockTelemetry } from '../../tests/telemetry';
|
|
1
3
|
import { NodesDebouncer } from './debouncer';
|
|
2
|
-
import { Logger } from '../../interface';
|
|
3
4
|
|
|
4
5
|
describe('NodesDebouncer', () => {
|
|
5
6
|
let debouncer: NodesDebouncer;
|
|
6
|
-
let
|
|
7
|
+
let mockTelemetry: ReturnType<typeof getMockTelemetry>;
|
|
7
8
|
|
|
8
9
|
beforeEach(() => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
info: jest.fn(),
|
|
12
|
-
warn: jest.fn(),
|
|
13
|
-
error: jest.fn(),
|
|
14
|
-
};
|
|
15
|
-
debouncer = new NodesDebouncer(mockLogger);
|
|
10
|
+
mockTelemetry = getMockTelemetry();
|
|
11
|
+
debouncer = new NodesDebouncer(mockTelemetry);
|
|
16
12
|
|
|
17
13
|
jest.useFakeTimers();
|
|
18
14
|
});
|
|
@@ -77,7 +73,22 @@ describe('NodesDebouncer', () => {
|
|
|
77
73
|
debouncer.loadingNode(nodeUid);
|
|
78
74
|
debouncer.loadingNode(nodeUid);
|
|
79
75
|
|
|
80
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(`
|
|
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;
|
|
81
92
|
});
|
|
82
93
|
|
|
83
94
|
it('should timeout', async () => {
|
|
@@ -85,7 +96,7 @@ describe('NodesDebouncer', () => {
|
|
|
85
96
|
debouncer.loadingNode(nodeUid);
|
|
86
97
|
|
|
87
98
|
jest.advanceTimersByTime(6000);
|
|
88
|
-
expect(mockLogger.warn).toHaveBeenCalledWith(`
|
|
99
|
+
expect(mockTelemetry.mockLogger.warn).toHaveBeenCalledWith(`Timeout for: ${nodeUid}`);
|
|
89
100
|
await expect(debouncer.waitForLoadingNode(nodeUid)).resolves.toBeUndefined();
|
|
90
101
|
});
|
|
91
102
|
|
|
@@ -112,7 +123,7 @@ describe('NodesDebouncer', () => {
|
|
|
112
123
|
|
|
113
124
|
const result = await debouncer.waitForLoadingNode(nodeUid);
|
|
114
125
|
expect(result).toBeUndefined();
|
|
115
|
-
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
126
|
+
expect(mockTelemetry.mockLogger.debug).not.toHaveBeenCalled();
|
|
116
127
|
});
|
|
117
128
|
|
|
118
129
|
it('should wait for registered node and log debug message', async () => {
|
|
@@ -121,7 +132,8 @@ describe('NodesDebouncer', () => {
|
|
|
121
132
|
|
|
122
133
|
const waitPromise = debouncer.waitForLoadingNode(nodeUid);
|
|
123
134
|
|
|
124
|
-
expect(mockLogger.debug).toHaveBeenCalledWith(`
|
|
135
|
+
expect(mockTelemetry.mockLogger.debug).toHaveBeenCalledWith(`Wait for: ${nodeUid}`);
|
|
136
|
+
|
|
125
137
|
debouncer.finishedLoadingNode(nodeUid);
|
|
126
138
|
await waitPromise;
|
|
127
139
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Logger } from
|
|
2
|
-
import { LoggerWithPrefix } from '../../telemetry';
|
|
1
|
+
import { Logger, ProtonDriveTelemetry } from '../../interface';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* The timeout for which the node is considered to be loading.
|
|
@@ -11,6 +10,12 @@ import { LoggerWithPrefix } from '../../telemetry';
|
|
|
11
10
|
*/
|
|
12
11
|
const DEBOUNCE_TIMEOUT = 5000;
|
|
13
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
|
+
|
|
14
19
|
/**
|
|
15
20
|
* Helper to avoid loading the same node twice.
|
|
16
21
|
*
|
|
@@ -23,6 +28,8 @@ const DEBOUNCE_TIMEOUT = 5000;
|
|
|
23
28
|
* the node to be loaded if that is the case.
|
|
24
29
|
*/
|
|
25
30
|
export class NodesDebouncer {
|
|
31
|
+
private logger: Logger;
|
|
32
|
+
|
|
26
33
|
private promises: Map<
|
|
27
34
|
string,
|
|
28
35
|
{
|
|
@@ -32,8 +39,9 @@ export class NodesDebouncer {
|
|
|
32
39
|
}
|
|
33
40
|
> = new Map();
|
|
34
41
|
|
|
35
|
-
constructor(private
|
|
36
|
-
this.logger =
|
|
42
|
+
constructor(private telemetry: ProtonDriveTelemetry) {
|
|
43
|
+
this.logger = telemetry.getLogger('nodes-debouncer');
|
|
44
|
+
this.telemetry = telemetry;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
loadingNodes(nodeUids: string[]) {
|
|
@@ -79,8 +87,16 @@ export class NodesDebouncer {
|
|
|
79
87
|
return;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
const metricTimeout = setTimeout(() => {
|
|
91
|
+
this.telemetry.recordMetric({
|
|
92
|
+
eventName: 'debounceLongWait',
|
|
93
|
+
});
|
|
94
|
+
}, DEBOUNCE_LONG_WAIT_TIMEOUT);
|
|
95
|
+
|
|
82
96
|
this.logger.debug(`Wait for: ${nodeUid}`);
|
|
83
97
|
await result.promise;
|
|
98
|
+
|
|
99
|
+
clearTimeout(metricTimeout);
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
clear() {
|
|
@@ -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', () => {
|
|
@@ -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
|
});
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { PrivateKey, SessionKey } from '../../crypto';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
InvalidNameError,
|
|
6
|
+
Logger,
|
|
7
|
+
MissingNode,
|
|
8
|
+
NodeType,
|
|
9
|
+
ProtonDriveTelemetry,
|
|
10
|
+
Result,
|
|
11
|
+
resultError,
|
|
12
|
+
resultOk,
|
|
13
|
+
} from '../../interface';
|
|
5
14
|
import { DecryptionError, ProtonDriveError } from '../../errors';
|
|
6
15
|
import { asyncIteratorMap } from '../asyncIteratorMap';
|
|
7
16
|
import { getErrorMessage } from '../errors';
|
|
@@ -41,10 +50,11 @@ const DECRYPTION_CONCURRENCY = 30;
|
|
|
41
50
|
* nodes metadata.
|
|
42
51
|
*/
|
|
43
52
|
export class NodesAccess {
|
|
53
|
+
private logger: Logger;
|
|
44
54
|
private debouncer: NodesDebouncer;
|
|
45
55
|
|
|
46
56
|
constructor(
|
|
47
|
-
private
|
|
57
|
+
private telemetry: ProtonDriveTelemetry,
|
|
48
58
|
private apiService: NodeAPIService,
|
|
49
59
|
private cache: NodesCache,
|
|
50
60
|
private cryptoCache: NodesCryptoCache,
|
|
@@ -54,13 +64,13 @@ export class NodesAccess {
|
|
|
54
64
|
'getOwnVolumeIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
|
|
55
65
|
>,
|
|
56
66
|
) {
|
|
57
|
-
this.logger =
|
|
67
|
+
this.logger = telemetry.getLogger('nodes');
|
|
58
68
|
this.apiService = apiService;
|
|
59
69
|
this.cache = cache;
|
|
60
70
|
this.cryptoCache = cryptoCache;
|
|
61
71
|
this.cryptoService = cryptoService;
|
|
62
72
|
this.shareService = shareService;
|
|
63
|
-
this.debouncer = new NodesDebouncer(this.
|
|
73
|
+
this.debouncer = new NodesDebouncer(this.telemetry);
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
async getVolumeRootFolder() {
|
|
@@ -357,7 +367,7 @@ export class NodesAccess {
|
|
|
357
367
|
}
|
|
358
368
|
|
|
359
369
|
async getParentKeys(
|
|
360
|
-
node: Pick<DecryptedNode, 'parentUid' | 'shareId'>,
|
|
370
|
+
node: Pick<DecryptedNode, 'uid' | 'parentUid' | 'shareId'>,
|
|
361
371
|
): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
|
|
362
372
|
if (node.parentUid) {
|
|
363
373
|
try {
|
|
@@ -50,7 +50,7 @@ describe('NodesManagement', () => {
|
|
|
50
50
|
apiService = {
|
|
51
51
|
renameNode: jest.fn(),
|
|
52
52
|
moveNode: jest.fn(),
|
|
53
|
-
copyNode: jest.fn(),
|
|
53
|
+
copyNode: jest.fn().mockResolvedValue('newCopiedNodeUid'),
|
|
54
54
|
trashNodes: jest.fn(async function* (uids) {
|
|
55
55
|
yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
|
|
56
56
|
}),
|
|
@@ -251,6 +251,7 @@ describe('NodesManagement', () => {
|
|
|
251
251
|
|
|
252
252
|
expect(newNode).toEqual({
|
|
253
253
|
...nodes.nodeUid,
|
|
254
|
+
uid: 'newCopiedNodeUid',
|
|
254
255
|
parentUid: 'newParentNodeUid',
|
|
255
256
|
encryptedName: 'copiedArmoredNodeName',
|
|
256
257
|
hash: 'copiedHash',
|
|
@@ -307,6 +308,7 @@ describe('NodesManagement', () => {
|
|
|
307
308
|
);
|
|
308
309
|
expect(newNode).toEqual({
|
|
309
310
|
...nodes.anonymousNodeUid,
|
|
311
|
+
uid: 'newCopiedNodeUid',
|
|
310
312
|
parentUid: 'newParentNodeUid',
|
|
311
313
|
encryptedName: 'copiedArmoredNodeName',
|
|
312
314
|
hash: 'copiedHash',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
|
-
import { MemberRole, NodeType, NodeResult, resultOk } from '../../interface';
|
|
3
|
+
import { MemberRole, NodeType, NodeResult, NodeResultWithNewUid, resultOk } from '../../interface';
|
|
4
4
|
import { AbortError, ValidationError } from '../../errors';
|
|
5
5
|
import { getErrorMessage } from '../errors';
|
|
6
6
|
import { splitNodeUid } from '../uids';
|
|
@@ -182,16 +182,21 @@ export class NodesManagement {
|
|
|
182
182
|
return newNode;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
// Improvement requested: copy nodes in parallel
|
|
186
|
-
async *copyNodes(
|
|
185
|
+
// Improvement requested: copy nodes in parallel using copy_multiple endpoint
|
|
186
|
+
async *copyNodes(
|
|
187
|
+
nodeUids: string[],
|
|
188
|
+
newParentNodeUid: string,
|
|
189
|
+
signal?: AbortSignal,
|
|
190
|
+
): AsyncGenerator<NodeResultWithNewUid> {
|
|
187
191
|
for (const nodeUid of nodeUids) {
|
|
188
192
|
if (signal?.aborted) {
|
|
189
193
|
throw new AbortError(c('Error').t`Copy operation aborted`);
|
|
190
194
|
}
|
|
191
195
|
try {
|
|
192
|
-
await this.copyNode(nodeUid, newParentNodeUid);
|
|
196
|
+
const { uid: newNodeUid } = await this.copyNode(nodeUid, newParentNodeUid);
|
|
193
197
|
yield {
|
|
194
198
|
uid: nodeUid,
|
|
199
|
+
newUid: newNodeUid,
|
|
195
200
|
ok: true,
|
|
196
201
|
};
|
|
197
202
|
} catch (error: unknown) {
|
|
@@ -237,7 +242,7 @@ export class NodesManagement {
|
|
|
237
242
|
signatureEmail: encryptedCrypto.signatureEmail,
|
|
238
243
|
armoredNodePassphraseSignature: encryptedCrypto.armoredNodePassphraseSignature,
|
|
239
244
|
};
|
|
240
|
-
await this.apiService.copyNode(nodeUid, {
|
|
245
|
+
const newNodeUid = await this.apiService.copyNode(nodeUid, {
|
|
241
246
|
...keySignatureProperties,
|
|
242
247
|
parentUid: newParentUid,
|
|
243
248
|
armoredNodePassphrase: encryptedCrypto.armoredNodePassphrase,
|
|
@@ -247,6 +252,7 @@ export class NodesManagement {
|
|
|
247
252
|
});
|
|
248
253
|
const newNode: DecryptedNode = {
|
|
249
254
|
...node,
|
|
255
|
+
uid: newNodeUid,
|
|
250
256
|
encryptedName: encryptedCrypto.encryptedName,
|
|
251
257
|
parentUid: newParentUid,
|
|
252
258
|
hash: encryptedCrypto.hash,
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { DriveAPIService, drivePaths, NotFoundAPIError } from '../apiService';
|
|
1
|
+
import { DriveAPIService, drivePaths } from '../apiService';
|
|
4
2
|
import { EncryptedRootShare, EncryptedShareCrypto, ShareType } from '../shares/interface';
|
|
5
3
|
import { makeNodeUid } from '../uids';
|
|
6
4
|
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
type GetShareResponse = drivePaths['/drive/shares/{shareID}']['get']['responses']['200']['content']['application/json'];
|
|
5
|
+
type GetPhotoShareResponse =
|
|
6
|
+
drivePaths['/drive/v2/shares/photos']['get']['responses']['200']['content']['application/json'];
|
|
10
7
|
|
|
11
8
|
type PostCreateVolumeRequest = Extract<
|
|
12
9
|
drivePaths['/drive/photos/volumes']['post']['requestBody'],
|
|
@@ -34,33 +31,19 @@ export class PhotosAPIService {
|
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
async getPhotoShare(): Promise<EncryptedRootShare> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const volumesResponse = await this.apiService.get<GetVolumesResponse>('drive/volumes');
|
|
40
|
-
|
|
41
|
-
const photoVolume = volumesResponse.Volumes.find((volume) => volume.Type === 2);
|
|
42
|
-
|
|
43
|
-
if (!photoVolume) {
|
|
44
|
-
throw new NotFoundAPIError(c('Error').t`Photo volume not found`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const response = await this.apiService.get<GetShareResponse>(`drive/shares/${photoVolume.Share.ShareID}`);
|
|
48
|
-
|
|
49
|
-
if (!response.AddressID) {
|
|
50
|
-
throw new Error('Photo root share has not address ID set');
|
|
51
|
-
}
|
|
34
|
+
const response = await this.apiService.get<GetPhotoShareResponse>('drive/v2/shares/photos');
|
|
52
35
|
|
|
53
36
|
return {
|
|
54
|
-
volumeId: response.VolumeID,
|
|
55
|
-
shareId: response.ShareID,
|
|
56
|
-
rootNodeId: response.LinkID,
|
|
57
|
-
creatorEmail: response.
|
|
37
|
+
volumeId: response.Volume.VolumeID,
|
|
38
|
+
shareId: response.Share.ShareID,
|
|
39
|
+
rootNodeId: response.Link.Link.LinkID,
|
|
40
|
+
creatorEmail: response.Share.CreatorEmail,
|
|
58
41
|
encryptedCrypto: {
|
|
59
|
-
armoredKey: response.Key,
|
|
60
|
-
armoredPassphrase: response.Passphrase,
|
|
61
|
-
armoredPassphraseSignature: response.PassphraseSignature,
|
|
42
|
+
armoredKey: response.Share.Key,
|
|
43
|
+
armoredPassphrase: response.Share.Passphrase,
|
|
44
|
+
armoredPassphraseSignature: response.Share.PassphraseSignature,
|
|
62
45
|
},
|
|
63
|
-
addressId: response.AddressID,
|
|
46
|
+
addressId: response.Share.AddressID,
|
|
64
47
|
type: ShareType.Photo,
|
|
65
48
|
};
|
|
66
49
|
}
|
|
@@ -85,7 +85,25 @@ export class PhotoStreamUploader extends StreamUploader {
|
|
|
85
85
|
controller: UploadController,
|
|
86
86
|
signal?: AbortSignal,
|
|
87
87
|
) {
|
|
88
|
-
|
|
88
|
+
const abortController = new AbortController();
|
|
89
|
+
if (signal) {
|
|
90
|
+
signal.addEventListener('abort', () => {
|
|
91
|
+
abortController.abort();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
super(
|
|
96
|
+
telemetry,
|
|
97
|
+
apiService,
|
|
98
|
+
cryptoService,
|
|
99
|
+
uploadManager,
|
|
100
|
+
blockVerifier,
|
|
101
|
+
revisionDraft,
|
|
102
|
+
metadata,
|
|
103
|
+
onFinish,
|
|
104
|
+
controller,
|
|
105
|
+
abortController,
|
|
106
|
+
);
|
|
89
107
|
this.photoUploadManager = uploadManager;
|
|
90
108
|
this.photoMetadata = metadata;
|
|
91
109
|
}
|
|
@@ -347,8 +347,8 @@ export class SharingAPIService {
|
|
|
347
347
|
return response.Share.ID;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
async deleteShare(shareId: string): Promise<void> {
|
|
351
|
-
await this.apiService.delete(`drive/shares/${shareId}?Force
|
|
350
|
+
async deleteShare(shareId: string, force: boolean = false): Promise<void> {
|
|
351
|
+
await this.apiService.delete(`drive/shares/${shareId}?Force=${force ? 1 : 0}`);
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
async inviteProtonUser(
|