@protontech/drive-sdk 0.3.2 → 0.4.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/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errors.d.ts +3 -0
- package/dist/internal/apiService/errors.js +7 -1
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/devices/interface.d.ts +1 -1
- package/dist/internal/devices/manager.js +1 -1
- package/dist/internal/devices/manager.js.map +1 -1
- package/dist/internal/devices/manager.test.js +3 -3
- package/dist/internal/devices/manager.test.js.map +1 -1
- package/dist/internal/events/apiService.js +1 -1
- package/dist/internal/events/apiService.js.map +1 -1
- package/dist/internal/events/coreEventManager.js +1 -1
- package/dist/internal/events/coreEventManager.js.map +1 -1
- package/dist/internal/events/coreEventManager.test.js +18 -24
- package/dist/internal/events/coreEventManager.test.js.map +1 -1
- package/dist/internal/events/index.d.ts +3 -4
- package/dist/internal/events/index.js +4 -4
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/events/interface.d.ts +3 -0
- package/dist/internal/nodes/apiService.d.ts +12 -3
- package/dist/internal/nodes/apiService.js +53 -13
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +19 -2
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +1 -1
- package/dist/internal/nodes/cryptoService.js +1 -1
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +4 -4
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/errors.d.ts +4 -0
- package/dist/internal/nodes/errors.js +9 -0
- package/dist/internal/nodes/errors.js.map +1 -0
- package/dist/internal/nodes/index.test.js +1 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +4 -1
- package/dist/internal/nodes/nodesAccess.d.ts +3 -3
- package/dist/internal/nodes/nodesAccess.js +25 -15
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +48 -8
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +2 -0
- package/dist/internal/nodes/nodesManagement.js +86 -9
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +81 -5
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/photos/albums.d.ts +9 -7
- package/dist/internal/photos/albums.js +26 -13
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/apiService.d.ts +34 -3
- package/dist/internal/photos/apiService.js +96 -3
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/index.d.ts +20 -4
- package/dist/internal/photos/index.js +30 -7
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +25 -1
- package/dist/internal/photos/shares.d.ts +43 -0
- package/dist/internal/photos/shares.js +112 -0
- package/dist/internal/photos/shares.js.map +1 -0
- package/dist/internal/photos/timeline.d.ts +15 -0
- package/dist/internal/photos/timeline.js +22 -0
- package/dist/internal/photos/timeline.js.map +1 -0
- package/dist/internal/shares/manager.d.ts +1 -1
- package/dist/internal/shares/manager.js +4 -4
- package/dist/internal/shares/manager.js.map +1 -1
- package/dist/internal/shares/manager.test.js +7 -7
- package/dist/internal/shares/manager.test.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +1 -1
- package/dist/internal/sharing/sharingAccess.js +1 -1
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +1 -1
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +20 -3
- package/dist/protonDriveClient.js +23 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +86 -12
- package/dist/protonDrivePhotosClient.js +132 -29
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/package.json +1 -1
- package/src/internal/apiService/errorCodes.ts +1 -0
- package/src/internal/apiService/errors.ts +6 -0
- package/src/internal/devices/interface.ts +1 -1
- package/src/internal/devices/manager.test.ts +3 -3
- package/src/internal/devices/manager.ts +1 -1
- package/src/internal/events/apiService.ts +1 -1
- package/src/internal/events/coreEventManager.test.ts +21 -27
- package/src/internal/events/coreEventManager.ts +1 -1
- package/src/internal/events/index.ts +3 -4
- package/src/internal/events/interface.ts +4 -0
- package/src/internal/nodes/apiService.test.ts +35 -1
- package/src/internal/nodes/apiService.ts +103 -17
- package/src/internal/nodes/cryptoService.test.ts +8 -8
- package/src/internal/nodes/cryptoService.ts +1 -1
- package/src/internal/nodes/errors.ts +5 -0
- package/src/internal/nodes/index.test.ts +1 -1
- package/src/internal/nodes/interface.ts +5 -1
- package/src/internal/nodes/nodesAccess.test.ts +68 -8
- package/src/internal/nodes/nodesAccess.ts +42 -15
- package/src/internal/nodes/nodesManagement.test.ts +100 -5
- package/src/internal/nodes/nodesManagement.ts +100 -13
- package/src/internal/photos/albums.ts +31 -12
- package/src/internal/photos/apiService.ts +159 -4
- package/src/internal/photos/index.ts +54 -9
- package/src/internal/photos/interface.ts +23 -1
- package/src/internal/photos/shares.ts +134 -0
- package/src/internal/photos/timeline.ts +24 -0
- package/src/internal/shares/manager.test.ts +7 -7
- package/src/internal/shares/manager.ts +4 -4
- package/src/internal/sharing/interface.ts +1 -1
- package/src/internal/sharing/sharingAccess.test.ts +1 -1
- package/src/internal/sharing/sharingAccess.ts +1 -1
- package/src/protonDriveClient.ts +33 -4
- package/src/protonDrivePhotosClient.ts +211 -32
- package/dist/internal/photos/cache.d.ts +0 -6
- package/dist/internal/photos/cache.js +0 -15
- package/dist/internal/photos/cache.js.map +0 -1
- package/dist/internal/photos/photosTimeline.d.ts +0 -10
- package/dist/internal/photos/photosTimeline.js +0 -19
- package/dist/internal/photos/photosTimeline.js.map +0 -1
- package/src/internal/photos/cache.ts +0 -11
- package/src/internal/photos/photosTimeline.ts +0 -17
|
@@ -6,6 +6,7 @@ import { MemberRole, RevisionState } from '../../interface/nodes';
|
|
|
6
6
|
import {
|
|
7
7
|
DriveAPIService,
|
|
8
8
|
drivePaths,
|
|
9
|
+
InvalidRequirementsAPIError,
|
|
9
10
|
isCodeOk,
|
|
10
11
|
nodeTypeNumberToNodeType,
|
|
11
12
|
permissionsToMemberRole,
|
|
@@ -13,7 +14,8 @@ import {
|
|
|
13
14
|
import { asyncIteratorRace } from '../asyncIteratorRace';
|
|
14
15
|
import { batch } from '../batch';
|
|
15
16
|
import { splitNodeUid, makeNodeUid, makeNodeRevisionUid, splitNodeRevisionUid, makeNodeThumbnailUid } from '../uids';
|
|
16
|
-
import {
|
|
17
|
+
import { NodeOutOfSyncError } from './errors';
|
|
18
|
+
import { EncryptedNode, EncryptedRevision, FilterOptions, Thumbnail } from './interface';
|
|
17
19
|
|
|
18
20
|
// This is the number of calls to the API that are made in parallel.
|
|
19
21
|
const API_CONCURRENCY = 15;
|
|
@@ -48,6 +50,13 @@ type PutMoveNodeRequest = Extract<
|
|
|
48
50
|
type PutMoveNodeResponse =
|
|
49
51
|
drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/move']['put']['responses']['200']['content']['application/json'];
|
|
50
52
|
|
|
53
|
+
type PostCopyNodeRequest = Extract<
|
|
54
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['requestBody'],
|
|
55
|
+
{ content: object }
|
|
56
|
+
>['content']['application/json'];
|
|
57
|
+
type PostCopyNodeResponse =
|
|
58
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['responses']['200']['content']['application/json'];
|
|
59
|
+
|
|
51
60
|
type PostTrashNodesRequest = Extract<
|
|
52
61
|
drivePaths['/drive/v2/volumes/{volumeID}/trash_multiple']['post']['requestBody'],
|
|
53
62
|
{ content: object }
|
|
@@ -108,7 +117,7 @@ export class NodeAPIService {
|
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
|
|
111
|
-
const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, signal);
|
|
120
|
+
const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, undefined, signal);
|
|
112
121
|
const result = await nodesGenerator.next();
|
|
113
122
|
if (!result.value) {
|
|
114
123
|
throw new ValidationError(c('Error').t`Node not found`);
|
|
@@ -117,7 +126,12 @@ export class NodeAPIService {
|
|
|
117
126
|
return result.value;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
async *iterateNodes(
|
|
129
|
+
async *iterateNodes(
|
|
130
|
+
nodeUids: string[],
|
|
131
|
+
ownVolumeId: string,
|
|
132
|
+
filterOptions?: FilterOptions,
|
|
133
|
+
signal?: AbortSignal,
|
|
134
|
+
): AsyncGenerator<EncryptedNode> {
|
|
121
135
|
const allNodeIds = nodeUids.map(splitNodeUid);
|
|
122
136
|
|
|
123
137
|
const nodeIdsByVolumeId = new Map<string, string[]>();
|
|
@@ -139,7 +153,13 @@ export class NodeAPIService {
|
|
|
139
153
|
const isAdmin = volumeId === ownVolumeId;
|
|
140
154
|
|
|
141
155
|
yield (async function* () {
|
|
142
|
-
const errorsPerVolume = yield* iterateNodesPerVolume(
|
|
156
|
+
const errorsPerVolume = yield* iterateNodesPerVolume(
|
|
157
|
+
volumeId,
|
|
158
|
+
nodeIds,
|
|
159
|
+
isAdmin,
|
|
160
|
+
filterOptions,
|
|
161
|
+
signal,
|
|
162
|
+
);
|
|
143
163
|
if (errorsPerVolume.length) {
|
|
144
164
|
errors.push(...errorsPerVolume);
|
|
145
165
|
}
|
|
@@ -159,6 +179,7 @@ export class NodeAPIService {
|
|
|
159
179
|
volumeId: string,
|
|
160
180
|
nodeIds: string[],
|
|
161
181
|
isOwnVolumeId: boolean,
|
|
182
|
+
filterOptions?: FilterOptions,
|
|
162
183
|
signal?: AbortSignal,
|
|
163
184
|
): AsyncGenerator<EncryptedNode, unknown[]> {
|
|
164
185
|
const errors: unknown[] = [];
|
|
@@ -174,7 +195,11 @@ export class NodeAPIService {
|
|
|
174
195
|
|
|
175
196
|
for (const link of response.Links) {
|
|
176
197
|
try {
|
|
177
|
-
|
|
198
|
+
const encryptedNode = linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
|
|
199
|
+
if (filterOptions?.type && encryptedNode.type !== filterOptions.type) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
yield encryptedNode;
|
|
178
203
|
} catch (error: unknown) {
|
|
179
204
|
this.logger.error(`Failed to transform node ${link.Link.LinkID}`, error);
|
|
180
205
|
errors.push(error);
|
|
@@ -186,13 +211,25 @@ export class NodeAPIService {
|
|
|
186
211
|
}
|
|
187
212
|
|
|
188
213
|
// Improvement requested: load next page sooner before all IDs are yielded.
|
|
189
|
-
async *iterateChildrenNodeUids(
|
|
214
|
+
async *iterateChildrenNodeUids(
|
|
215
|
+
parentNodeUid: string,
|
|
216
|
+
onlyFolders: boolean = false,
|
|
217
|
+
signal?: AbortSignal,
|
|
218
|
+
): AsyncGenerator<string> {
|
|
190
219
|
const { volumeId, nodeId } = splitNodeUid(parentNodeUid);
|
|
191
220
|
|
|
192
221
|
let anchor = '';
|
|
193
222
|
while (true) {
|
|
223
|
+
const queryParams = new URLSearchParams();
|
|
224
|
+
if (onlyFolders) {
|
|
225
|
+
queryParams.set('FoldersOnly', '1');
|
|
226
|
+
}
|
|
227
|
+
if (anchor) {
|
|
228
|
+
queryParams.set('AnchorID', anchor);
|
|
229
|
+
}
|
|
230
|
+
|
|
194
231
|
const response = await this.apiService.get<GetChildrenResponse>(
|
|
195
|
-
`drive/v2/volumes/${volumeId}/folders/${nodeId}/children?${
|
|
232
|
+
`drive/v2/volumes/${volumeId}/folders/${nodeId}/children?${queryParams.toString()}`,
|
|
196
233
|
signal,
|
|
197
234
|
);
|
|
198
235
|
for (const linkID of response.LinkIDs) {
|
|
@@ -251,16 +288,28 @@ export class NodeAPIService {
|
|
|
251
288
|
): Promise<void> {
|
|
252
289
|
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
253
290
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
try {
|
|
292
|
+
await this.apiService.put<
|
|
293
|
+
Omit<PutRenameNodeRequest, 'SignatureAddress' | 'MIMEType'>,
|
|
294
|
+
PutRenameNodeResponse
|
|
295
|
+
>(
|
|
296
|
+
`drive/v2/volumes/${volumeId}/links/${nodeId}/rename`,
|
|
297
|
+
{
|
|
298
|
+
Name: newNode.encryptedName,
|
|
299
|
+
NameSignatureEmail: newNode.nameSignatureEmail,
|
|
300
|
+
Hash: newNode.hash,
|
|
301
|
+
OriginalHash: originalNode.hash || null,
|
|
302
|
+
},
|
|
303
|
+
signal,
|
|
304
|
+
);
|
|
305
|
+
} catch (error: unknown) {
|
|
306
|
+
// API returns generic code 2000 when node is out of sync.
|
|
307
|
+
// We map this to specific error for clarity.
|
|
308
|
+
if (error instanceof InvalidRequirementsAPIError) {
|
|
309
|
+
throw new NodeOutOfSyncError(error.message, error.code, { cause: error });
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
264
313
|
}
|
|
265
314
|
|
|
266
315
|
async moveNode(
|
|
@@ -303,6 +352,43 @@ export class NodeAPIService {
|
|
|
303
352
|
);
|
|
304
353
|
}
|
|
305
354
|
|
|
355
|
+
async copyNode(
|
|
356
|
+
nodeUid: string,
|
|
357
|
+
newNode: {
|
|
358
|
+
parentUid: string;
|
|
359
|
+
armoredNodePassphrase: string;
|
|
360
|
+
armoredNodePassphraseSignature?: string;
|
|
361
|
+
signatureEmail?: string;
|
|
362
|
+
encryptedName: string;
|
|
363
|
+
nameSignatureEmail?: string;
|
|
364
|
+
hash: string;
|
|
365
|
+
},
|
|
366
|
+
signal?: AbortSignal,
|
|
367
|
+
): Promise<string> {
|
|
368
|
+
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
369
|
+
const { volumeId: parentVolumeId, nodeId: parentNodeId } = splitNodeUid(newNode.parentUid);
|
|
370
|
+
|
|
371
|
+
const response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
|
|
372
|
+
`drive/volumes/${volumeId}/links/${nodeId}/copy`,
|
|
373
|
+
{
|
|
374
|
+
TargetVolumeID: parentVolumeId,
|
|
375
|
+
TargetParentLinkID: parentNodeId,
|
|
376
|
+
NodePassphrase: newNode.armoredNodePassphrase,
|
|
377
|
+
// @ts-expect-error: API accepts NodePassphraseSignature as optional.
|
|
378
|
+
NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
|
|
379
|
+
// @ts-expect-error: API accepts SignatureEmail as optional.
|
|
380
|
+
SignatureEmail: newNode.signatureEmail,
|
|
381
|
+
Name: newNode.encryptedName,
|
|
382
|
+
// @ts-expect-error: API accepts NameSignatureEmail as optional.
|
|
383
|
+
NameSignatureEmail: newNode.nameSignatureEmail,
|
|
384
|
+
Hash: newNode.hash,
|
|
385
|
+
},
|
|
386
|
+
signal,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return makeNodeUid(volumeId, response.LinkID);
|
|
390
|
+
}
|
|
391
|
+
|
|
306
392
|
// Improvement requested: split into multiple calls for many nodes.
|
|
307
393
|
async *trashNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
308
394
|
const nodeIds = nodeUids.map(splitNodeUid);
|
|
@@ -985,7 +985,7 @@ describe('nodesCryptoService', () => {
|
|
|
985
985
|
});
|
|
986
986
|
});
|
|
987
987
|
|
|
988
|
-
describe('
|
|
988
|
+
describe('encryptNodeWithNewParent', () => {
|
|
989
989
|
it('should encrypt node data for move operation', async () => {
|
|
990
990
|
const node = {
|
|
991
991
|
name: { ok: true, value: 'testFile.txt' },
|
|
@@ -1012,7 +1012,7 @@ describe('nodesCryptoService', () => {
|
|
|
1012
1012
|
armoredPassphraseSignature: 'passphraseSignature',
|
|
1013
1013
|
});
|
|
1014
1014
|
|
|
1015
|
-
const result = await cryptoService.
|
|
1015
|
+
const result = await cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address);
|
|
1016
1016
|
|
|
1017
1017
|
expect(result).toEqual({
|
|
1018
1018
|
encryptedName: 'encryptedNodeName',
|
|
@@ -1056,9 +1056,9 @@ describe('nodesCryptoService', () => {
|
|
|
1056
1056
|
addressKey: 'addressKey' as any,
|
|
1057
1057
|
};
|
|
1058
1058
|
|
|
1059
|
-
await expect(
|
|
1060
|
-
|
|
1061
|
-
);
|
|
1059
|
+
await expect(
|
|
1060
|
+
cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address),
|
|
1061
|
+
).rejects.toThrow('Moving item to a non-folder is not allowed');
|
|
1062
1062
|
});
|
|
1063
1063
|
|
|
1064
1064
|
it('should throw error when node has invalid name', async () => {
|
|
@@ -1079,9 +1079,9 @@ describe('nodesCryptoService', () => {
|
|
|
1079
1079
|
addressKey: 'addressKey' as any,
|
|
1080
1080
|
};
|
|
1081
1081
|
|
|
1082
|
-
await expect(
|
|
1083
|
-
|
|
1084
|
-
);
|
|
1082
|
+
await expect(
|
|
1083
|
+
cryptoService.encryptNodeWithNewParent(node, keys as any, parentKeys, address),
|
|
1084
|
+
).rejects.toThrow('Cannot move item without a valid name, please rename the item first');
|
|
1085
1085
|
});
|
|
1086
1086
|
});
|
|
1087
1087
|
});
|
|
@@ -601,7 +601,7 @@ export class NodesCryptoService {
|
|
|
601
601
|
};
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
async
|
|
604
|
+
async encryptNodeWithNewParent(
|
|
605
605
|
node: Pick<DecryptedNode, 'name'>,
|
|
606
606
|
keys: { passphrase: string; passphraseSessionKey: SessionKey; nameSessionKey: SessionKey },
|
|
607
607
|
parentKeys: { key: PrivateKey; hashKey: Uint8Array },
|
|
@@ -52,7 +52,7 @@ describe('nodesModules integration tests', () => {
|
|
|
52
52
|
driveCrypto = {};
|
|
53
53
|
// @ts-expect-error No need to implement all methods for mocking
|
|
54
54
|
sharesService = {
|
|
55
|
-
|
|
55
|
+
getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
nodesModule = initNodesModule(
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
RevisionState,
|
|
13
13
|
} from '../../interface';
|
|
14
14
|
|
|
15
|
+
export type FilterOptions = {
|
|
16
|
+
type?: NodeType;
|
|
17
|
+
};
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* Internal common node interface for both encrypted or decrypted node.
|
|
17
21
|
*/
|
|
@@ -172,7 +176,7 @@ export interface DecryptedRevision extends Revision {
|
|
|
172
176
|
* Interface describing the dependencies to the shares module.
|
|
173
177
|
*/
|
|
174
178
|
export interface SharesService {
|
|
175
|
-
|
|
179
|
+
getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
|
|
176
180
|
getSharePrivateKey(shareId: string): Promise<PrivateKey>;
|
|
177
181
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
178
182
|
email: string;
|
|
@@ -46,7 +46,7 @@ describe('nodesAccess', () => {
|
|
|
46
46
|
};
|
|
47
47
|
// @ts-expect-error No need to implement all methods for mocking
|
|
48
48
|
shareService = {
|
|
49
|
-
|
|
49
|
+
getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
50
50
|
getSharePrivateKey: jest.fn(),
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -209,7 +209,12 @@ describe('nodesAccess', () => {
|
|
|
209
209
|
|
|
210
210
|
const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
|
|
211
211
|
expect(result).toMatchObject([node1, node4, node2, node3]);
|
|
212
|
-
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
212
|
+
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
213
|
+
[node2.uid, node3.uid],
|
|
214
|
+
'volumeId',
|
|
215
|
+
undefined, // filterOptions
|
|
216
|
+
undefined, // signal
|
|
217
|
+
);
|
|
213
218
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(2);
|
|
214
219
|
expect(cache.setNode).toHaveBeenCalledTimes(2);
|
|
215
220
|
expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(2);
|
|
@@ -226,7 +231,11 @@ describe('nodesAccess', () => {
|
|
|
226
231
|
|
|
227
232
|
const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
|
|
228
233
|
expect(result).toMatchObject([node1, node2, node3, node4]);
|
|
229
|
-
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
234
|
+
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
235
|
+
'volumeId~parentNodeid',
|
|
236
|
+
false, // onlyFolders
|
|
237
|
+
undefined, // signal
|
|
238
|
+
);
|
|
230
239
|
expect(apiService.iterateNodes).not.toHaveBeenCalled();
|
|
231
240
|
expect(cache.setFolderChildrenLoaded).toHaveBeenCalledWith('volumeId~parentNodeid');
|
|
232
241
|
});
|
|
@@ -247,11 +256,16 @@ describe('nodesAccess', () => {
|
|
|
247
256
|
|
|
248
257
|
const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
|
|
249
258
|
expect(result).toMatchObject([node1, node2, node3, node4]);
|
|
250
|
-
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
259
|
+
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith(
|
|
260
|
+
'volumeId~parentNodeid',
|
|
261
|
+
false, // onlyFolders
|
|
262
|
+
undefined, // signal
|
|
263
|
+
);
|
|
251
264
|
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
252
265
|
['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'],
|
|
253
266
|
'volumeId',
|
|
254
|
-
undefined,
|
|
267
|
+
undefined, // filterOptions
|
|
268
|
+
undefined, // signal
|
|
255
269
|
);
|
|
256
270
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
|
|
257
271
|
expect(cache.setNode).toHaveBeenCalledTimes(4);
|
|
@@ -320,6 +334,50 @@ describe('nodesAccess', () => {
|
|
|
320
334
|
expect(error.cause).toEqual([new DecryptionError('Decryption failed')]);
|
|
321
335
|
}
|
|
322
336
|
});
|
|
337
|
+
|
|
338
|
+
it('should return only filtered nodes from cache', async () => {
|
|
339
|
+
cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(true);
|
|
340
|
+
cache.iterateChildren = jest.fn().mockImplementation(async function* () {
|
|
341
|
+
yield { ok: true, node: { ...node1, type: NodeType.Folder } };
|
|
342
|
+
yield { ok: true, node: { ...node2, type: NodeType.Folder } };
|
|
343
|
+
yield { ok: true, node: { ...node3, type: NodeType.File } };
|
|
344
|
+
yield { ok: true, node: { ...node4, type: NodeType.File } };
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const result = await Array.fromAsync(
|
|
348
|
+
access.iterateFolderChildren('volumeId~parentNodeid', { type: NodeType.Folder }),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(result).toMatchObject([node1, node2]);
|
|
352
|
+
expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it.only('should return only filtered nodes from API', async () => {
|
|
356
|
+
cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(false);
|
|
357
|
+
cache.getNode = jest.fn().mockImplementation((uid: string) => {
|
|
358
|
+
if (uid === parentNode.uid) {
|
|
359
|
+
return parentNode;
|
|
360
|
+
}
|
|
361
|
+
throw new Error('Entity not found');
|
|
362
|
+
});
|
|
363
|
+
apiService.iterateChildrenNodeUids = jest.fn().mockImplementation(async function* () {
|
|
364
|
+
yield 'volumeId~node1';
|
|
365
|
+
yield 'volumeId~node2';
|
|
366
|
+
yield 'volumeId~node3';
|
|
367
|
+
yield 'volumeId~node4';
|
|
368
|
+
});
|
|
369
|
+
apiService.iterateNodes = jest.fn().mockImplementation(async function* () {
|
|
370
|
+
yield { ...node1, parentUid: 'volumeId~parentNodeId', type: NodeType.Folder };
|
|
371
|
+
yield { ...node2, parentUid: 'volumeId~parentNodeId', type: NodeType.Folder };
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const result = await Array.fromAsync(
|
|
375
|
+
access.iterateFolderChildren('volumeId~parentNodeid', { type: NodeType.Folder }),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(result).toMatchObject([node1, node2]);
|
|
379
|
+
expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
323
381
|
});
|
|
324
382
|
|
|
325
383
|
describe('iterateTrashedNodes', () => {
|
|
@@ -330,7 +388,7 @@ describe('nodesAccess', () => {
|
|
|
330
388
|
const node4 = { uid: 'volumeId~node4', isStale: false } as DecryptedNode;
|
|
331
389
|
|
|
332
390
|
beforeEach(() => {
|
|
333
|
-
shareService.
|
|
391
|
+
shareService.getOwnVolumeIDs = jest.fn().mockResolvedValue({ volumeId });
|
|
334
392
|
apiService.iterateTrashedNodeUids = jest.fn().mockImplementation(async function* () {
|
|
335
393
|
yield node1.uid;
|
|
336
394
|
yield node2.uid;
|
|
@@ -359,7 +417,8 @@ describe('nodesAccess', () => {
|
|
|
359
417
|
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
360
418
|
['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'],
|
|
361
419
|
volumeId,
|
|
362
|
-
undefined,
|
|
420
|
+
undefined, // filterOptions
|
|
421
|
+
undefined, // signal
|
|
363
422
|
);
|
|
364
423
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
|
|
365
424
|
expect(cache.setNode).toHaveBeenCalledTimes(4);
|
|
@@ -417,7 +476,8 @@ describe('nodesAccess', () => {
|
|
|
417
476
|
expect(apiService.iterateNodes).toHaveBeenCalledWith(
|
|
418
477
|
['volumeId~node2', 'volumeId~node3'],
|
|
419
478
|
'volumeId',
|
|
420
|
-
undefined,
|
|
479
|
+
undefined, // filterOptions
|
|
480
|
+
undefined, // signal
|
|
421
481
|
);
|
|
422
482
|
});
|
|
423
483
|
|
|
@@ -12,7 +12,14 @@ import { NodesCache } from './cache';
|
|
|
12
12
|
import { NodesCryptoCache } from './cryptoCache';
|
|
13
13
|
import { NodesCryptoService } from './cryptoService';
|
|
14
14
|
import { parseFileExtendedAttributes, parseFolderExtendedAttributes } from './extendedAttributes';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
SharesService,
|
|
17
|
+
EncryptedNode,
|
|
18
|
+
DecryptedUnparsedNode,
|
|
19
|
+
DecryptedNode,
|
|
20
|
+
DecryptedNodeKeys,
|
|
21
|
+
FilterOptions,
|
|
22
|
+
} from './interface';
|
|
16
23
|
import { validateNodeName } from './validations';
|
|
17
24
|
import { isProtonDocument, isProtonSheet } from './mediaTypes';
|
|
18
25
|
|
|
@@ -49,8 +56,8 @@ export class NodesAccess {
|
|
|
49
56
|
this.shareService = shareService;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
async
|
|
53
|
-
const { volumeId, rootNodeId } = await this.shareService.
|
|
59
|
+
async getVolumeRootFolder() {
|
|
60
|
+
const { volumeId, rootNodeId } = await this.shareService.getOwnVolumeIDs();
|
|
54
61
|
const nodeUid = makeNodeUid(volumeId, rootNodeId);
|
|
55
62
|
return this.getNode(nodeUid);
|
|
56
63
|
}
|
|
@@ -71,12 +78,16 @@ export class NodesAccess {
|
|
|
71
78
|
return node;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
async *iterateFolderChildren(
|
|
81
|
+
async *iterateFolderChildren(
|
|
82
|
+
parentNodeUid: string,
|
|
83
|
+
filterOptions?: FilterOptions,
|
|
84
|
+
signal?: AbortSignal,
|
|
85
|
+
): AsyncGenerator<DecryptedNode> {
|
|
75
86
|
// Ensure the parent is loaded and up-to-date.
|
|
76
87
|
const parentNode = await this.getNode(parentNodeUid);
|
|
77
88
|
|
|
78
89
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
79
|
-
iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal),
|
|
90
|
+
iterateItems: (nodeUids) => this.loadNodes(nodeUids, filterOptions, signal),
|
|
80
91
|
batchSize: BATCH_LOADING_SIZE,
|
|
81
92
|
});
|
|
82
93
|
|
|
@@ -84,6 +95,9 @@ export class NodesAccess {
|
|
|
84
95
|
if (areChildrenCached) {
|
|
85
96
|
for await (const node of this.cache.iterateChildren(parentNodeUid)) {
|
|
86
97
|
if (node.ok && !node.node.isStale) {
|
|
98
|
+
if (filterOptions?.type && node.node.type !== filterOptions.type) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
87
101
|
yield node.node;
|
|
88
102
|
} else {
|
|
89
103
|
yield* batchLoading.load(node.uid);
|
|
@@ -94,13 +108,17 @@ export class NodesAccess {
|
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
this.logger.debug(`Folder ${parentNodeUid} children are not cached`);
|
|
97
|
-
|
|
111
|
+
const onlyFolders = filterOptions?.type === NodeType.Folder;
|
|
112
|
+
for await (const nodeUid of this.apiService.iterateChildrenNodeUids(parentNode.uid, onlyFolders, signal)) {
|
|
98
113
|
let node;
|
|
99
114
|
try {
|
|
100
115
|
node = await this.cache.getNode(nodeUid);
|
|
101
116
|
} catch {}
|
|
102
117
|
|
|
103
118
|
if (node && !node.isStale) {
|
|
119
|
+
if (filterOptions?.type && node.type !== filterOptions.type) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
104
122
|
yield node;
|
|
105
123
|
} else {
|
|
106
124
|
this.logger.debug(`Node ${nodeUid} from ${parentNodeUid} is ${node?.isStale ? 'stale' : 'not cached'}`);
|
|
@@ -108,14 +126,18 @@ export class NodesAccess {
|
|
|
108
126
|
}
|
|
109
127
|
}
|
|
110
128
|
yield* batchLoading.loadRest();
|
|
111
|
-
|
|
129
|
+
|
|
130
|
+
// If some nodes were filtered out, we don't have the folder fully loaded.
|
|
131
|
+
if (!filterOptions) {
|
|
132
|
+
await this.cache.setFolderChildrenLoaded(parentNodeUid);
|
|
133
|
+
}
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
// Improvement requested: keep status of loaded trash and leverage cache.
|
|
115
137
|
async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
|
|
116
|
-
const { volumeId } = await this.shareService.
|
|
138
|
+
const { volumeId } = await this.shareService.getOwnVolumeIDs();
|
|
117
139
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
118
|
-
iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal),
|
|
140
|
+
iterateItems: (nodeUids) => this.loadNodes(nodeUids, undefined, signal),
|
|
119
141
|
batchSize: BATCH_LOADING_SIZE,
|
|
120
142
|
});
|
|
121
143
|
for await (const nodeUid of this.apiService.iterateTrashedNodeUids(volumeId, signal)) {
|
|
@@ -136,7 +158,7 @@ export class NodesAccess {
|
|
|
136
158
|
|
|
137
159
|
async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode> {
|
|
138
160
|
const batchLoading = new BatchLoading<string, DecryptedNode | MissingNode>({
|
|
139
|
-
iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, signal),
|
|
161
|
+
iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, undefined, signal),
|
|
140
162
|
batchSize: BATCH_LOADING_SIZE,
|
|
141
163
|
});
|
|
142
164
|
for await (const result of this.cache.iterateNodes(nodeUids)) {
|
|
@@ -186,13 +208,17 @@ export class NodesAccess {
|
|
|
186
208
|
}
|
|
187
209
|
|
|
188
210
|
private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
189
|
-
const { volumeId: ownVolumeId } = await this.shareService.
|
|
211
|
+
const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
|
|
190
212
|
const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
|
|
191
213
|
return this.decryptNode(encryptedNode);
|
|
192
214
|
}
|
|
193
215
|
|
|
194
|
-
private async *loadNodes(
|
|
195
|
-
|
|
216
|
+
private async *loadNodes(
|
|
217
|
+
nodeUids: string[],
|
|
218
|
+
filterOptions?: FilterOptions,
|
|
219
|
+
signal?: AbortSignal,
|
|
220
|
+
): AsyncGenerator<DecryptedNode> {
|
|
221
|
+
for await (const result of this.loadNodesWithMissingReport(nodeUids, filterOptions, signal)) {
|
|
196
222
|
if ('missingUid' in result) {
|
|
197
223
|
continue;
|
|
198
224
|
}
|
|
@@ -202,14 +228,15 @@ export class NodesAccess {
|
|
|
202
228
|
|
|
203
229
|
private async *loadNodesWithMissingReport(
|
|
204
230
|
nodeUids: string[],
|
|
231
|
+
filterOptions?: FilterOptions,
|
|
205
232
|
signal?: AbortSignal,
|
|
206
233
|
): AsyncGenerator<DecryptedNode | MissingNode> {
|
|
207
234
|
const returnedNodeUids: string[] = [];
|
|
208
235
|
const errors = [];
|
|
209
236
|
|
|
210
|
-
const { volumeId: ownVolumeId } = await this.shareService.
|
|
237
|
+
const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
|
|
211
238
|
|
|
212
|
-
const encryptedNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, signal);
|
|
239
|
+
const encryptedNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
|
|
213
240
|
const decryptNodeMapper = async (encryptedNode: EncryptedNode): Promise<Result<DecryptedNode, unknown>> => {
|
|
214
241
|
returnedNodeUids.push(encryptedNode.uid);
|
|
215
242
|
try {
|