@protontech/drive-sdk 0.4.1 → 0.5.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/diagnostic/sdkDiagnostic.js +1 -1
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/interface/download.d.ts +4 -4
- 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 +31 -48
- 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/fileDownloader.d.ts +3 -3
- package/dist/internal/download/fileDownloader.js +5 -5
- 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 +44 -32
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +148 -17
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/debouncer.d.ts +23 -0
- package/dist/internal/nodes/debouncer.js +80 -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 +100 -0
- package/dist/internal/nodes/debouncer.test.js.map +1 -0
- package/dist/internal/nodes/nodesAccess.d.ts +2 -1
- package/dist/internal/nodes/nodesAccess.js +24 -5
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +2 -2
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +2 -1
- package/dist/internal/photos/upload.js +3 -3
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharingPublic/apiService.d.ts +2 -2
- package/dist/internal/sharingPublic/apiService.js +1 -63
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -4
- package/dist/internal/sharingPublic/cryptoCache.js +0 -28
- package/dist/internal/sharingPublic/cryptoCache.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
- package/dist/internal/sharingPublic/cryptoReporter.js +44 -0
- package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
- package/dist/internal/sharingPublic/cryptoService.d.ts +3 -4
- package/dist/internal/sharingPublic/cryptoService.js +5 -43
- package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +21 -3
- package/dist/internal/sharingPublic/index.js +43 -12
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/interface.d.ts +0 -1
- package/dist/internal/sharingPublic/nodes.d.ts +13 -0
- package/dist/internal/sharingPublic/nodes.js +28 -0
- package/dist/internal/sharingPublic/nodes.js.map +1 -0
- package/dist/internal/sharingPublic/session/session.d.ts +3 -3
- package/dist/internal/sharingPublic/session/url.test.js +3 -3
- package/dist/internal/sharingPublic/shares.d.ts +34 -0
- package/dist/internal/sharingPublic/shares.js +69 -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 +8 -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 +3 -3
- 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 +6 -2
- package/dist/internal/upload/streamUploader.js +8 -4
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +10 -6
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +3 -3
- package/dist/protonDriveClient.js +4 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +31 -4
- package/dist/protonDrivePublicLinkClient.js +52 -9
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/diagnostic/sdkDiagnostic.ts +1 -1
- package/src/interface/download.ts +4 -4
- 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 +31 -48
- 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/fileDownloader.test.ts +8 -8
- package/src/internal/download/fileDownloader.ts +5 -5
- package/src/internal/nodes/apiService.test.ts +199 -16
- package/src/internal/nodes/apiService.ts +62 -49
- package/src/internal/nodes/debouncer.test.ts +129 -0
- package/src/internal/nodes/debouncer.ts +93 -0
- package/src/internal/nodes/nodesAccess.test.ts +2 -2
- package/src/internal/nodes/nodesAccess.ts +30 -5
- package/src/internal/photos/upload.ts +4 -1
- package/src/internal/sharingPublic/apiService.ts +4 -87
- package/src/internal/sharingPublic/cryptoCache.ts +0 -34
- package/src/internal/sharingPublic/cryptoReporter.ts +73 -0
- package/src/internal/sharingPublic/cryptoService.ts +4 -80
- package/src/internal/sharingPublic/index.ts +68 -6
- package/src/internal/sharingPublic/interface.ts +0 -9
- package/src/internal/sharingPublic/nodes.ts +37 -0
- package/src/internal/sharingPublic/session/apiService.ts +1 -1
- package/src/internal/sharingPublic/session/session.ts +3 -3
- package/src/internal/sharingPublic/session/url.test.ts +3 -3
- package/src/internal/sharingPublic/shares.ts +86 -0
- package/src/internal/upload/apiService.ts +12 -1
- package/src/internal/upload/controller.ts +2 -2
- package/src/internal/upload/fileUploader.test.ts +25 -11
- package/src/internal/upload/fileUploader.ts +4 -3
- package/src/internal/upload/streamUploader.test.ts +15 -3
- package/src/internal/upload/streamUploader.ts +8 -3
- package/src/protonDriveClient.ts +4 -4
- package/src/protonDrivePublicLinkClient.ts +93 -12
- 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/manager.ts +0 -86
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Logger } from "../../interface";
|
|
2
|
+
import { LoggerWithPrefix } from '../../telemetry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The timeout for which the node is considered to be loading.
|
|
6
|
+
* If the node is not loaded after this timeout, it is considered to be
|
|
7
|
+
* loaded or failed to be loaded, and allowed other places to proceed.
|
|
8
|
+
*
|
|
9
|
+
* Decrypting many nodes in parallel can take a lot of time, so we allow
|
|
10
|
+
* more time for this.
|
|
11
|
+
*/
|
|
12
|
+
const DEBOUNCE_TIMEOUT = 5000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper to avoid loading the same node twice.
|
|
16
|
+
*
|
|
17
|
+
* Each place that loads a node should report it is being loaded,
|
|
18
|
+
* and when it is finished, it should report it is finished.
|
|
19
|
+
* The finish must be called even if the node fails to be loaded
|
|
20
|
+
* to clear the promise.
|
|
21
|
+
*
|
|
22
|
+
* Each place that loads a node from cache should first wait for
|
|
23
|
+
* the node to be loaded if that is the case.
|
|
24
|
+
*/
|
|
25
|
+
export class NodesDebouncer {
|
|
26
|
+
private promises: Map<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
promise: Promise<void>;
|
|
30
|
+
resolve: () => void;
|
|
31
|
+
timeout: NodeJS.Timeout;
|
|
32
|
+
}
|
|
33
|
+
> = new Map();
|
|
34
|
+
|
|
35
|
+
constructor(private logger: Logger) {
|
|
36
|
+
this.logger = new LoggerWithPrefix(logger, 'debouncer');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
loadingNodes(nodeUids: string[]) {
|
|
40
|
+
for (const nodeUid of nodeUids) {
|
|
41
|
+
this.loadingNode(nodeUid);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
loadingNode(nodeUid: string) {
|
|
46
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
47
|
+
if (this.promises.has(nodeUid)) {
|
|
48
|
+
this.logger.warn(`Loading twice for: ${nodeUid}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const timeout = setTimeout(() => {
|
|
53
|
+
this.logger.warn(`Timeout for: ${nodeUid}`);
|
|
54
|
+
this.finishedLoadingNode(nodeUid);
|
|
55
|
+
}, DEBOUNCE_TIMEOUT);
|
|
56
|
+
this.promises.set(nodeUid, { promise, resolve, timeout });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
finishedLoadingNodes(nodeUids: string[]) {
|
|
60
|
+
for (const nodeUid of nodeUids) {
|
|
61
|
+
this.finishedLoadingNode(nodeUid);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
finishedLoadingNode(nodeUid: string) {
|
|
66
|
+
const result = this.promises.get(nodeUid);
|
|
67
|
+
if (!result) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clearTimeout(result.timeout);
|
|
72
|
+
result.resolve();
|
|
73
|
+
this.promises.delete(nodeUid);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async waitForLoadingNode(nodeUid: string) {
|
|
77
|
+
const result = this.promises.get(nodeUid);
|
|
78
|
+
if (!result) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.logger.debug(`Wait for: ${nodeUid}`);
|
|
83
|
+
await result.promise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
clear() {
|
|
87
|
+
for (const result of this.promises.values()) {
|
|
88
|
+
clearTimeout(result.timeout);
|
|
89
|
+
result.resolve();
|
|
90
|
+
}
|
|
91
|
+
this.promises.clear();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -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* () {
|
|
@@ -11,6 +11,7 @@ import { NodeAPIService } from './apiService';
|
|
|
11
11
|
import { NodesCache } from './cache';
|
|
12
12
|
import { NodesCryptoCache } from './cryptoCache';
|
|
13
13
|
import { NodesCryptoService } from './cryptoService';
|
|
14
|
+
import { NodesDebouncer } from './debouncer';
|
|
14
15
|
import { parseFileExtendedAttributes, parseFolderExtendedAttributes } from './extendedAttributes';
|
|
15
16
|
import {
|
|
16
17
|
SharesService,
|
|
@@ -40,13 +41,18 @@ const DECRYPTION_CONCURRENCY = 30;
|
|
|
40
41
|
* nodes metadata.
|
|
41
42
|
*/
|
|
42
43
|
export class NodesAccess {
|
|
44
|
+
private debouncer: NodesDebouncer;
|
|
45
|
+
|
|
43
46
|
constructor(
|
|
44
47
|
private logger: Logger,
|
|
45
48
|
private apiService: NodeAPIService,
|
|
46
49
|
private cache: NodesCache,
|
|
47
50
|
private cryptoCache: NodesCryptoCache,
|
|
48
51
|
private cryptoService: NodesCryptoService,
|
|
49
|
-
private shareService:
|
|
52
|
+
private shareService: Pick<
|
|
53
|
+
SharesService,
|
|
54
|
+
'getOwnVolumeIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
|
|
55
|
+
>,
|
|
50
56
|
) {
|
|
51
57
|
this.logger = logger;
|
|
52
58
|
this.apiService = apiService;
|
|
@@ -54,6 +60,7 @@ export class NodesAccess {
|
|
|
54
60
|
this.cryptoCache = cryptoCache;
|
|
55
61
|
this.cryptoService = cryptoService;
|
|
56
62
|
this.shareService = shareService;
|
|
63
|
+
this.debouncer = new NodesDebouncer(this.logger);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
async getVolumeRootFolder() {
|
|
@@ -65,6 +72,7 @@ export class NodesAccess {
|
|
|
65
72
|
async getNode(nodeUid: string): Promise<DecryptedNode> {
|
|
66
73
|
let cachedNode;
|
|
67
74
|
try {
|
|
75
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
68
76
|
cachedNode = await this.cache.getNode(nodeUid);
|
|
69
77
|
} catch {}
|
|
70
78
|
|
|
@@ -112,6 +120,7 @@ export class NodesAccess {
|
|
|
112
120
|
for await (const nodeUid of this.apiService.iterateChildrenNodeUids(parentNode.uid, onlyFolders, signal)) {
|
|
113
121
|
let node;
|
|
114
122
|
try {
|
|
123
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
115
124
|
node = await this.cache.getNode(nodeUid);
|
|
116
125
|
} catch {}
|
|
117
126
|
|
|
@@ -143,6 +152,7 @@ export class NodesAccess {
|
|
|
143
152
|
for await (const nodeUid of this.apiService.iterateTrashedNodeUids(volumeId, signal)) {
|
|
144
153
|
let node;
|
|
145
154
|
try {
|
|
155
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
146
156
|
node = await this.cache.getNode(nodeUid);
|
|
147
157
|
} catch {}
|
|
148
158
|
|
|
@@ -208,9 +218,14 @@ export class NodesAccess {
|
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
this.debouncer.loadingNode(nodeUid);
|
|
222
|
+
try {
|
|
223
|
+
const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
|
|
224
|
+
const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
|
|
225
|
+
return this.decryptNode(encryptedNode);
|
|
226
|
+
} finally {
|
|
227
|
+
this.debouncer.finishedLoadingNode(nodeUid);
|
|
228
|
+
}
|
|
214
229
|
}
|
|
215
230
|
|
|
216
231
|
private async *loadNodes(
|
|
@@ -236,7 +251,14 @@ export class NodesAccess {
|
|
|
236
251
|
|
|
237
252
|
const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
|
|
238
253
|
|
|
239
|
-
const
|
|
254
|
+
const apiNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
|
|
255
|
+
|
|
256
|
+
const debouncedNodeMapper = async (encryptedNode: EncryptedNode): Promise<EncryptedNode> => {
|
|
257
|
+
this.debouncer.loadingNode(encryptedNode.uid);
|
|
258
|
+
return encryptedNode;
|
|
259
|
+
};
|
|
260
|
+
const encryptedNodesIterator = asyncIteratorMap(apiNodesIterator, debouncedNodeMapper, 1);
|
|
261
|
+
|
|
240
262
|
const decryptNodeMapper = async (encryptedNode: EncryptedNode): Promise<Result<DecryptedNode, unknown>> => {
|
|
241
263
|
returnedNodeUids.push(encryptedNode.uid);
|
|
242
264
|
try {
|
|
@@ -250,6 +272,7 @@ export class NodesAccess {
|
|
|
250
272
|
encryptedNodesIterator,
|
|
251
273
|
decryptNodeMapper,
|
|
252
274
|
DECRYPTION_CONCURRENCY,
|
|
275
|
+
signal,
|
|
253
276
|
);
|
|
254
277
|
for await (const node of decryptedNodesIterator) {
|
|
255
278
|
if (node.ok) {
|
|
@@ -329,6 +352,7 @@ export class NodesAccess {
|
|
|
329
352
|
this.logger.error(`Failed to cache node keys ${node.uid}`, error);
|
|
330
353
|
}
|
|
331
354
|
}
|
|
355
|
+
this.debouncer.finishedLoadingNode(node.uid);
|
|
332
356
|
return { node, keys };
|
|
333
357
|
}
|
|
334
358
|
|
|
@@ -360,6 +384,7 @@ export class NodesAccess {
|
|
|
360
384
|
|
|
361
385
|
async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
|
|
362
386
|
try {
|
|
387
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
363
388
|
return await this.cryptoCache.getNodeKeys(nodeUid);
|
|
364
389
|
} catch {
|
|
365
390
|
const { keys } = await this.loadNode(nodeUid);
|
|
@@ -5,6 +5,7 @@ import { generateFileExtendedAttributes } from '../nodes';
|
|
|
5
5
|
import { splitNodeRevisionUid } from '../uids';
|
|
6
6
|
import { UploadAPIService } from '../upload/apiService';
|
|
7
7
|
import { BlockVerifier } from '../upload/blockVerifier';
|
|
8
|
+
import { UploadController } from '../upload/controller';
|
|
8
9
|
import { UploadCryptoService } from '../upload/cryptoService';
|
|
9
10
|
import { FileUploader } from '../upload/fileUploader';
|
|
10
11
|
import { NodeRevisionDraft, NodesService } from '../upload/interface';
|
|
@@ -62,6 +63,7 @@ export class PhotoFileUploader extends FileUploader {
|
|
|
62
63
|
revisionDraft,
|
|
63
64
|
this.photoMetadata,
|
|
64
65
|
onFinish,
|
|
66
|
+
this.controller,
|
|
65
67
|
this.signal,
|
|
66
68
|
);
|
|
67
69
|
}
|
|
@@ -80,9 +82,10 @@ export class PhotoStreamUploader extends StreamUploader {
|
|
|
80
82
|
revisionDraft: NodeRevisionDraft,
|
|
81
83
|
metadata: PhotoUploadMetadata,
|
|
82
84
|
onFinish: (failure: boolean) => Promise<void>,
|
|
85
|
+
controller: UploadController,
|
|
83
86
|
signal?: AbortSignal,
|
|
84
87
|
) {
|
|
85
|
-
super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, signal);
|
|
88
|
+
super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, controller, signal);
|
|
86
89
|
this.photoUploadManager = uploadManager;
|
|
87
90
|
this.photoMetadata = metadata;
|
|
88
91
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { DriveAPIService, drivePaths, nodeTypeNumberToNodeType } from '../apiService';
|
|
2
2
|
import { Logger, MemberRole } from '../../interface';
|
|
3
|
-
import { makeNodeUid
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const PAGE_SIZE = 50;
|
|
3
|
+
import { makeNodeUid } from '../uids';
|
|
4
|
+
import { EncryptedNode } from '../nodes/interface';
|
|
5
|
+
import { EncryptedShareCrypto } from './interface';
|
|
7
6
|
|
|
8
7
|
type GetTokenInfoResponse = drivePaths['/drive/urls/{token}']['get']['responses']['200']['content']['application/json'];
|
|
9
8
|
|
|
10
|
-
type GetTokenFolderChildrenResponse =
|
|
11
|
-
drivePaths['/drive/urls/{token}/folders/{linkID}/children']['get']['responses']['200']['content']['application/json'];
|
|
12
|
-
|
|
13
9
|
/**
|
|
14
10
|
* Provides API communication for accessing public link data.
|
|
15
11
|
*
|
|
@@ -41,27 +37,6 @@ export class SharingPublicAPIService {
|
|
|
41
37
|
},
|
|
42
38
|
};
|
|
43
39
|
}
|
|
44
|
-
|
|
45
|
-
async *iterateFolderChildren(parentUid: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
|
|
46
|
-
const { volumeId: token, nodeId } = splitNodeUid(parentUid);
|
|
47
|
-
|
|
48
|
-
let page = 0;
|
|
49
|
-
while (true) {
|
|
50
|
-
const response = await this.apiService.get<GetTokenFolderChildrenResponse>(
|
|
51
|
-
`drive/urls/${token}/folders/${nodeId}/children?Page=${page}&PageSize=${PAGE_SIZE}`,
|
|
52
|
-
signal,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
for (const link of response.Links) {
|
|
56
|
-
yield linkToEncryptedNode(this.logger, token, link);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (response.Links.length < PAGE_SIZE) {
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
page++;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
40
|
}
|
|
66
41
|
|
|
67
42
|
function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token']): EncryptedNode {
|
|
@@ -70,7 +45,7 @@ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token
|
|
|
70
45
|
encryptedName: token.Name,
|
|
71
46
|
|
|
72
47
|
// Basic node metadata
|
|
73
|
-
uid: makeNodeUid(token.
|
|
48
|
+
uid: makeNodeUid(token.VolumeID, token.LinkID),
|
|
74
49
|
parentUid: undefined,
|
|
75
50
|
type: nodeTypeNumberToNodeType(logger, token.LinkType),
|
|
76
51
|
creationTime: new Date(), // TODO
|
|
@@ -115,61 +90,3 @@ function tokenToEncryptedNode(logger: Logger, token: GetTokenInfoResponse['Token
|
|
|
115
90
|
|
|
116
91
|
throw new Error(`Unknown node type: ${token.LinkType}`);
|
|
117
92
|
}
|
|
118
|
-
|
|
119
|
-
function linkToEncryptedNode(
|
|
120
|
-
logger: Logger,
|
|
121
|
-
token: string,
|
|
122
|
-
link: GetTokenFolderChildrenResponse['Links'][0],
|
|
123
|
-
): EncryptedNode {
|
|
124
|
-
const baseNodeMetadata = {
|
|
125
|
-
// Internal metadata
|
|
126
|
-
hash: link.Hash || undefined,
|
|
127
|
-
encryptedName: link.Name,
|
|
128
|
-
|
|
129
|
-
// Basic node metadata
|
|
130
|
-
uid: makeNodeUid(token, link.LinkID),
|
|
131
|
-
parentUid: link.ParentLinkID ? makeNodeUid(token, link.ParentLinkID) : undefined,
|
|
132
|
-
type: nodeTypeNumberToNodeType(logger, link.Type),
|
|
133
|
-
creationTime: new Date(), // TODO
|
|
134
|
-
totalStorageSize: link.TotalSize,
|
|
135
|
-
|
|
136
|
-
isShared: false,
|
|
137
|
-
isSharedPublicly: false,
|
|
138
|
-
directRole: MemberRole.Viewer, // TODO
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const baseCryptoNodeMetadata = {
|
|
142
|
-
signatureEmail: link.SignatureEmail || undefined,
|
|
143
|
-
armoredKey: link.NodeKey,
|
|
144
|
-
armoredNodePassphrase: link.NodePassphrase,
|
|
145
|
-
armoredNodePassphraseSignature: link.NodePassphraseSignature || undefined,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
if (link.Type === 1 && link.FolderProperties) {
|
|
149
|
-
return {
|
|
150
|
-
...baseNodeMetadata,
|
|
151
|
-
encryptedCrypto: {
|
|
152
|
-
...baseCryptoNodeMetadata,
|
|
153
|
-
folder: {
|
|
154
|
-
armoredHashKey: link.FolderProperties.NodeHashKey as string,
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (link.Type === 2 && link.FileProperties?.ContentKeyPacket) {
|
|
161
|
-
return {
|
|
162
|
-
...baseNodeMetadata,
|
|
163
|
-
totalStorageSize: link.FileProperties.ActiveRevision?.Size || undefined,
|
|
164
|
-
mediaType: link.MIMEType || undefined,
|
|
165
|
-
encryptedCrypto: {
|
|
166
|
-
...baseCryptoNodeMetadata,
|
|
167
|
-
file: {
|
|
168
|
-
base64ContentKeyPacket: link.FileProperties.ContentKeyPacket,
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
throw new Error(`Unknown node type: ${link.Type}`);
|
|
175
|
-
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { PrivateKey } from '../../crypto';
|
|
2
2
|
import { ProtonDriveCryptoCache, Logger } from '../../interface';
|
|
3
|
-
import { DecryptedNodeKeys } from './interface';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Provides caching for public link crypto material.
|
|
@@ -39,41 +38,8 @@ export class SharingPublicCryptoCache {
|
|
|
39
38
|
}
|
|
40
39
|
return shareKeyData.publicShareKey.key;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
async setNodeKeys(nodeUid: string, keys: DecryptedNodeKeys): Promise<void> {
|
|
44
|
-
const cacheUid = getNodeCacheKey(nodeUid);
|
|
45
|
-
await this.driveCache.setEntity(cacheUid, {
|
|
46
|
-
nodeKeys: keys,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
|
|
51
|
-
const nodeKeysData = await this.driveCache.getEntity(getNodeCacheKey(nodeUid));
|
|
52
|
-
if (!nodeKeysData.nodeKeys) {
|
|
53
|
-
try {
|
|
54
|
-
await this.removeNodeKeys([nodeUid]);
|
|
55
|
-
} catch (removingError: unknown) {
|
|
56
|
-
// The node keys will not be returned, thus SDK will re-fetch
|
|
57
|
-
// and re-cache it. Setting it again should then fix the problem.
|
|
58
|
-
this.logger.warn(
|
|
59
|
-
`Failed to remove corrupted public node keys from the cache: ${removingError instanceof Error ? removingError.message : removingError}`,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
throw new Error(`Failed to deserialize public node keys`);
|
|
63
|
-
}
|
|
64
|
-
return nodeKeysData.nodeKeys;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async removeNodeKeys(nodeUids: string[]): Promise<void> {
|
|
68
|
-
const cacheUids = nodeUids.map(getNodeCacheKey);
|
|
69
|
-
await this.driveCache.removeEntities(cacheUids);
|
|
70
|
-
}
|
|
71
41
|
}
|
|
72
42
|
|
|
73
43
|
function getShareKeyCacheKey() {
|
|
74
44
|
return 'publicShareKey';
|
|
75
45
|
}
|
|
76
|
-
|
|
77
|
-
function getNodeCacheKey(nodeUid: string) {
|
|
78
|
-
return `publicNodeKeys-${nodeUid}`;
|
|
79
|
-
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { c } from 'ttag';
|
|
2
|
+
|
|
3
|
+
import { VERIFICATION_STATUS } from '../../crypto';
|
|
4
|
+
import { getVerificationMessage } from '../errors';
|
|
5
|
+
import {
|
|
6
|
+
resultOk,
|
|
7
|
+
resultError,
|
|
8
|
+
Author,
|
|
9
|
+
AnonymousUser,
|
|
10
|
+
ProtonDriveTelemetry,
|
|
11
|
+
MetricVerificationErrorField,
|
|
12
|
+
MetricVolumeType,
|
|
13
|
+
MetricsDecryptionErrorField,
|
|
14
|
+
Logger,
|
|
15
|
+
} from '../../interface';
|
|
16
|
+
|
|
17
|
+
export class SharingPublicCryptoReporter {
|
|
18
|
+
private logger: Logger;
|
|
19
|
+
private telemetry: ProtonDriveTelemetry;
|
|
20
|
+
|
|
21
|
+
constructor(telemetry: ProtonDriveTelemetry) {
|
|
22
|
+
this.telemetry = telemetry;
|
|
23
|
+
this.logger = telemetry.getLogger('sharingPublic-crypto');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async handleClaimedAuthor(
|
|
27
|
+
node: { uid: string; creationTime: Date },
|
|
28
|
+
field: MetricVerificationErrorField,
|
|
29
|
+
signatureType: string,
|
|
30
|
+
verified: VERIFICATION_STATUS,
|
|
31
|
+
verificationErrors?: Error[],
|
|
32
|
+
claimedAuthor?: string,
|
|
33
|
+
notAvailableVerificationKeys = false,
|
|
34
|
+
): Promise<Author> {
|
|
35
|
+
if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
|
|
36
|
+
return resultOk(claimedAuthor || (null as AnonymousUser));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return resultError({
|
|
40
|
+
claimedAuthor,
|
|
41
|
+
error: !claimedAuthor
|
|
42
|
+
? c('Info').t`Author is not provided on public link`
|
|
43
|
+
: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
reportDecryptionError(
|
|
48
|
+
node: { uid: string; creationTime: Date },
|
|
49
|
+
field: MetricsDecryptionErrorField,
|
|
50
|
+
error: unknown,
|
|
51
|
+
) {
|
|
52
|
+
const fromBefore2024 = node.creationTime < new Date('2024-01-01');
|
|
53
|
+
|
|
54
|
+
this.logger.error(
|
|
55
|
+
`Failed to decrypt public link node ${node.uid} (from before 2024: ${fromBefore2024})`,
|
|
56
|
+
error,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
this.telemetry.recordMetric({
|
|
60
|
+
eventName: 'decryptionError',
|
|
61
|
+
volumeType: MetricVolumeType.SharedPublic,
|
|
62
|
+
field,
|
|
63
|
+
fromBefore2024,
|
|
64
|
+
error,
|
|
65
|
+
uid: node.uid,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
reportVerificationError() {
|
|
70
|
+
// Authors or signatures are not provided on public links.
|
|
71
|
+
// We do not report any signature verification errors at this moment.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,30 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { DriveCrypto, PrivateKey, VERIFICATION_STATUS } from '../../crypto';
|
|
4
|
-
import { getVerificationMessage } from '../errors';
|
|
5
|
-
import {
|
|
6
|
-
resultOk,
|
|
7
|
-
resultError,
|
|
8
|
-
Author,
|
|
9
|
-
AnonymousUser,
|
|
10
|
-
ProtonDriveTelemetry,
|
|
11
|
-
MetricVerificationErrorField,
|
|
12
|
-
MetricVolumeType,
|
|
13
|
-
MetricsDecryptionErrorField,
|
|
14
|
-
Logger,
|
|
15
|
-
ProtonDriveAccount,
|
|
16
|
-
} from '../../interface';
|
|
17
|
-
import { NodesCryptoService } from '../nodes/cryptoService';
|
|
1
|
+
import { DriveCrypto, PrivateKey } from '../../crypto';
|
|
18
2
|
import { EncryptedShareCrypto } from './interface';
|
|
19
3
|
|
|
20
|
-
export class SharingPublicCryptoService
|
|
4
|
+
export class SharingPublicCryptoService {
|
|
21
5
|
constructor(
|
|
22
|
-
|
|
23
|
-
driveCrypto: DriveCrypto,
|
|
24
|
-
account: ProtonDriveAccount,
|
|
6
|
+
private driveCrypto: DriveCrypto,
|
|
25
7
|
private password: string,
|
|
26
8
|
) {
|
|
27
|
-
|
|
9
|
+
this.driveCrypto = driveCrypto;
|
|
28
10
|
this.password = password;
|
|
29
11
|
}
|
|
30
12
|
|
|
@@ -38,61 +20,3 @@ export class SharingPublicCryptoService extends NodesCryptoService {
|
|
|
38
20
|
return shareKey;
|
|
39
21
|
}
|
|
40
22
|
}
|
|
41
|
-
|
|
42
|
-
class SharingPublicCryptoReporter {
|
|
43
|
-
private logger: Logger;
|
|
44
|
-
private telemetry: ProtonDriveTelemetry;
|
|
45
|
-
|
|
46
|
-
constructor(telemetry: ProtonDriveTelemetry) {
|
|
47
|
-
this.telemetry = telemetry;
|
|
48
|
-
this.logger = telemetry.getLogger('sharingPublic-crypto');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async handleClaimedAuthor(
|
|
52
|
-
node: { uid: string; creationTime: Date },
|
|
53
|
-
field: MetricVerificationErrorField,
|
|
54
|
-
signatureType: string,
|
|
55
|
-
verified: VERIFICATION_STATUS,
|
|
56
|
-
verificationErrors?: Error[],
|
|
57
|
-
claimedAuthor?: string,
|
|
58
|
-
notAvailableVerificationKeys = false,
|
|
59
|
-
): Promise<Author> {
|
|
60
|
-
if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
|
|
61
|
-
return resultOk(claimedAuthor || (null as AnonymousUser));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return resultError({
|
|
65
|
-
claimedAuthor,
|
|
66
|
-
error: !claimedAuthor
|
|
67
|
-
? c('Info').t`Author is not provided on public link`
|
|
68
|
-
: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
reportDecryptionError(
|
|
73
|
-
node: { uid: string; creationTime: Date },
|
|
74
|
-
field: MetricsDecryptionErrorField,
|
|
75
|
-
error: unknown,
|
|
76
|
-
) {
|
|
77
|
-
const fromBefore2024 = node.creationTime < new Date('2024-01-01');
|
|
78
|
-
|
|
79
|
-
this.logger.error(
|
|
80
|
-
`Failed to decrypt public link node ${node.uid} (from before 2024: ${fromBefore2024})`,
|
|
81
|
-
error,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
this.telemetry.recordMetric({
|
|
85
|
-
eventName: 'decryptionError',
|
|
86
|
-
volumeType: MetricVolumeType.SharedPublic,
|
|
87
|
-
field,
|
|
88
|
-
fromBefore2024,
|
|
89
|
-
error,
|
|
90
|
-
uid: node.uid,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
reportVerificationError() {
|
|
95
|
-
// Authors or signatures are not provided on public links.
|
|
96
|
-
// We do not report any signature verification errors at this moment.
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { DriveCrypto } from '../../crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ProtonDriveCryptoCache,
|
|
4
|
+
ProtonDriveTelemetry,
|
|
5
|
+
ProtonDriveAccount,
|
|
6
|
+
ProtonDriveEntitiesCache,
|
|
7
|
+
} from '../../interface';
|
|
3
8
|
import { DriveAPIService } from '../apiService';
|
|
9
|
+
import { NodeAPIService } from '../nodes/apiService';
|
|
10
|
+
import { NodesCache } from '../nodes/cache';
|
|
11
|
+
import { NodesCryptoCache } from '../nodes/cryptoCache';
|
|
12
|
+
import { NodesCryptoService } from '../nodes/cryptoService';
|
|
13
|
+
import { NodesRevisons } from '../nodes/nodesRevisions';
|
|
4
14
|
import { SharingPublicAPIService } from './apiService';
|
|
5
15
|
import { SharingPublicCryptoCache } from './cryptoCache';
|
|
16
|
+
import { SharingPublicCryptoReporter } from './cryptoReporter';
|
|
6
17
|
import { SharingPublicCryptoService } from './cryptoService';
|
|
7
|
-
import {
|
|
18
|
+
import { SharingPublicNodesAccess } from './nodes';
|
|
19
|
+
import { SharingPublicSharesManager } from './shares';
|
|
8
20
|
|
|
9
21
|
export { SharingPublicSessionManager } from './session/manager';
|
|
10
22
|
|
|
@@ -20,22 +32,72 @@ export { SharingPublicSessionManager } from './session/manager';
|
|
|
20
32
|
export function initSharingPublicModule(
|
|
21
33
|
telemetry: ProtonDriveTelemetry,
|
|
22
34
|
apiService: DriveAPIService,
|
|
35
|
+
driveEntitiesCache: ProtonDriveEntitiesCache,
|
|
23
36
|
driveCryptoCache: ProtonDriveCryptoCache,
|
|
24
37
|
driveCrypto: DriveCrypto,
|
|
25
38
|
account: ProtonDriveAccount,
|
|
39
|
+
url: string,
|
|
26
40
|
token: string,
|
|
27
41
|
password: string,
|
|
28
42
|
) {
|
|
29
43
|
const api = new SharingPublicAPIService(telemetry.getLogger('sharingPublic-api'), apiService);
|
|
30
44
|
const cryptoCache = new SharingPublicCryptoCache(telemetry.getLogger('sharingPublic-crypto'), driveCryptoCache);
|
|
31
|
-
const cryptoService = new SharingPublicCryptoService(
|
|
32
|
-
const
|
|
33
|
-
|
|
45
|
+
const cryptoService = new SharingPublicCryptoService(driveCrypto, password);
|
|
46
|
+
const shares = new SharingPublicSharesManager(api, cryptoCache, cryptoService, account, token);
|
|
47
|
+
const nodes = initSharingPublicNodesModule(
|
|
48
|
+
telemetry,
|
|
49
|
+
apiService,
|
|
50
|
+
driveEntitiesCache,
|
|
51
|
+
driveCryptoCache,
|
|
52
|
+
driveCrypto,
|
|
53
|
+
account,
|
|
54
|
+
shares,
|
|
55
|
+
url,
|
|
56
|
+
token,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
shares,
|
|
61
|
+
nodes,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provides facade for the public link nodes module.
|
|
67
|
+
*
|
|
68
|
+
* The public link nodes initializes the core nodes module, but uses public
|
|
69
|
+
* link shares or crypto reporter instead.
|
|
70
|
+
*/
|
|
71
|
+
export function initSharingPublicNodesModule(
|
|
72
|
+
telemetry: ProtonDriveTelemetry,
|
|
73
|
+
apiService: DriveAPIService,
|
|
74
|
+
driveEntitiesCache: ProtonDriveEntitiesCache,
|
|
75
|
+
driveCryptoCache: ProtonDriveCryptoCache,
|
|
76
|
+
driveCrypto: DriveCrypto,
|
|
77
|
+
account: ProtonDriveAccount,
|
|
78
|
+
sharesService: SharingPublicSharesManager,
|
|
79
|
+
url: string,
|
|
80
|
+
token: string,
|
|
81
|
+
) {
|
|
82
|
+
const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
|
|
83
|
+
const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
|
|
84
|
+
const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
|
|
85
|
+
const cryptoReporter = new SharingPublicCryptoReporter(telemetry);
|
|
86
|
+
const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
|
|
87
|
+
const nodesAccess = new SharingPublicNodesAccess(
|
|
88
|
+
telemetry.getLogger('nodes'),
|
|
34
89
|
api,
|
|
90
|
+
cache,
|
|
35
91
|
cryptoCache,
|
|
36
92
|
cryptoService,
|
|
93
|
+
sharesService,
|
|
94
|
+
url,
|
|
37
95
|
token,
|
|
38
96
|
);
|
|
97
|
+
const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
|
|
39
98
|
|
|
40
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
access: nodesAccess,
|
|
101
|
+
revisions: nodesRevisions,
|
|
102
|
+
};
|
|
41
103
|
}
|