@protontech/drive-sdk 0.8.0 → 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/errors.d.ts +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/interface/download.d.ts +14 -0
- 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 +1 -0
- package/dist/internal/nodes/apiService.js +3 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +1 -0
- package/dist/internal/nodes/nodesManagement.js +5 -0
- package/dist/internal/nodes/nodesManagement.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.js +1 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +2 -2
- package/src/interface/download.ts +16 -0
- 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 +7 -0
- package/src/internal/nodes/nodesManagement.ts +6 -0
- 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 +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { PrivateKey, SessionKey, base64StringToUint8Array } from '../../crypto';
|
|
4
|
-
import { AbortError } from '../../errors';
|
|
4
|
+
import { AbortError, IntegrityError } from '../../errors';
|
|
5
5
|
import { Logger } from '../../interface';
|
|
6
6
|
import { LoggerWithPrefix } from '../../telemetry';
|
|
7
7
|
import { APIHTTPError, HTTPErrorCode } from '../apiService';
|
|
@@ -10,7 +10,7 @@ import { DownloadAPIService } from './apiService';
|
|
|
10
10
|
import { getBlockIndex } from './blockIndex';
|
|
11
11
|
import { DownloadController } from './controller';
|
|
12
12
|
import { DownloadCryptoService } from './cryptoService';
|
|
13
|
-
import { BlockMetadata, RevisionKeys } from './interface';
|
|
13
|
+
import { BlockMetadata, RevisionKeys, SignatureVerificationError } from './interface';
|
|
14
14
|
import { BufferedSeekableStream } from './seekableStream';
|
|
15
15
|
import { DownloadTelemetry } from './telemetry';
|
|
16
16
|
|
|
@@ -233,13 +233,17 @@ export class FileDownloader {
|
|
|
233
233
|
);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
await writer.close();
|
|
237
236
|
void this.telemetry.downloadFinished(this.revision.uid, fileProgress);
|
|
238
237
|
this.logger.info(`Download succeeded`);
|
|
239
238
|
} catch (error: unknown) {
|
|
240
|
-
|
|
239
|
+
if (error instanceof SignatureVerificationError) {
|
|
240
|
+
this.logger.warn(`Download finished with signature verification issues`);
|
|
241
|
+
this.controller.setIsDownloadCompleteWithSignatureIssues(true);
|
|
242
|
+
error = new IntegrityError(error.message, error.debug, { cause: error });
|
|
243
|
+
} else {
|
|
244
|
+
this.logger.error(`Download failed`, error);
|
|
245
|
+
}
|
|
241
246
|
void this.telemetry.downloadFailed(this.revision.uid, error, fileProgress, this.getClaimedSizeInBytes());
|
|
242
|
-
await writer.abort?.();
|
|
243
247
|
throw error;
|
|
244
248
|
} finally {
|
|
245
249
|
this.logger.debug(`Download cleanup`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PrivateKey, PublicKey, SessionKey } from '../../crypto';
|
|
2
|
+
import { IntegrityError } from '../../errors';
|
|
2
3
|
import { NodeType, Result, MissingNode, MetricVolumeType } from '../../interface';
|
|
3
4
|
import { DecryptedNode, DecryptedRevision } from '../nodes';
|
|
4
5
|
|
|
@@ -35,3 +36,17 @@ export interface NodesServiceNode {
|
|
|
35
36
|
export interface RevisionsService {
|
|
36
37
|
getRevision(nodeRevisionUid: string): Promise<DecryptedRevision>;
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Error thrown when the manifest signature verification fails.
|
|
42
|
+
* This is a special case that is reported as download complete with signature
|
|
43
|
+
* issues. The client must then ask the user to agree to save the file anyway
|
|
44
|
+
* or abort and clean up the file.
|
|
45
|
+
*
|
|
46
|
+
* This error is not exposed to the client. It is only used internally to track
|
|
47
|
+
* the signature verification issues. For the client it must be reported as
|
|
48
|
+
* the IntegrityError.
|
|
49
|
+
*/
|
|
50
|
+
export class SignatureVerificationError extends IntegrityError {
|
|
51
|
+
name = 'SignatureVerificationError';
|
|
52
|
+
}
|
|
@@ -57,6 +57,9 @@ type PostCopyNodeRequest = Extract<
|
|
|
57
57
|
type PostCopyNodeResponse =
|
|
58
58
|
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['responses']['200']['content']['application/json'];
|
|
59
59
|
|
|
60
|
+
type EmptyTrashResponse =
|
|
61
|
+
drivePaths['/drive/volumes/{volumeID}/trash']['delete']['responses']['200']['content']['application/json'];
|
|
62
|
+
|
|
60
63
|
type PostTrashNodesRequest = Extract<
|
|
61
64
|
drivePaths['/drive/v2/volumes/{volumeID}/trash_multiple']['post']['requestBody'],
|
|
62
65
|
{ content: object }
|
|
@@ -436,6 +439,10 @@ export abstract class NodeAPIServiceBase<
|
|
|
436
439
|
}
|
|
437
440
|
}
|
|
438
441
|
|
|
442
|
+
async emptyTrash(volumeId: string): Promise<void> {
|
|
443
|
+
await this.apiService.delete<EmptyTrashResponse>(`drive/volumes/${volumeId}/trash`);
|
|
444
|
+
}
|
|
445
|
+
|
|
439
446
|
async *restoreNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
440
447
|
for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
|
|
441
448
|
const response = await this.apiService.put<PutRestoreNodesRequest, PutRestoreNodesResponse>(
|
|
@@ -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
|
|
|
@@ -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