@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
|
@@ -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 {
|
|
@@ -5,6 +5,7 @@ import { NodesAccess } from './nodesAccess';
|
|
|
5
5
|
import { DecryptedNode } from './interface';
|
|
6
6
|
import { NodesManagement } from './nodesManagement';
|
|
7
7
|
import { NodeResult } from '../../interface';
|
|
8
|
+
import { NodeOutOfSyncError } from './errors';
|
|
8
9
|
|
|
9
10
|
describe('NodesManagement', () => {
|
|
10
11
|
let apiService: NodeAPIService;
|
|
@@ -49,6 +50,7 @@ describe('NodesManagement', () => {
|
|
|
49
50
|
apiService = {
|
|
50
51
|
renameNode: jest.fn(),
|
|
51
52
|
moveNode: jest.fn(),
|
|
53
|
+
copyNode: jest.fn(),
|
|
52
54
|
trashNodes: jest.fn(async function* (uids) {
|
|
53
55
|
yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
|
|
54
56
|
}),
|
|
@@ -71,7 +73,7 @@ describe('NodesManagement', () => {
|
|
|
71
73
|
armoredNodeName: 'newArmoredNodeName',
|
|
72
74
|
hash: 'newHash',
|
|
73
75
|
}),
|
|
74
|
-
|
|
76
|
+
encryptNodeWithNewParent: jest.fn(),
|
|
75
77
|
createFolder: jest.fn(),
|
|
76
78
|
};
|
|
77
79
|
// @ts-expect-error No need to implement all methods for mocking
|
|
@@ -100,6 +102,7 @@ describe('NodesManagement', () => {
|
|
|
100
102
|
getRootNodeEmailKey: jest.fn().mockResolvedValue({ email: 'root-email', addressKey: 'root-key' }),
|
|
101
103
|
notifyNodeChanged: jest.fn(),
|
|
102
104
|
notifyNodeDeleted: jest.fn(),
|
|
105
|
+
notifyChildCreated: jest.fn(),
|
|
103
106
|
};
|
|
104
107
|
|
|
105
108
|
management = new NodesManagement(apiService, cryptoCache, cryptoService, nodesAccess);
|
|
@@ -130,6 +133,15 @@ describe('NodesManagement', () => {
|
|
|
130
133
|
expect(nodesAccess.notifyNodeChanged).toHaveBeenCalledWith('nodeUid');
|
|
131
134
|
});
|
|
132
135
|
|
|
136
|
+
it('renameNode refreshes cache if node is out of sync', async () => {
|
|
137
|
+
const error = new NodeOutOfSyncError('Node is out of sync');
|
|
138
|
+
apiService.renameNode = jest.fn().mockRejectedValue(error);
|
|
139
|
+
|
|
140
|
+
await expect(management.renameNode('nodeUid', 'new name')).rejects.toThrow(error);
|
|
141
|
+
|
|
142
|
+
expect(nodesAccess.notifyNodeChanged).toHaveBeenCalledWith('nodeUid');
|
|
143
|
+
});
|
|
144
|
+
|
|
133
145
|
it('moveNode manages move and updates cache', async () => {
|
|
134
146
|
const encryptedCrypto = {
|
|
135
147
|
encryptedName: 'movedArmoredNodeName',
|
|
@@ -139,7 +151,7 @@ describe('NodesManagement', () => {
|
|
|
139
151
|
signatureEmail: 'movedSignatureEmail',
|
|
140
152
|
nameSignatureEmail: 'movedNameSignatureEmail',
|
|
141
153
|
};
|
|
142
|
-
cryptoService.
|
|
154
|
+
cryptoService.encryptNodeWithNewParent = jest.fn().mockResolvedValue(encryptedCrypto);
|
|
143
155
|
|
|
144
156
|
const newNode = await management.moveNode('nodeUid', 'newParentNodeUid');
|
|
145
157
|
|
|
@@ -152,7 +164,7 @@ describe('NodesManagement', () => {
|
|
|
152
164
|
nameAuthor: { ok: true, value: 'movedNameSignatureEmail' },
|
|
153
165
|
});
|
|
154
166
|
expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
|
|
155
|
-
expect(cryptoService.
|
|
167
|
+
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
156
168
|
nodes.nodeUid,
|
|
157
169
|
expect.objectContaining({
|
|
158
170
|
key: 'nodeUid-key',
|
|
@@ -188,11 +200,11 @@ describe('NodesManagement', () => {
|
|
|
188
200
|
signatureEmail: 'movedSignatureEmail',
|
|
189
201
|
nameSignatureEmail: 'movedNameSignatureEmail',
|
|
190
202
|
};
|
|
191
|
-
cryptoService.
|
|
203
|
+
cryptoService.encryptNodeWithNewParent = jest.fn().mockResolvedValue(encryptedCrypto);
|
|
192
204
|
|
|
193
205
|
const newNode = await management.moveNode('anonymousNodeUid', 'newParentNodeUid');
|
|
194
206
|
|
|
195
|
-
expect(cryptoService.
|
|
207
|
+
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
196
208
|
nodes.anonymousNodeUid,
|
|
197
209
|
expect.objectContaining({
|
|
198
210
|
key: 'anonymousNodeUid-key',
|
|
@@ -224,6 +236,89 @@ describe('NodesManagement', () => {
|
|
|
224
236
|
);
|
|
225
237
|
});
|
|
226
238
|
|
|
239
|
+
it('copyNode manages copy and updates cache', async () => {
|
|
240
|
+
const encryptedCrypto = {
|
|
241
|
+
encryptedName: 'copiedArmoredNodeName',
|
|
242
|
+
hash: 'copiedHash',
|
|
243
|
+
armoredNodePassphrase: 'copiedArmoredNodePassphrase',
|
|
244
|
+
armoredNodePassphraseSignature: 'copiedArmoredNodePassphraseSignature',
|
|
245
|
+
signatureEmail: 'copiedSignatureEmail',
|
|
246
|
+
nameSignatureEmail: 'copiedNameSignatureEmail',
|
|
247
|
+
};
|
|
248
|
+
cryptoService.encryptNodeWithNewParent = jest.fn().mockResolvedValue(encryptedCrypto);
|
|
249
|
+
|
|
250
|
+
const newNode = await management.copyNode('nodeUid', 'newParentNodeUid');
|
|
251
|
+
|
|
252
|
+
expect(newNode).toEqual({
|
|
253
|
+
...nodes.nodeUid,
|
|
254
|
+
parentUid: 'newParentNodeUid',
|
|
255
|
+
encryptedName: 'copiedArmoredNodeName',
|
|
256
|
+
hash: 'copiedHash',
|
|
257
|
+
keyAuthor: { ok: true, value: 'copiedSignatureEmail' },
|
|
258
|
+
nameAuthor: { ok: true, value: 'copiedNameSignatureEmail' },
|
|
259
|
+
});
|
|
260
|
+
expect(nodesAccess.getRootNodeEmailKey).toHaveBeenCalledWith('newParentNodeUid');
|
|
261
|
+
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
262
|
+
nodes.nodeUid,
|
|
263
|
+
expect.objectContaining({
|
|
264
|
+
key: 'nodeUid-key',
|
|
265
|
+
passphrase: 'nodeUid-passphrase',
|
|
266
|
+
passphraseSessionKey: 'nodeUid-passphraseSessionKey',
|
|
267
|
+
contentKeyPacketSessionKey: 'nodeUid-contentKeyPacketSessionKey',
|
|
268
|
+
nameSessionKey: 'nodeUid-nameSessionKey',
|
|
269
|
+
}),
|
|
270
|
+
expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
|
|
271
|
+
{ email: 'root-email', addressKey: 'root-key' },
|
|
272
|
+
);
|
|
273
|
+
expect(apiService.copyNode).toHaveBeenCalledWith('nodeUid', {
|
|
274
|
+
parentUid: 'newParentNodeUid',
|
|
275
|
+
...encryptedCrypto,
|
|
276
|
+
armoredNodePassphraseSignature: undefined,
|
|
277
|
+
signatureEmail: undefined,
|
|
278
|
+
});
|
|
279
|
+
expect(nodesAccess.notifyNodeChanged).not.toHaveBeenCalledWith();
|
|
280
|
+
expect(nodesAccess.notifyChildCreated).toHaveBeenCalledWith('newParentNodeUid');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('copyNode manages copy of anonymous node', async () => {
|
|
284
|
+
const encryptedCrypto = {
|
|
285
|
+
encryptedName: 'copiedArmoredNodeName',
|
|
286
|
+
hash: 'copiedHash',
|
|
287
|
+
armoredNodePassphrase: 'copiedArmoredNodePassphrase',
|
|
288
|
+
armoredNodePassphraseSignature: 'copiedArmoredNodePassphraseSignature',
|
|
289
|
+
signatureEmail: 'copiedSignatureEmail',
|
|
290
|
+
nameSignatureEmail: 'copiedNameSignatureEmail',
|
|
291
|
+
};
|
|
292
|
+
cryptoService.encryptNodeWithNewParent = jest.fn().mockResolvedValue(encryptedCrypto);
|
|
293
|
+
|
|
294
|
+
const newNode = await management.copyNode('anonymousNodeUid', 'newParentNodeUid');
|
|
295
|
+
|
|
296
|
+
expect(cryptoService.encryptNodeWithNewParent).toHaveBeenCalledWith(
|
|
297
|
+
nodes.anonymousNodeUid,
|
|
298
|
+
expect.objectContaining({
|
|
299
|
+
key: 'anonymousNodeUid-key',
|
|
300
|
+
passphrase: 'anonymousNodeUid-passphrase',
|
|
301
|
+
passphraseSessionKey: 'anonymousNodeUid-passphraseSessionKey',
|
|
302
|
+
contentKeyPacketSessionKey: 'anonymousNodeUid-contentKeyPacketSessionKey',
|
|
303
|
+
nameSessionKey: 'anonymousNodeUid-nameSessionKey',
|
|
304
|
+
}),
|
|
305
|
+
expect.objectContaining({ key: 'newParentNodeUid-key', hashKey: 'newParentNodeUid-hashKey' }),
|
|
306
|
+
{ email: 'root-email', addressKey: 'root-key' },
|
|
307
|
+
);
|
|
308
|
+
expect(newNode).toEqual({
|
|
309
|
+
...nodes.anonymousNodeUid,
|
|
310
|
+
parentUid: 'newParentNodeUid',
|
|
311
|
+
encryptedName: 'copiedArmoredNodeName',
|
|
312
|
+
hash: 'copiedHash',
|
|
313
|
+
keyAuthor: { ok: true, value: 'copiedSignatureEmail' },
|
|
314
|
+
nameAuthor: { ok: true, value: 'copiedNameSignatureEmail' },
|
|
315
|
+
});
|
|
316
|
+
expect(apiService.copyNode).toHaveBeenCalledWith('anonymousNodeUid', {
|
|
317
|
+
parentUid: 'newParentNodeUid',
|
|
318
|
+
...encryptedCrypto,
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
227
322
|
it('trashes node and updates cache', async () => {
|
|
228
323
|
const uids = ['v1~n1', 'v1~n2'];
|
|
229
324
|
const trashed = new Set();
|
|
@@ -3,14 +3,15 @@ import { c } from 'ttag';
|
|
|
3
3
|
import { MemberRole, NodeType, NodeResult, resultOk } from '../../interface';
|
|
4
4
|
import { AbortError, ValidationError } from '../../errors';
|
|
5
5
|
import { getErrorMessage } from '../errors';
|
|
6
|
+
import { splitNodeUid } from '../uids';
|
|
6
7
|
import { NodeAPIService } from './apiService';
|
|
7
8
|
import { NodesCryptoCache } from './cryptoCache';
|
|
8
9
|
import { NodesCryptoService } from './cryptoService';
|
|
10
|
+
import { NodeOutOfSyncError } from './errors';
|
|
9
11
|
import { DecryptedNode } from './interface';
|
|
10
12
|
import { NodesAccess } from './nodesAccess';
|
|
11
13
|
import { validateNodeName } from './validations';
|
|
12
14
|
import { generateFolderExtendedAttributes } from './extendedAttributes';
|
|
13
|
-
import { splitNodeUid } from '../uids';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Provides high-level actions for managing nodes.
|
|
@@ -63,17 +64,28 @@ export class NodesManagement {
|
|
|
63
64
|
throw new Error('Node hash not generated');
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
try {
|
|
68
|
+
await this.apiService.renameNode(
|
|
69
|
+
nodeUid,
|
|
70
|
+
{
|
|
71
|
+
hash: node.hash,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
encryptedName: armoredNodeName,
|
|
75
|
+
nameSignatureEmail: signatureEmail,
|
|
76
|
+
hash: hash,
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
} catch (error: unknown) {
|
|
80
|
+
// If node is out of sync, we notify cache to refresh it before next usage.
|
|
81
|
+
// We let the code still throw the error as it must bubble to the user
|
|
82
|
+
// so user can re-open the node to ensure they still want to rename it.
|
|
83
|
+
if (error instanceof NodeOutOfSyncError) {
|
|
84
|
+
await this.nodesAccess.notifyNodeChanged(nodeUid);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
await this.nodesAccess.notifyNodeChanged(nodeUid);
|
|
78
90
|
const newNode: DecryptedNode = {
|
|
79
91
|
...node,
|
|
@@ -125,7 +137,7 @@ export class NodesManagement {
|
|
|
125
137
|
throw new ValidationError(c('Error').t`Moving item to a non-folder is not allowed`);
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
const encryptedCrypto = await this.cryptoService.
|
|
140
|
+
const encryptedCrypto = await this.cryptoService.encryptNodeWithNewParent(
|
|
129
141
|
node,
|
|
130
142
|
keys,
|
|
131
143
|
{ key: newParentKeys.key, hashKey: newParentKeys.hashKey },
|
|
@@ -170,6 +182,81 @@ export class NodesManagement {
|
|
|
170
182
|
return newNode;
|
|
171
183
|
}
|
|
172
184
|
|
|
185
|
+
// Improvement requested: copy nodes in parallel
|
|
186
|
+
async *copyNodes(nodeUids: string[], newParentNodeUid: string, signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
187
|
+
for (const nodeUid of nodeUids) {
|
|
188
|
+
if (signal?.aborted) {
|
|
189
|
+
throw new AbortError(c('Error').t`Copy operation aborted`);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await this.copyNode(nodeUid, newParentNodeUid);
|
|
193
|
+
yield {
|
|
194
|
+
uid: nodeUid,
|
|
195
|
+
ok: true,
|
|
196
|
+
};
|
|
197
|
+
} catch (error: unknown) {
|
|
198
|
+
yield {
|
|
199
|
+
uid: nodeUid,
|
|
200
|
+
ok: false,
|
|
201
|
+
error: getErrorMessage(error),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async copyNode(nodeUid: string, newParentUid: string): Promise<DecryptedNode> {
|
|
208
|
+
const [node, address] = await Promise.all([
|
|
209
|
+
this.nodesAccess.getNode(nodeUid),
|
|
210
|
+
this.nodesAccess.getRootNodeEmailKey(newParentUid),
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
const [keys, newParentKeys] = await Promise.all([
|
|
214
|
+
this.nodesAccess.getNodePrivateAndSessionKeys(nodeUid),
|
|
215
|
+
this.nodesAccess.getNodeKeys(newParentUid),
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
if (!newParentKeys.hashKey) {
|
|
219
|
+
throw new ValidationError(c('Error').t`Copying item to a non-folder is not allowed`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const encryptedCrypto = await this.cryptoService.encryptNodeWithNewParent(
|
|
223
|
+
node,
|
|
224
|
+
keys,
|
|
225
|
+
{ key: newParentKeys.key, hashKey: newParentKeys.hashKey },
|
|
226
|
+
address,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Node could be uploaded or renamed by anonymous user and thus have
|
|
230
|
+
// missing signatures that must be added to the copy request.
|
|
231
|
+
// Node passphrase and signature email must be passed if and only if
|
|
232
|
+
// the the signatures are missing (key author is null).
|
|
233
|
+
const anonymousKey = node.keyAuthor.ok && node.keyAuthor.value === null;
|
|
234
|
+
const keySignatureProperties = !anonymousKey
|
|
235
|
+
? {}
|
|
236
|
+
: {
|
|
237
|
+
signatureEmail: encryptedCrypto.signatureEmail,
|
|
238
|
+
armoredNodePassphraseSignature: encryptedCrypto.armoredNodePassphraseSignature,
|
|
239
|
+
};
|
|
240
|
+
await this.apiService.copyNode(nodeUid, {
|
|
241
|
+
...keySignatureProperties,
|
|
242
|
+
parentUid: newParentUid,
|
|
243
|
+
armoredNodePassphrase: encryptedCrypto.armoredNodePassphrase,
|
|
244
|
+
encryptedName: encryptedCrypto.encryptedName,
|
|
245
|
+
nameSignatureEmail: encryptedCrypto.nameSignatureEmail,
|
|
246
|
+
hash: encryptedCrypto.hash,
|
|
247
|
+
});
|
|
248
|
+
const newNode: DecryptedNode = {
|
|
249
|
+
...node,
|
|
250
|
+
encryptedName: encryptedCrypto.encryptedName,
|
|
251
|
+
parentUid: newParentUid,
|
|
252
|
+
hash: encryptedCrypto.hash,
|
|
253
|
+
keyAuthor: resultOk(encryptedCrypto.signatureEmail),
|
|
254
|
+
nameAuthor: resultOk(encryptedCrypto.nameSignatureEmail),
|
|
255
|
+
};
|
|
256
|
+
await this.nodesAccess.notifyChildCreated(newParentUid);
|
|
257
|
+
return newNode;
|
|
258
|
+
}
|
|
259
|
+
|
|
173
260
|
async *trashNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
174
261
|
for await (const result of this.apiService.trashNodes(nodeUids, signal)) {
|
|
175
262
|
if (result.ok) {
|
|
@@ -242,6 +329,7 @@ export class NodesManagement {
|
|
|
242
329
|
|
|
243
330
|
// Share node metadata
|
|
244
331
|
isShared: false,
|
|
332
|
+
isSharedPublicly: false,
|
|
245
333
|
directRole: MemberRole.Inherited,
|
|
246
334
|
|
|
247
335
|
// Decrypted metadata
|
|
@@ -1,29 +1,48 @@
|
|
|
1
|
+
import { BatchLoading } from '../batchLoading';
|
|
2
|
+
import { DecryptedNode } from '../nodes';
|
|
1
3
|
import { PhotosAPIService } from './apiService';
|
|
2
|
-
import { PhotosCache } from './cache';
|
|
3
4
|
import { NodesService } from './interface';
|
|
5
|
+
import { PhotoSharesManager } from './shares';
|
|
4
6
|
|
|
7
|
+
const BATCH_LOADING_SIZE = 10;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provides access and high-level actions for managing albums.
|
|
11
|
+
*/
|
|
5
12
|
export class Albums {
|
|
6
13
|
constructor(
|
|
7
14
|
private apiService: PhotosAPIService,
|
|
8
|
-
private
|
|
15
|
+
private photoShares: PhotoSharesManager,
|
|
9
16
|
private nodesService: NodesService,
|
|
10
17
|
) {
|
|
11
18
|
this.apiService = apiService;
|
|
12
|
-
this.
|
|
19
|
+
this.photoShares = photoShares;
|
|
13
20
|
this.nodesService = nodesService;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
async *iterateAlbums() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
async *iterateAlbums(signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
|
|
24
|
+
const { volumeId } = await this.photoShares.getOwnVolumeIDs();
|
|
25
|
+
|
|
26
|
+
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
27
|
+
iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
|
|
28
|
+
batchSize: BATCH_LOADING_SIZE,
|
|
29
|
+
});
|
|
30
|
+
for await (const album of this.apiService.iterateAlbums(volumeId, signal)) {
|
|
31
|
+
yield* batchLoading.load(album.albumUid);
|
|
22
32
|
}
|
|
33
|
+
yield* batchLoading.loadRest();
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
async
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
private async *iterateNodesAndIgnoreMissingOnes(
|
|
37
|
+
nodeUids: string[],
|
|
38
|
+
signal?: AbortSignal,
|
|
39
|
+
): AsyncGenerator<DecryptedNode> {
|
|
40
|
+
const nodeGenerator = this.nodesService.iterateNodes(nodeUids, signal);
|
|
41
|
+
for await (const node of nodeGenerator) {
|
|
42
|
+
if ('missingUid' in node) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
yield node;
|
|
46
|
+
}
|
|
28
47
|
}
|
|
29
48
|
}
|