@protontech/drive-sdk 0.7.0 → 0.7.2
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/index.d.ts +1 -0
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +14 -10
- package/dist/interface/nodes.js +5 -8
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/photos.d.ts +62 -0
- package/dist/interface/photos.js +3 -0
- package/dist/interface/photos.js.map +1 -0
- package/dist/internal/apiService/driveTypes.d.ts +1294 -517
- package/dist/internal/errors.d.ts +1 -0
- package/dist/internal/errors.js +4 -0
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +60 -9
- package/dist/internal/nodes/apiService.js +125 -81
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +2 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.d.ts +16 -8
- package/dist/internal/nodes/cache.js +19 -5
- package/dist/internal/nodes/cache.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 +4 -4
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +3 -3
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/events.d.ts +2 -2
- package/dist/internal/nodes/events.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -0
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +1 -0
- package/dist/internal/nodes/nodesAccess.d.ts +29 -20
- package/dist/internal/nodes/nodesAccess.js +41 -29
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +32 -12
- package/dist/internal/nodes/nodesManagement.js +30 -13
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +39 -4
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/nodes/nodesRevisions.d.ts +2 -2
- package/dist/internal/nodes/nodesRevisions.js.map +1 -1
- package/dist/internal/photos/albums.d.ts +2 -2
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/index.d.ts +19 -3
- package/dist/internal/photos/index.js +38 -8
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +18 -9
- package/dist/internal/photos/nodes.d.ts +57 -0
- package/dist/internal/photos/nodes.js +166 -0
- package/dist/internal/photos/nodes.js.map +1 -0
- package/dist/internal/photos/nodes.test.d.ts +1 -0
- package/dist/internal/photos/nodes.test.js +233 -0
- package/dist/internal/photos/nodes.test.js.map +1 -0
- package/dist/internal/photos/timeline.d.ts +2 -2
- package/dist/internal/photos/timeline.js.map +1 -1
- package/dist/internal/photos/timeline.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +10 -1
- package/dist/protonDriveClient.js +18 -3
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +12 -8
- package/dist/protonDrivePhotosClient.js +15 -9
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +7 -1
- package/dist/protonDrivePublicLinkClient.js +9 -0
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.d.ts +7 -2
- package/dist/transformers.js +37 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/interface/index.ts +1 -0
- package/src/interface/nodes.ts +14 -11
- package/src/interface/photos.ts +67 -0
- package/src/internal/apiService/driveTypes.ts +1294 -517
- package/src/internal/errors.ts +4 -0
- package/src/internal/nodes/apiService.test.ts +2 -0
- package/src/internal/nodes/apiService.ts +187 -114
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/cache.ts +32 -13
- package/src/internal/nodes/cryptoService.test.ts +13 -3
- package/src/internal/nodes/cryptoService.ts +4 -4
- package/src/internal/nodes/events.ts +2 -2
- package/src/internal/nodes/index.test.ts +1 -0
- package/src/internal/nodes/interface.ts +1 -0
- package/src/internal/nodes/nodesAccess.ts +82 -54
- package/src/internal/nodes/nodesManagement.test.ts +48 -4
- package/src/internal/nodes/nodesManagement.ts +76 -26
- package/src/internal/nodes/nodesRevisions.ts +3 -3
- package/src/internal/photos/albums.ts +2 -2
- package/src/internal/photos/index.ts +45 -3
- package/src/internal/photos/interface.ts +21 -9
- package/src/internal/photos/nodes.test.ts +258 -0
- package/src/internal/photos/nodes.ts +234 -0
- package/src/internal/photos/timeline.test.ts +2 -2
- package/src/internal/photos/timeline.ts +2 -2
- package/src/protonDriveClient.ts +20 -3
- package/src/protonDrivePhotosClient.ts +31 -23
- package/src/protonDrivePublicLinkClient.ts +11 -0
- package/src/transformers.ts +49 -2
|
@@ -16,8 +16,8 @@ import { asyncIteratorMap } from '../asyncIteratorMap';
|
|
|
16
16
|
import { getErrorMessage } from '../errors';
|
|
17
17
|
import { BatchLoading } from '../batchLoading';
|
|
18
18
|
import { makeNodeUid, splitNodeUid } from '../uids';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { NodeAPIServiceBase } from './apiService';
|
|
20
|
+
import { NodesCacheBase } from './cache';
|
|
21
21
|
import { NodesCryptoCache } from './cryptoCache';
|
|
22
22
|
import { NodesCryptoService } from './cryptoService';
|
|
23
23
|
import { NodesDebouncer } from './debouncer';
|
|
@@ -50,17 +50,21 @@ const DECRYPTION_CONCURRENCY = 30;
|
|
|
50
50
|
* The node access module is responsible for fetching, decrypting and caching
|
|
51
51
|
* nodes metadata.
|
|
52
52
|
*/
|
|
53
|
-
export class
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
export abstract class NodesAccessBase<
|
|
54
|
+
TEncryptedNode extends EncryptedNode = EncryptedNode,
|
|
55
|
+
TDecryptedNode extends DecryptedNode = DecryptedNode,
|
|
56
|
+
TCryptoService extends NodesCryptoService = NodesCryptoService,
|
|
57
|
+
> {
|
|
58
|
+
protected logger: Logger;
|
|
59
|
+
protected debouncer: NodesDebouncer;
|
|
56
60
|
|
|
57
61
|
constructor(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
protected telemetry: ProtonDriveTelemetry,
|
|
63
|
+
protected apiService: NodeAPIServiceBase<TEncryptedNode>,
|
|
64
|
+
protected cache: NodesCacheBase<TDecryptedNode>,
|
|
65
|
+
protected cryptoCache: NodesCryptoCache,
|
|
66
|
+
protected cryptoService: TCryptoService,
|
|
67
|
+
protected shareService: Pick<
|
|
64
68
|
SharesService,
|
|
65
69
|
'getRootIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
|
|
66
70
|
>,
|
|
@@ -80,7 +84,7 @@ export class NodesAccess {
|
|
|
80
84
|
return this.getNode(nodeUid);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
async getNode(nodeUid: string): Promise<
|
|
87
|
+
async getNode(nodeUid: string): Promise<TDecryptedNode> {
|
|
84
88
|
let cachedNode;
|
|
85
89
|
try {
|
|
86
90
|
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
@@ -101,11 +105,11 @@ export class NodesAccess {
|
|
|
101
105
|
parentNodeUid: string,
|
|
102
106
|
filterOptions?: FilterOptions,
|
|
103
107
|
signal?: AbortSignal,
|
|
104
|
-
): AsyncGenerator<
|
|
108
|
+
): AsyncGenerator<TDecryptedNode> {
|
|
105
109
|
// Ensure the parent is loaded and up-to-date.
|
|
106
110
|
const parentNode = await this.getNode(parentNodeUid);
|
|
107
111
|
|
|
108
|
-
const batchLoading = new BatchLoading<string,
|
|
112
|
+
const batchLoading = new BatchLoading<string, TDecryptedNode>({
|
|
109
113
|
iterateItems: (nodeUids) => this.loadNodes(nodeUids, filterOptions, signal),
|
|
110
114
|
batchSize: BATCH_LOADING_SIZE,
|
|
111
115
|
});
|
|
@@ -154,9 +158,9 @@ export class NodesAccess {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
// Improvement requested: keep status of loaded trash and leverage cache.
|
|
157
|
-
async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<
|
|
161
|
+
async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<TDecryptedNode> {
|
|
158
162
|
const { volumeId } = await this.shareService.getRootIDs();
|
|
159
|
-
const batchLoading = new BatchLoading<string,
|
|
163
|
+
const batchLoading = new BatchLoading<string, TDecryptedNode>({
|
|
160
164
|
iterateItems: (nodeUids) => this.loadNodes(nodeUids, undefined, signal),
|
|
161
165
|
batchSize: BATCH_LOADING_SIZE,
|
|
162
166
|
});
|
|
@@ -177,8 +181,8 @@ export class NodesAccess {
|
|
|
177
181
|
yield* batchLoading.loadRest();
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<
|
|
181
|
-
const batchLoading = new BatchLoading<string,
|
|
184
|
+
async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<TDecryptedNode | MissingNode> {
|
|
185
|
+
const batchLoading = new BatchLoading<string, TDecryptedNode | MissingNode>({
|
|
182
186
|
iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, undefined, signal),
|
|
183
187
|
batchSize: BATCH_LOADING_SIZE,
|
|
184
188
|
});
|
|
@@ -228,7 +232,7 @@ export class NodesAccess {
|
|
|
228
232
|
await this.cache.removeNodes([nodeUid]);
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
private async loadNode(nodeUid: string): Promise<{ node:
|
|
235
|
+
private async loadNode(nodeUid: string): Promise<{ node: TDecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
232
236
|
this.debouncer.loadingNode(nodeUid);
|
|
233
237
|
try {
|
|
234
238
|
const { volumeId: ownVolumeId } = await this.shareService.getRootIDs();
|
|
@@ -243,7 +247,7 @@ export class NodesAccess {
|
|
|
243
247
|
nodeUids: string[],
|
|
244
248
|
filterOptions?: FilterOptions,
|
|
245
249
|
signal?: AbortSignal,
|
|
246
|
-
): AsyncGenerator<
|
|
250
|
+
): AsyncGenerator<TDecryptedNode> {
|
|
247
251
|
for await (const result of this.loadNodesWithMissingReport(nodeUids, filterOptions, signal)) {
|
|
248
252
|
if ('missingUid' in result) {
|
|
249
253
|
continue;
|
|
@@ -256,7 +260,7 @@ export class NodesAccess {
|
|
|
256
260
|
nodeUids: string[],
|
|
257
261
|
filterOptions?: FilterOptions,
|
|
258
262
|
signal?: AbortSignal,
|
|
259
|
-
): AsyncGenerator<
|
|
263
|
+
): AsyncGenerator<TDecryptedNode | MissingNode> {
|
|
260
264
|
const returnedNodeUids: string[] = [];
|
|
261
265
|
const errors = [];
|
|
262
266
|
|
|
@@ -264,13 +268,13 @@ export class NodesAccess {
|
|
|
264
268
|
|
|
265
269
|
const apiNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
|
|
266
270
|
|
|
267
|
-
const debouncedNodeMapper = async (encryptedNode:
|
|
271
|
+
const debouncedNodeMapper = async (encryptedNode: TEncryptedNode): Promise<TEncryptedNode> => {
|
|
268
272
|
this.debouncer.loadingNode(encryptedNode.uid);
|
|
269
273
|
return encryptedNode;
|
|
270
274
|
};
|
|
271
275
|
const encryptedNodesIterator = asyncIteratorMap(apiNodesIterator, debouncedNodeMapper, 1);
|
|
272
276
|
|
|
273
|
-
const decryptNodeMapper = async (encryptedNode:
|
|
277
|
+
const decryptNodeMapper = async (encryptedNode: TEncryptedNode): Promise<Result<TDecryptedNode, unknown>> => {
|
|
274
278
|
returnedNodeUids.push(encryptedNode.uid);
|
|
275
279
|
try {
|
|
276
280
|
const { node } = await this.decryptNode(encryptedNode);
|
|
@@ -310,8 +314,8 @@ export class NodesAccess {
|
|
|
310
314
|
}
|
|
311
315
|
|
|
312
316
|
private async decryptNode(
|
|
313
|
-
encryptedNode:
|
|
314
|
-
): Promise<{ node:
|
|
317
|
+
encryptedNode: TEncryptedNode,
|
|
318
|
+
): Promise<{ node: TDecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
315
319
|
let parentKey;
|
|
316
320
|
try {
|
|
317
321
|
const parentKeys = await this.getParentKeys(encryptedNode);
|
|
@@ -319,38 +323,14 @@ export class NodesAccess {
|
|
|
319
323
|
} catch (error: unknown) {
|
|
320
324
|
if (error instanceof DecryptionError) {
|
|
321
325
|
return {
|
|
322
|
-
node:
|
|
323
|
-
...encryptedNode,
|
|
324
|
-
isStale: false,
|
|
325
|
-
name: resultError(error),
|
|
326
|
-
keyAuthor: resultError({
|
|
327
|
-
claimedAuthor: encryptedNode.encryptedCrypto.signatureEmail,
|
|
328
|
-
error: getErrorMessage(error),
|
|
329
|
-
}),
|
|
330
|
-
nameAuthor: resultError({
|
|
331
|
-
claimedAuthor: encryptedNode.encryptedCrypto.nameSignatureEmail,
|
|
332
|
-
error: getErrorMessage(error),
|
|
333
|
-
}),
|
|
334
|
-
membership: encryptedNode.membership
|
|
335
|
-
? {
|
|
336
|
-
role: encryptedNode.membership.role,
|
|
337
|
-
inviteTime: encryptedNode.membership.inviteTime,
|
|
338
|
-
sharedBy: resultError({
|
|
339
|
-
claimedAuthor: encryptedNode.encryptedCrypto.membership?.inviterEmail,
|
|
340
|
-
error: getErrorMessage(error),
|
|
341
|
-
}),
|
|
342
|
-
}
|
|
343
|
-
: undefined,
|
|
344
|
-
errors: [error],
|
|
345
|
-
treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
|
|
346
|
-
},
|
|
326
|
+
node: this.getDegradedUndecryptableNode(encryptedNode, error),
|
|
347
327
|
};
|
|
348
328
|
}
|
|
349
329
|
throw error;
|
|
350
330
|
}
|
|
351
331
|
|
|
352
332
|
const { node: unparsedNode, keys } = await this.cryptoService.decryptNode(encryptedNode, parentKey);
|
|
353
|
-
const node =
|
|
333
|
+
const node = this.parseNode(unparsedNode);
|
|
354
334
|
try {
|
|
355
335
|
await this.cache.setNode(node);
|
|
356
336
|
} catch (error: unknown) {
|
|
@@ -367,8 +347,45 @@ export class NodesAccess {
|
|
|
367
347
|
return { node, keys };
|
|
368
348
|
}
|
|
369
349
|
|
|
350
|
+
protected abstract getDegradedUndecryptableNode(
|
|
351
|
+
encryptedNode: TEncryptedNode,
|
|
352
|
+
error: DecryptionError,
|
|
353
|
+
): TDecryptedNode;
|
|
354
|
+
|
|
355
|
+
protected getDegradedUndecryptableNodeBase(encryptedNode: EncryptedNode, error: DecryptionError): DecryptedNode {
|
|
356
|
+
return {
|
|
357
|
+
...encryptedNode,
|
|
358
|
+
isStale: false,
|
|
359
|
+
name: resultError(error),
|
|
360
|
+
keyAuthor: resultError({
|
|
361
|
+
claimedAuthor: encryptedNode.encryptedCrypto.signatureEmail,
|
|
362
|
+
error: getErrorMessage(error),
|
|
363
|
+
}),
|
|
364
|
+
nameAuthor: resultError({
|
|
365
|
+
claimedAuthor: encryptedNode.encryptedCrypto.nameSignatureEmail,
|
|
366
|
+
error: getErrorMessage(error),
|
|
367
|
+
}),
|
|
368
|
+
membership: encryptedNode.membership
|
|
369
|
+
? {
|
|
370
|
+
role: encryptedNode.membership.role,
|
|
371
|
+
inviteTime: encryptedNode.membership.inviteTime,
|
|
372
|
+
sharedBy: resultError({
|
|
373
|
+
claimedAuthor: encryptedNode.encryptedCrypto.membership?.inviterEmail,
|
|
374
|
+
error: getErrorMessage(error),
|
|
375
|
+
}),
|
|
376
|
+
}
|
|
377
|
+
: undefined,
|
|
378
|
+
errors: [error],
|
|
379
|
+
treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
protected abstract parseNode(
|
|
384
|
+
unparsedNode: Awaited<ReturnType<TCryptoService['decryptNode']>>['node'],
|
|
385
|
+
): TDecryptedNode;
|
|
386
|
+
|
|
370
387
|
async getParentKeys(
|
|
371
|
-
node: Pick<
|
|
388
|
+
node: Pick<TDecryptedNode, 'uid' | 'parentUid' | 'shareId'>,
|
|
372
389
|
): Promise<Pick<DecryptedNodeKeys, 'key' | 'hashKey'>> {
|
|
373
390
|
if (node.parentUid) {
|
|
374
391
|
try {
|
|
@@ -473,13 +490,23 @@ export class NodesAccess {
|
|
|
473
490
|
return `https://drive.proton.me/${rootNode.shareId}/${type}/${nodeId}`;
|
|
474
491
|
}
|
|
475
492
|
|
|
476
|
-
private async getRootNode(nodeUid: string): Promise<
|
|
493
|
+
private async getRootNode(nodeUid: string): Promise<TDecryptedNode> {
|
|
477
494
|
const node = await this.getNode(nodeUid);
|
|
478
495
|
return node.parentUid ? this.getRootNode(node.parentUid) : node;
|
|
479
496
|
}
|
|
480
497
|
}
|
|
481
498
|
|
|
482
|
-
export
|
|
499
|
+
export class NodesAccess extends NodesAccessBase {
|
|
500
|
+
protected getDegradedUndecryptableNode(encryptedNode: EncryptedNode, error: DecryptionError): DecryptedNode {
|
|
501
|
+
return this.getDegradedUndecryptableNodeBase(encryptedNode, error);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
protected parseNode(unparsedNode: DecryptedUnparsedNode): DecryptedNode {
|
|
505
|
+
return parseNode(this.logger, unparsedNode);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedNode): DecryptedNode {
|
|
483
510
|
let nodeName: Result<string, Error | InvalidNameError> = unparsedNode.name;
|
|
484
511
|
if (unparsedNode.name.ok) {
|
|
485
512
|
try {
|
|
@@ -526,6 +553,7 @@ export async function parseNode(logger: Logger, unparsedNode: DecryptedUnparsedN
|
|
|
526
553
|
const extendedAttributes = unparsedNode.folder?.extendedAttributes
|
|
527
554
|
? parseFolderExtendedAttributes(logger, unparsedNode.folder.extendedAttributes)
|
|
528
555
|
: undefined;
|
|
556
|
+
|
|
529
557
|
return {
|
|
530
558
|
...unparsedNode,
|
|
531
559
|
name: nodeName,
|
|
@@ -6,6 +6,7 @@ import { DecryptedNode } from './interface';
|
|
|
6
6
|
import { NodesManagement } from './nodesManagement';
|
|
7
7
|
import { NodeResult } from '../../interface';
|
|
8
8
|
import { NodeOutOfSyncError } from './errors';
|
|
9
|
+
import { ValidationError } from '../../errors';
|
|
9
10
|
|
|
10
11
|
describe('NodesManagement', () => {
|
|
11
12
|
let apiService: NodeAPIService;
|
|
@@ -191,7 +192,7 @@ describe('NodesManagement', () => {
|
|
|
191
192
|
parentNodeUid: 'newParentNodeUid',
|
|
192
193
|
});
|
|
193
194
|
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
194
|
-
nodes.nodeUid,
|
|
195
|
+
nodes.nodeUid.name,
|
|
195
196
|
expect.objectContaining({
|
|
196
197
|
key: 'nodeUid-key',
|
|
197
198
|
passphrase: 'nodeUid-passphrase',
|
|
@@ -231,7 +232,7 @@ describe('NodesManagement', () => {
|
|
|
231
232
|
const newNode = await management.moveNode('anonymousNodeUid', 'newParentNodeUid');
|
|
232
233
|
|
|
233
234
|
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
234
|
-
nodes.anonymousNodeUid,
|
|
235
|
+
nodes.anonymousNodeUid.name,
|
|
235
236
|
expect.objectContaining({
|
|
236
237
|
key: 'anonymousNodeUid-key',
|
|
237
238
|
passphrase: 'anonymousNodeUid-passphrase',
|
|
@@ -289,7 +290,7 @@ describe('NodesManagement', () => {
|
|
|
289
290
|
parentNodeUid: 'newParentNodeUid',
|
|
290
291
|
});
|
|
291
292
|
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
292
|
-
nodes.nodeUid,
|
|
293
|
+
nodes.nodeUid.name,
|
|
293
294
|
expect.objectContaining({
|
|
294
295
|
key: 'nodeUid-key',
|
|
295
296
|
passphrase: 'nodeUid-passphrase',
|
|
@@ -324,7 +325,7 @@ describe('NodesManagement', () => {
|
|
|
324
325
|
const newNode = await management.copyNode('anonymousNodeUid', 'newParentNodeUid');
|
|
325
326
|
|
|
326
327
|
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
327
|
-
nodes.anonymousNodeUid,
|
|
328
|
+
nodes.anonymousNodeUid.name,
|
|
328
329
|
expect.objectContaining({
|
|
329
330
|
key: 'anonymousNodeUid-key',
|
|
330
331
|
passphrase: 'anonymousNodeUid-passphrase',
|
|
@@ -350,6 +351,49 @@ describe('NodesManagement', () => {
|
|
|
350
351
|
});
|
|
351
352
|
});
|
|
352
353
|
|
|
354
|
+
it('copyNode manages copy of node with new name', async () => {
|
|
355
|
+
const encryptedCrypto = {
|
|
356
|
+
encryptedName: 'copiedArmoredNodeName',
|
|
357
|
+
hash: 'copiedHash',
|
|
358
|
+
armoredNodePassphrase: 'copiedArmoredNodePassphrase',
|
|
359
|
+
armoredNodePassphraseSignature: 'copiedArmoredNodePassphraseSignature',
|
|
360
|
+
signatureEmail: 'copiedSignatureEmail',
|
|
361
|
+
nameSignatureEmail: 'copiedNameSignatureEmail',
|
|
362
|
+
};
|
|
363
|
+
cryptoService.encryptNodeWithNewParent = jest.fn().mockResolvedValue(encryptedCrypto);
|
|
364
|
+
|
|
365
|
+
const newName = 'new name';
|
|
366
|
+
const newNode = await management.copyNode('nodeUid', 'newParentNodeUid', newName);
|
|
367
|
+
|
|
368
|
+
expect(newNode).toEqual({
|
|
369
|
+
...nodes.nodeUid,
|
|
370
|
+
name: { ok: true, value: newName },
|
|
371
|
+
uid: 'newCopiedNodeUid',
|
|
372
|
+
parentUid: 'newParentNodeUid',
|
|
373
|
+
encryptedName: 'copiedArmoredNodeName',
|
|
374
|
+
hash: 'copiedHash',
|
|
375
|
+
keyAuthor: { ok: true, value: 'copiedSignatureEmail' },
|
|
376
|
+
nameAuthor: { ok: true, value: 'copiedNameSignatureEmail' },
|
|
377
|
+
});
|
|
378
|
+
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
379
|
+
{ ok: true, value: newName },
|
|
380
|
+
expect.objectContaining({
|
|
381
|
+
key: 'nodeUid-key',
|
|
382
|
+
passphrase: 'nodeUid-passphrase',
|
|
383
|
+
passphraseSessionKey: 'nodeUid-passphraseSessionKey',
|
|
384
|
+
contentKeyPacketSessionKey: 'nodeUid-contentKeyPacketSessionKey',
|
|
385
|
+
nameSessionKey: 'nodeUid-nameSessionKey',
|
|
386
|
+
}),
|
|
387
|
+
expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
|
|
388
|
+
{ type: 'userAddress', email: 'root-email', addressId: 'root-addressId', key: 'root-key' },
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('copyNode throws error if name is invalid', async () => {
|
|
393
|
+
const promise = management.copyNode('nodeUid', 'newParentNodeUid', 'invalid/name');
|
|
394
|
+
await expect(promise).rejects.toThrow(ValidationError);
|
|
395
|
+
});
|
|
396
|
+
|
|
353
397
|
it('trashes node and updates cache', async () => {
|
|
354
398
|
const uids = ['v1~n1', 'v1~n2'];
|
|
355
399
|
const trashed = new Set();
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
|
-
import { MemberRole, NodeType, NodeResult, NodeResultWithNewUid, resultOk } from '../../interface';
|
|
3
|
+
import { MemberRole, NodeType, NodeResult, NodeResultWithNewUid, resultOk, InvalidNameError } from '../../interface';
|
|
4
4
|
import { AbortError, ValidationError } from '../../errors';
|
|
5
|
-
import { getErrorMessage } from '../errors';
|
|
5
|
+
import { createErrorFromUnknown, getErrorMessage } from '../errors';
|
|
6
6
|
import { splitNodeUid } from '../uids';
|
|
7
|
-
import {
|
|
7
|
+
import { NodeAPIServiceBase } from './apiService';
|
|
8
8
|
import { NodesCryptoCache } from './cryptoCache';
|
|
9
9
|
import { NodesCryptoService } from './cryptoService';
|
|
10
10
|
import { NodeOutOfSyncError } from './errors';
|
|
11
11
|
import { generateFolderExtendedAttributes } from './extendedAttributes';
|
|
12
|
-
import { DecryptedNode } from './interface';
|
|
12
|
+
import { DecryptedNode, EncryptedNode } from './interface';
|
|
13
13
|
import { splitExtension, joinNameAndExtension } from './nodeName';
|
|
14
|
-
import {
|
|
14
|
+
import { NodesAccessBase } from './nodesAccess';
|
|
15
15
|
import { validateNodeName } from './validations';
|
|
16
16
|
|
|
17
17
|
const AVAILABLE_NAME_BATCH_SIZE = 10;
|
|
@@ -26,12 +26,16 @@ const AVAILABLE_NAME_LIMIT = 1000;
|
|
|
26
26
|
* This module uses other modules providing low-level operations, such
|
|
27
27
|
* as API service, cache, crypto service, etc.
|
|
28
28
|
*/
|
|
29
|
-
export class
|
|
29
|
+
export abstract class NodesManagementBase<
|
|
30
|
+
TEncryptedNode extends EncryptedNode = EncryptedNode,
|
|
31
|
+
TDecryptedNode extends DecryptedNode = DecryptedNode,
|
|
32
|
+
TNodesCryptoService extends NodesCryptoService = NodesCryptoService,
|
|
33
|
+
> {
|
|
30
34
|
constructor(
|
|
31
|
-
protected apiService:
|
|
35
|
+
protected apiService: NodeAPIServiceBase<TEncryptedNode>,
|
|
32
36
|
protected cryptoCache: NodesCryptoCache,
|
|
33
37
|
protected cryptoService: NodesCryptoService,
|
|
34
|
-
protected nodesAccess:
|
|
38
|
+
protected nodesAccess: NodesAccessBase<TEncryptedNode, TDecryptedNode, TNodesCryptoService>,
|
|
35
39
|
) {
|
|
36
40
|
this.apiService = apiService;
|
|
37
41
|
this.cryptoCache = cryptoCache;
|
|
@@ -43,7 +47,7 @@ export class NodesManagement {
|
|
|
43
47
|
nodeUid: string,
|
|
44
48
|
newName: string,
|
|
45
49
|
options = { allowRenameRootNode: false },
|
|
46
|
-
): Promise<
|
|
50
|
+
): Promise<TDecryptedNode> {
|
|
47
51
|
validateNodeName(newName);
|
|
48
52
|
|
|
49
53
|
const node = await this.nodesAccess.getNode(nodeUid);
|
|
@@ -91,7 +95,7 @@ export class NodesManagement {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
await this.nodesAccess.notifyNodeChanged(nodeUid);
|
|
94
|
-
const newNode:
|
|
98
|
+
const newNode: TDecryptedNode = {
|
|
95
99
|
...node,
|
|
96
100
|
name: resultOk(newName),
|
|
97
101
|
encryptedName: armoredNodeName,
|
|
@@ -123,7 +127,7 @@ export class NodesManagement {
|
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
async moveNode(nodeUid: string, newParentUid: string): Promise<
|
|
130
|
+
async moveNode(nodeUid: string, newParentUid: string): Promise<TDecryptedNode> {
|
|
127
131
|
const node = await this.nodesAccess.getNode(nodeUid);
|
|
128
132
|
|
|
129
133
|
const [keys, newParentKeys, signingKeys] = await Promise.all([
|
|
@@ -140,7 +144,7 @@ export class NodesManagement {
|
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
const encryptedCrypto = await this.cryptoService.encryptNodeWithNewParent(
|
|
143
|
-
node,
|
|
147
|
+
node.name,
|
|
144
148
|
keys,
|
|
145
149
|
{ key: newParentKeys.key, hashKey: newParentKeys.hashKey },
|
|
146
150
|
signingKeys,
|
|
@@ -172,7 +176,7 @@ export class NodesManagement {
|
|
|
172
176
|
// TODO: When moving photos, we need to pass content hash.
|
|
173
177
|
},
|
|
174
178
|
);
|
|
175
|
-
const newNode:
|
|
179
|
+
const newNode: TDecryptedNode = {
|
|
176
180
|
...node,
|
|
177
181
|
encryptedName: encryptedCrypto.encryptedName,
|
|
178
182
|
parentUid: newParentUid,
|
|
@@ -186,16 +190,18 @@ export class NodesManagement {
|
|
|
186
190
|
|
|
187
191
|
// Improvement requested: copy nodes in parallel using copy_multiple endpoint
|
|
188
192
|
async *copyNodes(
|
|
189
|
-
|
|
193
|
+
nodeUidsOrWithNames: (string | { uid: string; name: string })[],
|
|
190
194
|
newParentNodeUid: string,
|
|
191
195
|
signal?: AbortSignal,
|
|
192
196
|
): AsyncGenerator<NodeResultWithNewUid> {
|
|
193
|
-
for (const
|
|
197
|
+
for (const nodeUidOrWithName of nodeUidsOrWithNames) {
|
|
194
198
|
if (signal?.aborted) {
|
|
195
199
|
throw new AbortError(c('Error').t`Copy operation aborted`);
|
|
196
200
|
}
|
|
201
|
+
const nodeUid = typeof nodeUidOrWithName === 'string' ? nodeUidOrWithName : nodeUidOrWithName.uid;
|
|
202
|
+
const name = typeof nodeUidOrWithName === 'string' ? undefined : nodeUidOrWithName.name;
|
|
197
203
|
try {
|
|
198
|
-
const { uid: newNodeUid } = await this.copyNode(nodeUid, newParentNodeUid);
|
|
204
|
+
const { uid: newNodeUid } = await this.copyNode(nodeUid, newParentNodeUid, name);
|
|
199
205
|
yield {
|
|
200
206
|
uid: nodeUid,
|
|
201
207
|
newUid: newNodeUid,
|
|
@@ -205,14 +211,19 @@ export class NodesManagement {
|
|
|
205
211
|
yield {
|
|
206
212
|
uid: nodeUid,
|
|
207
213
|
ok: false,
|
|
208
|
-
error:
|
|
214
|
+
error: createErrorFromUnknown(error),
|
|
209
215
|
};
|
|
210
216
|
}
|
|
211
217
|
}
|
|
212
218
|
}
|
|
213
219
|
|
|
214
|
-
async copyNode(nodeUid: string, newParentUid: string): Promise<
|
|
220
|
+
async copyNode(nodeUid: string, newParentUid: string, name?: string): Promise<TDecryptedNode> {
|
|
221
|
+
if (name) {
|
|
222
|
+
validateNodeName(name);
|
|
223
|
+
}
|
|
224
|
+
|
|
215
225
|
const node = await this.nodesAccess.getNode(nodeUid);
|
|
226
|
+
const nodeName = name ? resultOk<string, Error | InvalidNameError>(name) : node.name;
|
|
216
227
|
|
|
217
228
|
const [keys, newParentKeys, signingKeys] = await Promise.all([
|
|
218
229
|
this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
|
|
@@ -225,7 +236,7 @@ export class NodesManagement {
|
|
|
225
236
|
}
|
|
226
237
|
|
|
227
238
|
const encryptedCrypto = await this.cryptoService.encryptNodeWithNewParent(
|
|
228
|
-
|
|
239
|
+
nodeName,
|
|
229
240
|
keys,
|
|
230
241
|
{ key: newParentKeys.key, hashKey: newParentKeys.hashKey },
|
|
231
242
|
signingKeys,
|
|
@@ -250,8 +261,9 @@ export class NodesManagement {
|
|
|
250
261
|
nameSignatureEmail: encryptedCrypto.nameSignatureEmail,
|
|
251
262
|
hash: encryptedCrypto.hash,
|
|
252
263
|
});
|
|
253
|
-
const newNode:
|
|
264
|
+
const newNode: TDecryptedNode = {
|
|
254
265
|
...node,
|
|
266
|
+
name: nodeName,
|
|
255
267
|
uid: newNodeUid,
|
|
256
268
|
encryptedName: encryptedCrypto.encryptedName,
|
|
257
269
|
parentUid: newParentUid,
|
|
@@ -291,7 +303,7 @@ export class NodesManagement {
|
|
|
291
303
|
}
|
|
292
304
|
|
|
293
305
|
// FIXME create test for create folder
|
|
294
|
-
async createFolder(parentNodeUid: string, folderName: string, modificationTime?: Date): Promise<
|
|
306
|
+
async createFolder(parentNodeUid: string, folderName: string, modificationTime?: Date): Promise<TDecryptedNode> {
|
|
295
307
|
validateNodeName(folderName);
|
|
296
308
|
|
|
297
309
|
const parentKeys = await this.nodesAccess.getNodeKeys(parentNodeUid);
|
|
@@ -320,8 +332,33 @@ export class NodesManagement {
|
|
|
320
332
|
});
|
|
321
333
|
|
|
322
334
|
await this.nodesAccess.notifyChildCreated(parentNodeUid);
|
|
335
|
+
const node = this.generateNodeFolder(nodeUid, parentNodeUid, folderName, encryptedCrypto);
|
|
336
|
+
await this.cryptoCache.setNodeKeys(nodeUid, keys);
|
|
337
|
+
return node;
|
|
338
|
+
}
|
|
323
339
|
|
|
324
|
-
|
|
340
|
+
protected abstract generateNodeFolder(
|
|
341
|
+
nodeUid: string,
|
|
342
|
+
parentUid: string,
|
|
343
|
+
name: string,
|
|
344
|
+
encryptedCrypto: {
|
|
345
|
+
hash: string;
|
|
346
|
+
encryptedName: string;
|
|
347
|
+
signatureEmail: string | null;
|
|
348
|
+
},
|
|
349
|
+
): TDecryptedNode;
|
|
350
|
+
|
|
351
|
+
protected generateNodeFolderBase(
|
|
352
|
+
nodeUid: string,
|
|
353
|
+
parentNodeUid: string,
|
|
354
|
+
name: string,
|
|
355
|
+
encryptedCrypto: {
|
|
356
|
+
hash: string;
|
|
357
|
+
encryptedName: string;
|
|
358
|
+
signatureEmail: string | null;
|
|
359
|
+
},
|
|
360
|
+
): DecryptedNode {
|
|
361
|
+
return {
|
|
325
362
|
// Internal metadata
|
|
326
363
|
hash: encryptedCrypto.hash,
|
|
327
364
|
encryptedName: encryptedCrypto.encryptedName,
|
|
@@ -332,6 +369,7 @@ export class NodesManagement {
|
|
|
332
369
|
type: NodeType.Folder,
|
|
333
370
|
mediaType: 'Folder',
|
|
334
371
|
creationTime: new Date(),
|
|
372
|
+
modificationTime: new Date(),
|
|
335
373
|
|
|
336
374
|
// Share node metadata
|
|
337
375
|
isShared: false,
|
|
@@ -342,12 +380,9 @@ export class NodesManagement {
|
|
|
342
380
|
isStale: false,
|
|
343
381
|
keyAuthor: resultOk(encryptedCrypto.signatureEmail || null),
|
|
344
382
|
nameAuthor: resultOk(encryptedCrypto.signatureEmail || null),
|
|
345
|
-
name: resultOk(
|
|
383
|
+
name: resultOk(name),
|
|
346
384
|
treeEventScopeId: splitNodeUid(nodeUid).volumeId,
|
|
347
385
|
};
|
|
348
|
-
|
|
349
|
-
await this.cryptoCache.setNodeKeys(nodeUid, keys);
|
|
350
|
-
return node;
|
|
351
386
|
}
|
|
352
387
|
|
|
353
388
|
async findAvailableName(parentFolderUid: string, name: string): Promise<string> {
|
|
@@ -388,3 +423,18 @@ export class NodesManagement {
|
|
|
388
423
|
throw new ValidationError(c('Error').t`No available name found`);
|
|
389
424
|
}
|
|
390
425
|
}
|
|
426
|
+
|
|
427
|
+
export class NodesManagement extends NodesManagementBase {
|
|
428
|
+
protected generateNodeFolder(
|
|
429
|
+
nodeUid: string,
|
|
430
|
+
parentNodeUid: string,
|
|
431
|
+
name: string,
|
|
432
|
+
encryptedCrypto: {
|
|
433
|
+
hash: string;
|
|
434
|
+
encryptedName: string;
|
|
435
|
+
signatureEmail: string | null;
|
|
436
|
+
},
|
|
437
|
+
): DecryptedNode {
|
|
438
|
+
return this.generateNodeFolderBase(nodeUid, parentNodeUid, name, encryptedCrypto);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Logger } from '../../interface';
|
|
2
2
|
import { makeNodeUidFromRevisionUid } from '../uids';
|
|
3
|
-
import {
|
|
3
|
+
import { NodeAPIServiceBase } from './apiService';
|
|
4
4
|
import { NodesCryptoService } from './cryptoService';
|
|
5
5
|
import { NodesAccess } from './nodesAccess';
|
|
6
6
|
import { parseFileExtendedAttributes } from './extendedAttributes';
|
|
@@ -12,9 +12,9 @@ import { DecryptedRevision } from './interface';
|
|
|
12
12
|
export class NodesRevisons {
|
|
13
13
|
constructor(
|
|
14
14
|
private logger: Logger,
|
|
15
|
-
private apiService:
|
|
15
|
+
private apiService: NodeAPIServiceBase,
|
|
16
16
|
private cryptoService: NodesCryptoService,
|
|
17
|
-
private nodesAccess: NodesAccess,
|
|
17
|
+
private nodesAccess: Pick<NodesAccess, 'getNodeKeys'>,
|
|
18
18
|
) {
|
|
19
19
|
this.logger = logger;
|
|
20
20
|
this.apiService = apiService;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BatchLoading } from '../batchLoading';
|
|
2
2
|
import { DecryptedNode } from '../nodes';
|
|
3
3
|
import { PhotosAPIService } from './apiService';
|
|
4
|
-
import {
|
|
4
|
+
import { PhotosNodesAccess } from './nodes';
|
|
5
5
|
import { PhotoSharesManager } from './shares';
|
|
6
6
|
|
|
7
7
|
const BATCH_LOADING_SIZE = 10;
|
|
@@ -13,7 +13,7 @@ export class Albums {
|
|
|
13
13
|
constructor(
|
|
14
14
|
private apiService: PhotosAPIService,
|
|
15
15
|
private photoShares: PhotoSharesManager,
|
|
16
|
-
private nodesService:
|
|
16
|
+
private nodesService: PhotosNodesAccess,
|
|
17
17
|
) {
|
|
18
18
|
this.apiService = apiService;
|
|
19
19
|
this.photoShares = photoShares;
|