@protontech/drive-sdk 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/interface/nodes.d.ts +4 -0
- package/dist/interface/nodes.js.map +1 -1
- 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 +54 -13
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +35 -2
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +1 -0
- package/dist/internal/nodes/cache.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/extendedAttributes.d.ts +2 -2
- package/dist/internal/nodes/extendedAttributes.js +15 -11
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +19 -1
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/index.test.js +2 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +5 -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 +87 -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 +31 -4
- package/dist/internal/photos/index.js +57 -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/photos/upload.d.ts +59 -0
- package/dist/internal/photos/upload.js +104 -0
- package/dist/internal/photos/upload.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/internal/sharingPublic/apiService.js +2 -0
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/upload/apiService.d.ts +2 -2
- package/dist/internal/upload/apiService.js +1 -1
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +2 -2
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +1 -0
- package/dist/internal/upload/fileUploader.js +3 -0
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +3 -0
- package/dist/internal/upload/manager.d.ts +12 -11
- package/dist/internal/upload/manager.js +8 -2
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +8 -0
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +34 -24
- package/dist/internal/upload/streamUploader.js +7 -4
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +1 -1
- package/dist/internal/upload/streamUploader.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 +102 -12
- package/dist/protonDrivePhotosClient.js +149 -29
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +1 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/interface/nodes.ts +4 -0
- 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 +58 -1
- package/src/internal/nodes/apiService.ts +104 -17
- package/src/internal/nodes/cache.test.ts +1 -0
- 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/extendedAttributes.test.ts +23 -1
- package/src/internal/nodes/extendedAttributes.ts +26 -18
- package/src/internal/nodes/index.test.ts +2 -1
- package/src/internal/nodes/interface.ts +6 -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 +101 -13
- package/src/internal/photos/albums.ts +31 -12
- package/src/internal/photos/apiService.ts +159 -4
- package/src/internal/photos/index.ts +116 -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/photos/upload.ts +209 -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/internal/sharingPublic/apiService.ts +2 -0
- package/src/internal/upload/apiService.ts +3 -3
- package/src/internal/upload/cryptoService.ts +2 -2
- package/src/internal/upload/fileUploader.ts +12 -0
- package/src/internal/upload/interface.ts +3 -0
- package/src/internal/upload/manager.test.ts +8 -0
- package/src/internal/upload/manager.ts +20 -10
- package/src/internal/upload/streamUploader.test.ts +17 -12
- package/src/internal/upload/streamUploader.ts +35 -27
- package/src/protonDriveClient.ts +33 -4
- package/src/protonDrivePhotosClient.ts +251 -32
- package/src/transformers.ts +2 -0
- 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);
|
|
@@ -491,6 +577,7 @@ function linkToEncryptedNode(
|
|
|
491
577
|
// Sharing node metadata
|
|
492
578
|
shareId: link.Sharing?.ShareID || undefined,
|
|
493
579
|
isShared: !!link.Sharing,
|
|
580
|
+
isSharedPublicly: !!link.Sharing?.ShareURLID,
|
|
494
581
|
directRole: isAdmin ? MemberRole.Admin : membershipRole,
|
|
495
582
|
membership: link.Membership
|
|
496
583
|
? {
|
|
@@ -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 },
|
|
@@ -50,7 +50,7 @@ describe('extended attrbiutes', () => {
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
describe('should generate file attributes', () => {
|
|
53
|
+
describe('should generate file attributes without additional metadata', () => {
|
|
54
54
|
const testCases: [object, string | undefined][] = [
|
|
55
55
|
[{}, undefined],
|
|
56
56
|
[
|
|
@@ -82,6 +82,28 @@ describe('extended attrbiutes', () => {
|
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
describe('should generate file attributes with additional metadata', () => {
|
|
86
|
+
const testCases: [object, string | undefined][] = [
|
|
87
|
+
[{}, '{"Media":{"Width":100,"Height":100}}'],
|
|
88
|
+
[{ size: undefined }, '{"Media":{"Width":100,"Height":100}}'],
|
|
89
|
+
[{ size: 123 }, '{"Common":{"Size":123},"Media":{"Width":100,"Height":100}}'],
|
|
90
|
+
];
|
|
91
|
+
testCases.forEach(([input, expectedAttributes]) => {
|
|
92
|
+
it(`should generate ${input}`, () => {
|
|
93
|
+
const output = generateFileExtendedAttributes(input, { Media: { Width: 100, Height: 100 } });
|
|
94
|
+
expect(output).toBe(expectedAttributes);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('should throw an error if additional metadata contains common attributes', () => {
|
|
100
|
+
it('should throw an error', () => {
|
|
101
|
+
expect(() => generateFileExtendedAttributes({ size: 123 }, { Common: { Hello: 'World' } })).toThrow(
|
|
102
|
+
'Common attributes are not allowed in additional metadata',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
85
107
|
describe('should parses file attributes', () => {
|
|
86
108
|
const testCases: [Date, string, FileExtendedAttributesParsed][] = [
|
|
87
109
|
[new Date('2025-01-01'), '', {}],
|
|
@@ -83,34 +83,42 @@ export function parseFolderExtendedAttributes(logger: Logger, extendedAttributes
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function generateFileExtendedAttributes(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
86
|
+
export function generateFileExtendedAttributes(
|
|
87
|
+
common: {
|
|
88
|
+
modificationTime?: Date;
|
|
89
|
+
size?: number;
|
|
90
|
+
blockSizes?: number[];
|
|
91
|
+
digests?: {
|
|
92
|
+
sha1?: string;
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
additionalMetadata?: object,
|
|
96
|
+
): string | undefined {
|
|
97
|
+
if (additionalMetadata && 'Common' in additionalMetadata) {
|
|
98
|
+
throw new Error('Common attributes are not allowed in additional metadata');
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
const commonAttributes: FileExtendedAttributesSchema['Common'] = {};
|
|
95
|
-
if (
|
|
96
|
-
commonAttributes.ModificationTime = dateToIsoString(
|
|
102
|
+
if (common.modificationTime) {
|
|
103
|
+
commonAttributes.ModificationTime = dateToIsoString(common.modificationTime);
|
|
97
104
|
}
|
|
98
|
-
if (
|
|
99
|
-
commonAttributes.Size =
|
|
105
|
+
if (common.size !== undefined) {
|
|
106
|
+
commonAttributes.Size = common.size;
|
|
100
107
|
}
|
|
101
|
-
if (
|
|
102
|
-
commonAttributes.BlockSizes =
|
|
108
|
+
if (common.blockSizes?.length) {
|
|
109
|
+
commonAttributes.BlockSizes = common.blockSizes;
|
|
103
110
|
}
|
|
104
|
-
if (
|
|
111
|
+
if (common.digests?.sha1) {
|
|
105
112
|
commonAttributes.Digests = {
|
|
106
|
-
SHA1:
|
|
113
|
+
SHA1: common.digests.sha1,
|
|
107
114
|
};
|
|
108
115
|
}
|
|
109
|
-
if (!Object.keys(commonAttributes).length) {
|
|
116
|
+
if (!Object.keys(commonAttributes).length && !additionalMetadata) {
|
|
110
117
|
return undefined;
|
|
111
118
|
}
|
|
112
119
|
return JSON.stringify({
|
|
113
|
-
Common: commonAttributes,
|
|
120
|
+
...(Object.keys(commonAttributes).length ? { Common: commonAttributes } : {}),
|
|
121
|
+
...(additionalMetadata ? { ...additionalMetadata } : {}),
|
|
114
122
|
});
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -24,6 +24,7 @@ function generateNode(uid: string, parentUid = 'volumeId~root', params: Partial<
|
|
|
24
24
|
type: NodeType.File,
|
|
25
25
|
mediaType: 'text',
|
|
26
26
|
isShared: false,
|
|
27
|
+
isSharedPublicly: false,
|
|
27
28
|
creationTime: new Date(),
|
|
28
29
|
trashTime: undefined,
|
|
29
30
|
isStale: false,
|
|
@@ -52,7 +53,7 @@ describe('nodesModules integration tests', () => {
|
|
|
52
53
|
driveCrypto = {};
|
|
53
54
|
// @ts-expect-error No need to implement all methods for mocking
|
|
54
55
|
sharesService = {
|
|
55
|
-
|
|
56
|
+
getOwnVolumeIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
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
|
*/
|
|
@@ -34,6 +38,7 @@ interface BaseNode {
|
|
|
34
38
|
// Share node metadata
|
|
35
39
|
shareId?: string;
|
|
36
40
|
isShared: boolean;
|
|
41
|
+
isSharedPublicly: boolean;
|
|
37
42
|
directRole: MemberRole;
|
|
38
43
|
membership?: {
|
|
39
44
|
role: MemberRole;
|
|
@@ -172,7 +177,7 @@ export interface DecryptedRevision extends Revision {
|
|
|
172
177
|
* Interface describing the dependencies to the shares module.
|
|
173
178
|
*/
|
|
174
179
|
export interface SharesService {
|
|
175
|
-
|
|
180
|
+
getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
|
|
176
181
|
getSharePrivateKey(shareId: string): Promise<PrivateKey>;
|
|
177
182
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
178
183
|
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
|
|