@protontech/drive-sdk 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto/driveCrypto.js +1 -1
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/interface.d.ts +3 -1
- package/dist/crypto/openPGPCrypto.d.ts +4 -1
- package/dist/crypto/openPGPCrypto.js +2 -1
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/interface/account.d.ts +6 -0
- package/dist/interface/download.d.ts +14 -0
- package/dist/internal/apiService/driveTypes.d.ts +197 -22
- package/dist/internal/download/controller.d.ts +3 -0
- package/dist/internal/download/controller.js +7 -0
- package/dist/internal/download/controller.js.map +1 -1
- package/dist/internal/download/cryptoService.js +9 -2
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.js +9 -3
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +14 -11
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/download/interface.d.ts +14 -0
- package/dist/internal/download/interface.js +16 -0
- package/dist/internal/download/interface.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +2 -1
- package/dist/internal/nodes/apiService.js +5 -2
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +1 -0
- package/dist/internal/nodes/cryptoService.js +28 -4
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +70 -2
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +2 -1
- package/dist/internal/nodes/nodesManagement.js +6 -1
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/shares/apiService.js +2 -0
- package/dist/internal/shares/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +1 -1
- package/dist/internal/sharingPublic/nodes.js +2 -2
- package/dist/internal/sharingPublic/nodes.js.map +1 -1
- package/dist/internal/upload/apiService.d.ts +1 -0
- package/dist/internal/upload/apiService.js +12 -0
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/manager.js +19 -1
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +23 -0
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +1 -1
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/protonDriveClient.d.ts +1 -1
- package/dist/protonDriveClient.js +3 -3
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.js +1 -1
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +3 -1
- package/dist/protonDrivePublicLinkClient.js +4 -2
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +1 -0
- package/src/crypto/interface.ts +1 -0
- package/src/crypto/openPGPCrypto.ts +3 -0
- package/src/errors.ts +2 -2
- package/src/interface/account.ts +6 -0
- package/src/interface/download.ts +16 -0
- package/src/internal/apiService/driveTypes.ts +197 -22
- package/src/internal/download/controller.ts +9 -0
- package/src/internal/download/cryptoService.ts +13 -3
- package/src/internal/download/fileDownloader.test.ts +17 -11
- package/src/internal/download/fileDownloader.ts +9 -5
- package/src/internal/download/interface.ts +15 -0
- package/src/internal/nodes/apiService.ts +20 -6
- package/src/internal/nodes/cryptoService.test.ts +113 -2
- package/src/internal/nodes/cryptoService.ts +53 -8
- package/src/internal/nodes/nodesManagement.ts +7 -1
- package/src/internal/shares/apiService.ts +3 -1
- package/src/internal/sharingPublic/nodes.ts +2 -2
- package/src/internal/upload/apiService.ts +25 -0
- package/src/internal/upload/manager.test.ts +37 -0
- package/src/internal/upload/manager.ts +17 -1
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/protonDriveClient.ts +3 -3
- package/src/protonDrivePhotosClient.ts +1 -1
- package/src/protonDrivePublicLinkClient.ts +4 -2
|
@@ -20,6 +20,9 @@ describe('nodesCryptoService', () => {
|
|
|
20
20
|
|
|
21
21
|
let cryptoService: NodesCryptoService;
|
|
22
22
|
|
|
23
|
+
const publicAddressKey = { _idx: 21312 };
|
|
24
|
+
const ownPrivateAddressKey = { id: 'id', key: 'key' as unknown as PrivateKey };
|
|
25
|
+
|
|
23
26
|
beforeEach(() => {
|
|
24
27
|
jest.clearAllMocks();
|
|
25
28
|
|
|
@@ -71,7 +74,15 @@ describe('nodesCryptoService', () => {
|
|
|
71
74
|
};
|
|
72
75
|
account = {
|
|
73
76
|
// @ts-expect-error No need to implement all methods for mocking
|
|
74
|
-
getPublicKeys: jest.fn(async () => [
|
|
77
|
+
getPublicKeys: jest.fn(async () => [publicAddressKey]),
|
|
78
|
+
getOwnAddresses: jest.fn(async () => [
|
|
79
|
+
{
|
|
80
|
+
email: 'email',
|
|
81
|
+
addressId: 'addressId',
|
|
82
|
+
primaryKeyIndex: 0,
|
|
83
|
+
keys: [ownPrivateAddressKey],
|
|
84
|
+
},
|
|
85
|
+
]),
|
|
75
86
|
};
|
|
76
87
|
// @ts-expect-error No need to implement all methods for mocking
|
|
77
88
|
sharesService = {
|
|
@@ -576,6 +587,7 @@ describe('nodesCryptoService', () => {
|
|
|
576
587
|
armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
|
|
577
588
|
file: {
|
|
578
589
|
base64ContentKeyPacket: 'base64ContentKeyPacket',
|
|
590
|
+
armoredContentKeyPacketSignature: 'armoredContentKeyPacketSignature',
|
|
579
591
|
},
|
|
580
592
|
activeRevision: {
|
|
581
593
|
uid: 'revisionUid',
|
|
@@ -764,7 +776,7 @@ describe('nodesCryptoService', () => {
|
|
|
764
776
|
});
|
|
765
777
|
});
|
|
766
778
|
|
|
767
|
-
it('on content key packet', async () => {
|
|
779
|
+
it('on content key packet without fallback verification', async () => {
|
|
768
780
|
driveCrypto.decryptAndVerifySessionKey = jest.fn(
|
|
769
781
|
async () =>
|
|
770
782
|
Promise.resolve({
|
|
@@ -789,6 +801,105 @@ describe('nodesCryptoService', () => {
|
|
|
789
801
|
error: 'verification error',
|
|
790
802
|
});
|
|
791
803
|
});
|
|
804
|
+
|
|
805
|
+
it('on content key packet with successful fallback verification', async () => {
|
|
806
|
+
driveCrypto.decryptAndVerifySessionKey = jest
|
|
807
|
+
.fn()
|
|
808
|
+
.mockImplementationOnce(
|
|
809
|
+
async () =>
|
|
810
|
+
Promise.resolve({
|
|
811
|
+
sessionKey: 'contentKeyPacketSessionKey',
|
|
812
|
+
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
813
|
+
verificationErrors: [new Error('verification error')],
|
|
814
|
+
}) as any,
|
|
815
|
+
)
|
|
816
|
+
.mockImplementationOnce(
|
|
817
|
+
async () =>
|
|
818
|
+
Promise.resolve({
|
|
819
|
+
sessionKey: 'contentKeyPacketSessionKey',
|
|
820
|
+
verified: VERIFICATION_STATUS.SIGNED_AND_VALID,
|
|
821
|
+
}) as any,
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
const result = await cryptoService.decryptNode(
|
|
825
|
+
{
|
|
826
|
+
...encryptedNode,
|
|
827
|
+
creationTime: new Date('2022-01-01'),
|
|
828
|
+
},
|
|
829
|
+
parentKey,
|
|
830
|
+
);
|
|
831
|
+
verifyResult(result);
|
|
832
|
+
expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledTimes(2);
|
|
833
|
+
expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
|
|
834
|
+
'base64ContentKeyPacket',
|
|
835
|
+
'armoredContentKeyPacketSignature',
|
|
836
|
+
'decryptedKey',
|
|
837
|
+
['decryptedKey', publicAddressKey],
|
|
838
|
+
);
|
|
839
|
+
expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
|
|
840
|
+
'base64ContentKeyPacket',
|
|
841
|
+
'armoredContentKeyPacketSignature',
|
|
842
|
+
'decryptedKey',
|
|
843
|
+
[ownPrivateAddressKey.key],
|
|
844
|
+
);
|
|
845
|
+
expect(telemetry.recordMetric).not.toHaveBeenCalled();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('on content key packet with failed fallback verification', async () => {
|
|
849
|
+
driveCrypto.decryptAndVerifySessionKey = jest
|
|
850
|
+
.fn()
|
|
851
|
+
.mockImplementationOnce(
|
|
852
|
+
async () =>
|
|
853
|
+
Promise.resolve({
|
|
854
|
+
sessionKey: 'contentKeyPacketSessionKey',
|
|
855
|
+
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
856
|
+
verificationErrors: [new Error('verification error')],
|
|
857
|
+
}) as any,
|
|
858
|
+
)
|
|
859
|
+
.mockImplementationOnce(
|
|
860
|
+
async () =>
|
|
861
|
+
Promise.resolve({
|
|
862
|
+
sessionKey: 'contentKeyPacketSessionKey',
|
|
863
|
+
verified: VERIFICATION_STATUS.SIGNED_AND_INVALID,
|
|
864
|
+
verificationErrors: [new Error('fallback verification error')],
|
|
865
|
+
}) as any,
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const result = await cryptoService.decryptNode(
|
|
869
|
+
{
|
|
870
|
+
...encryptedNode,
|
|
871
|
+
creationTime: new Date('2022-01-01'),
|
|
872
|
+
},
|
|
873
|
+
parentKey,
|
|
874
|
+
);
|
|
875
|
+
verifyResult(result, {
|
|
876
|
+
keyAuthor: {
|
|
877
|
+
ok: false,
|
|
878
|
+
error: {
|
|
879
|
+
claimedAuthor: 'signatureEmail',
|
|
880
|
+
error: 'Signature verification for content key failed: verification error',
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledTimes(2);
|
|
885
|
+
expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
|
|
886
|
+
'base64ContentKeyPacket',
|
|
887
|
+
'armoredContentKeyPacketSignature',
|
|
888
|
+
'decryptedKey',
|
|
889
|
+
['decryptedKey', publicAddressKey],
|
|
890
|
+
);
|
|
891
|
+
expect(driveCrypto.decryptAndVerifySessionKey).toHaveBeenCalledWith(
|
|
892
|
+
'base64ContentKeyPacket',
|
|
893
|
+
'armoredContentKeyPacketSignature',
|
|
894
|
+
'decryptedKey',
|
|
895
|
+
[ownPrivateAddressKey.key],
|
|
896
|
+
);
|
|
897
|
+
verifyLogEventVerificationError({
|
|
898
|
+
field: 'nodeContentKey',
|
|
899
|
+
error: 'verification error',
|
|
900
|
+
fromBefore2024: true,
|
|
901
|
+
});
|
|
902
|
+
});
|
|
792
903
|
});
|
|
793
904
|
|
|
794
905
|
describe('should decrypt with decryption issues', () => {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
EncryptedRevision,
|
|
26
26
|
DecryptedUnparsedRevision,
|
|
27
27
|
NodeSigningKeys,
|
|
28
|
+
EncryptedNodeFileCrypto,
|
|
28
29
|
} from './interface';
|
|
29
30
|
|
|
30
31
|
export interface NodesCryptoReporter {
|
|
@@ -200,14 +201,7 @@ export class NodesCryptoService {
|
|
|
200
201
|
if ('file' in node.encryptedCrypto) {
|
|
201
202
|
const [activeRevisionPromise, contentKeyPacketSessionKeyPromise] = [
|
|
202
203
|
this.decryptRevision(node.uid, node.encryptedCrypto.activeRevision, key),
|
|
203
|
-
this.
|
|
204
|
-
node.encryptedCrypto.file.base64ContentKeyPacket,
|
|
205
|
-
node.encryptedCrypto.file.armoredContentKeyPacketSignature,
|
|
206
|
-
key,
|
|
207
|
-
// Content key packet is signed with the node key, but
|
|
208
|
-
// in the past some clients signed with the address key.
|
|
209
|
-
[key, ...keyVerificationKeys],
|
|
210
|
-
),
|
|
204
|
+
this.decryptContentKeyPacket(node, node.encryptedCrypto, key, keyVerificationKeys),
|
|
211
205
|
];
|
|
212
206
|
|
|
213
207
|
try {
|
|
@@ -502,6 +496,57 @@ export class NodesCryptoService {
|
|
|
502
496
|
};
|
|
503
497
|
}
|
|
504
498
|
|
|
499
|
+
private async decryptContentKeyPacket(
|
|
500
|
+
node: EncryptedNode,
|
|
501
|
+
encryptedCrypto: EncryptedNodeFileCrypto,
|
|
502
|
+
key: PrivateKey,
|
|
503
|
+
keyVerificationKeys: PublicKey[],
|
|
504
|
+
): Promise<{
|
|
505
|
+
sessionKey: SessionKey;
|
|
506
|
+
verified?: VERIFICATION_STATUS;
|
|
507
|
+
verificationErrors?: Error[];
|
|
508
|
+
}> {
|
|
509
|
+
const result = await this.driveCrypto.decryptAndVerifySessionKey(
|
|
510
|
+
encryptedCrypto.file.base64ContentKeyPacket,
|
|
511
|
+
encryptedCrypto.file.armoredContentKeyPacketSignature,
|
|
512
|
+
key,
|
|
513
|
+
// Content key packet is signed with the node key, but
|
|
514
|
+
// in the past some clients signed with the address key.
|
|
515
|
+
[key, ...keyVerificationKeys],
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// Return right away if the verification is signed or not signed.
|
|
519
|
+
// If the verification is failing and the file is before 2023, try
|
|
520
|
+
// to decrypt with all owners keys. Because of the old nodes signed
|
|
521
|
+
// with address key instead of node key, when the node was renamed
|
|
522
|
+
// or moved, it could change the address but without updating the
|
|
523
|
+
// content key packet, which is now failing.
|
|
524
|
+
if (result.verified !== VERIFICATION_STATUS.SIGNED_AND_INVALID || node.creationTime > new Date(2023, 0, 1)) {
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const allAddresses = await this.account.getOwnAddresses();
|
|
529
|
+
const allKeys = allAddresses.flatMap((address) => address.keys.map(({ key }) => key));
|
|
530
|
+
|
|
531
|
+
const resultWithAllKeys = await this.driveCrypto.decryptAndVerifySessionKey(
|
|
532
|
+
encryptedCrypto.file.base64ContentKeyPacket,
|
|
533
|
+
encryptedCrypto.file.armoredContentKeyPacketSignature,
|
|
534
|
+
key,
|
|
535
|
+
// Content key packet is signed with the node key, but
|
|
536
|
+
// in the past some clients signed with the address key.
|
|
537
|
+
allKeys,
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
// Return original result with original error if the fallback verification also fails.
|
|
541
|
+
if (resultWithAllKeys.verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
|
|
542
|
+
this.logger.warn(
|
|
543
|
+
'Content key packet signature verification failed, but fallback to all addresses succeeded',
|
|
544
|
+
);
|
|
545
|
+
return resultWithAllKeys;
|
|
546
|
+
}
|
|
547
|
+
return result;
|
|
548
|
+
}
|
|
549
|
+
|
|
505
550
|
private async decryptExtendedAttributes(
|
|
506
551
|
node: { uid: string; creationTime: Date },
|
|
507
552
|
encryptedExtendedAttributes: string | undefined,
|
|
@@ -127,6 +127,12 @@ export abstract class NodesManagementBase<
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
async emptyTrash(): Promise<void> {
|
|
131
|
+
const node = await this.nodesAccess.getVolumeRootFolder();
|
|
132
|
+
const { volumeId } = splitNodeUid(node.uid);
|
|
133
|
+
await this.apiService.emptyTrash(volumeId);
|
|
134
|
+
}
|
|
135
|
+
|
|
130
136
|
async moveNode(nodeUid: string, newParentUid: string): Promise<TDecryptedNode> {
|
|
131
137
|
const node = await this.nodesAccess.getNode(nodeUid);
|
|
132
138
|
|
|
@@ -293,7 +299,7 @@ export abstract class NodesManagementBase<
|
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
|
|
296
|
-
async *
|
|
302
|
+
async *deleteTrashedNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
297
303
|
for await (const result of this.apiService.deleteTrashedNodes(nodeUids, signal)) {
|
|
298
304
|
if (result.ok) {
|
|
299
305
|
await this.nodesAccess.notifyNodeDeleted(result.uid);
|
|
@@ -173,7 +173,7 @@ function convertSharePayload(response: GetShareResponse): EncryptedShare {
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
function convertShareTypeNumberToEnum(type: 1 | 2 | 3 | 4): ShareType {
|
|
176
|
+
function convertShareTypeNumberToEnum(type: 1 | 2 | 3 | 4 | 5): ShareType {
|
|
177
177
|
switch (type) {
|
|
178
178
|
case 1:
|
|
179
179
|
return ShareType.Main;
|
|
@@ -183,5 +183,7 @@ function convertShareTypeNumberToEnum(type: 1 | 2 | 3 | 4): ShareType {
|
|
|
183
183
|
return ShareType.Device;
|
|
184
184
|
case 4:
|
|
185
185
|
return ShareType.Photo;
|
|
186
|
+
case 5:
|
|
187
|
+
throw new Error('Organization shares are not supported yet');
|
|
186
188
|
}
|
|
187
189
|
}
|
|
@@ -87,10 +87,10 @@ export class SharingPublicNodesManagement extends NodesManagement {
|
|
|
87
87
|
super(apiService, cryptoCache, cryptoService, nodesAccess);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
async *
|
|
90
|
+
async *deleteMyNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
91
91
|
// Public link does not support trashing and deleting trashed nodes.
|
|
92
92
|
// Instead, if user is owner, API allows directly deleting existing nodes.
|
|
93
|
-
for await (const result of this.apiService.
|
|
93
|
+
for await (const result of this.apiService.deleteMyNodes(nodeUids, signal)) {
|
|
94
94
|
if (result.ok) {
|
|
95
95
|
await this.nodesAccess.notifyNodeDeleted(result.uid);
|
|
96
96
|
}
|
|
@@ -45,6 +45,13 @@ type PostDeleteNodesRequest = Extract<
|
|
|
45
45
|
type PostDeleteNodesResponse =
|
|
46
46
|
drivePaths['/drive/v2/volumes/{volumeID}/delete_multiple']['post']['responses']['200']['content']['application/json'];
|
|
47
47
|
|
|
48
|
+
type PostLoadLinksMetadataRequest = Extract<
|
|
49
|
+
drivePaths['/drive/v2/volumes/{volumeID}/links']['post']['requestBody'],
|
|
50
|
+
{ content: object }
|
|
51
|
+
>['content']['application/json'];
|
|
52
|
+
type PostLoadLinksMetadataResponse =
|
|
53
|
+
drivePaths['/drive/v2/volumes/{volumeID}/links']['post']['responses']['200']['content']['application/json'];
|
|
54
|
+
|
|
48
55
|
export class UploadAPIService {
|
|
49
56
|
constructor(
|
|
50
57
|
protected apiService: DriveAPIService,
|
|
@@ -262,4 +269,22 @@ export class UploadAPIService {
|
|
|
262
269
|
|
|
263
270
|
await this.apiService.postBlockStream(url, token, formData, onProgress, signal);
|
|
264
271
|
}
|
|
272
|
+
|
|
273
|
+
async isRevisionUploaded(nodeRevisionUid: string): Promise<boolean> {
|
|
274
|
+
const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(nodeRevisionUid);
|
|
275
|
+
const result = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(
|
|
276
|
+
`drive/v2/volumes/${volumeId}/links`,
|
|
277
|
+
{
|
|
278
|
+
LinkIDs: [nodeId],
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
if (result.Links.length === 0) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const link = result.Links[0];
|
|
285
|
+
return (
|
|
286
|
+
link.Link.State === 1 && // ACTIVE state
|
|
287
|
+
link.File?.ActiveRevision?.RevisionID === revisionId
|
|
288
|
+
);
|
|
289
|
+
}
|
|
265
290
|
}
|
|
@@ -327,5 +327,42 @@ describe('UploadManager', () => {
|
|
|
327
327
|
expect(nodesService.notifyChildCreated).toHaveBeenCalledWith('parentUid');
|
|
328
328
|
expect(nodesService.notifyNodeChanged).not.toHaveBeenCalled();
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
it('should ignore error if revision was committed successfully', async () => {
|
|
332
|
+
apiService.commitDraftRevision = jest
|
|
333
|
+
.fn()
|
|
334
|
+
.mockRejectedValue(new Error('Revision to commit must be a draft'));
|
|
335
|
+
apiService.isRevisionUploaded = jest.fn().mockResolvedValue(true);
|
|
336
|
+
|
|
337
|
+
await manager.commitDraft(nodeRevisionDraft as any, manifest, extendedAttributes);
|
|
338
|
+
|
|
339
|
+
expect(apiService.commitDraftRevision).toHaveBeenCalledWith(
|
|
340
|
+
nodeRevisionDraft.nodeRevisionUid,
|
|
341
|
+
expect.anything(),
|
|
342
|
+
);
|
|
343
|
+
expect(nodesService.notifyNodeChanged).toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should throw error if revision was not committed successfully', async () => {
|
|
347
|
+
apiService.commitDraftRevision = jest
|
|
348
|
+
.fn()
|
|
349
|
+
.mockRejectedValue(new Error('Revision to commit must be a draft'));
|
|
350
|
+
apiService.isRevisionUploaded = jest.fn().mockResolvedValue(false);
|
|
351
|
+
|
|
352
|
+
await expect(manager.commitDraft(nodeRevisionDraft as any, manifest, extendedAttributes)).rejects.toThrow(
|
|
353
|
+
'Revision to commit must be a draft',
|
|
354
|
+
);
|
|
355
|
+
expect(nodesService.notifyNodeChanged).not.toHaveBeenCalled();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should throw original error if revision cannot be verified', async () => {
|
|
359
|
+
apiService.commitDraftRevision = jest.fn().mockRejectedValue(new Error('Failed to commit revision'));
|
|
360
|
+
apiService.isRevisionUploaded = jest.fn().mockRejectedValue(new Error('Failed to verify revision'));
|
|
361
|
+
|
|
362
|
+
await expect(manager.commitDraft(nodeRevisionDraft as any, manifest, extendedAttributes)).rejects.toThrow(
|
|
363
|
+
'Failed to commit revision',
|
|
364
|
+
);
|
|
365
|
+
expect(nodesService.notifyNodeChanged).not.toHaveBeenCalled();
|
|
366
|
+
});
|
|
330
367
|
});
|
|
331
368
|
});
|
|
@@ -246,7 +246,23 @@ export class UploadManager {
|
|
|
246
246
|
manifest,
|
|
247
247
|
generatedExtendedAttributes,
|
|
248
248
|
);
|
|
249
|
-
|
|
249
|
+
try {
|
|
250
|
+
await this.apiService.commitDraftRevision(nodeRevisionDraft.nodeRevisionUid, nodeCommitCrypto);
|
|
251
|
+
} catch (error: unknown) {
|
|
252
|
+
// Commit might be sent but due to network error no response is
|
|
253
|
+
// received. In this case, API service automatically retries the
|
|
254
|
+
// request. If the first attempt passed, it will fail on the second
|
|
255
|
+
// attempt. We need to check if the revision was actually committed.
|
|
256
|
+
try {
|
|
257
|
+
const isRevisionUploaded = await this.apiService.isRevisionUploaded(nodeRevisionDraft.nodeRevisionUid);
|
|
258
|
+
if (!isRevisionUploaded) {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
throw error; // Throw original error, not the checking one.
|
|
263
|
+
}
|
|
264
|
+
this.logger.warn(`Node commit failed but node was committed successfully ${nodeRevisionDraft.nodeUid}`);
|
|
265
|
+
}
|
|
250
266
|
await this.notifyNodeUploaded(nodeRevisionDraft);
|
|
251
267
|
}
|
|
252
268
|
|
|
@@ -181,7 +181,7 @@ export class StreamUploader {
|
|
|
181
181
|
await this.controller.waitWhilePaused();
|
|
182
182
|
await this.waitForUploadCapacityAndBufferedBlocks();
|
|
183
183
|
|
|
184
|
-
if (this.isEncryptionFullyFinished) {
|
|
184
|
+
if (this.isEncryptionFullyFinished || this.isUploadAborted) {
|
|
185
185
|
break;
|
|
186
186
|
}
|
|
187
187
|
|
package/src/protonDriveClient.ts
CHANGED
|
@@ -495,7 +495,7 @@ export class ProtonDriveClient {
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
/**
|
|
498
|
-
* Delete the nodes permanently.
|
|
498
|
+
* Delete the trashed nodes permanently. Only the owner can do that.
|
|
499
499
|
*
|
|
500
500
|
* The operation is performed in batches and the results are yielded
|
|
501
501
|
* as they are available. Order of the results is not guaranteed.
|
|
@@ -509,12 +509,12 @@ export class ProtonDriveClient {
|
|
|
509
509
|
*/
|
|
510
510
|
async *deleteNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
511
511
|
this.logger.info(`Deleting ${nodeUids.length} nodes`);
|
|
512
|
-
yield* this.nodes.management.
|
|
512
|
+
yield* this.nodes.management.deleteTrashedNodes(getUids(nodeUids), signal);
|
|
513
513
|
}
|
|
514
514
|
|
|
515
515
|
async emptyTrash(): Promise<void> {
|
|
516
516
|
this.logger.info('Emptying trash');
|
|
517
|
-
|
|
517
|
+
return this.nodes.management.emptyTrash();
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
/**
|
|
@@ -295,7 +295,7 @@ export class ProtonDrivePhotosClient {
|
|
|
295
295
|
*/
|
|
296
296
|
async *deleteNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
297
297
|
this.logger.info(`Deleting ${nodeUids.length} nodes`);
|
|
298
|
-
yield* this.nodes.management.
|
|
298
|
+
yield * this.nodes.management.deleteTrashedNodes(getUids(nodeUids), signal);
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
/**
|
|
@@ -233,13 +233,15 @@ export class ProtonDrivePublicLinkClient {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
* Delete
|
|
236
|
+
* Delete own nodes permanently. It skips the trash and allows to delete
|
|
237
|
+
* only nodes that are owned by the user. For anonymous files, this method
|
|
238
|
+
* allows to delete them only in the same session.
|
|
237
239
|
*
|
|
238
240
|
* See `ProtonDriveClient.deleteNodes` for more information.
|
|
239
241
|
*/
|
|
240
242
|
async *deleteNodes(nodeUids: NodeOrUid[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
241
243
|
this.logger.info(`Deleting ${nodeUids.length} nodes`);
|
|
242
|
-
yield* this.sharingPublic.nodes.management.
|
|
244
|
+
yield* this.sharingPublic.nodes.management.deleteMyNodes(getUids(nodeUids), signal);
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
/**
|