@protontech/drive-sdk 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errors.d.ts +3 -0
- package/dist/internal/apiService/errors.js +7 -1
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/devices/interface.d.ts +1 -1
- package/dist/internal/devices/manager.js +1 -1
- package/dist/internal/devices/manager.js.map +1 -1
- package/dist/internal/devices/manager.test.js +3 -3
- package/dist/internal/devices/manager.test.js.map +1 -1
- package/dist/internal/events/apiService.js +1 -1
- package/dist/internal/events/apiService.js.map +1 -1
- package/dist/internal/events/coreEventManager.js +1 -1
- package/dist/internal/events/coreEventManager.js.map +1 -1
- package/dist/internal/events/coreEventManager.test.js +18 -24
- package/dist/internal/events/coreEventManager.test.js.map +1 -1
- package/dist/internal/events/index.d.ts +3 -4
- package/dist/internal/events/index.js +4 -4
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/events/interface.d.ts +3 -0
- package/dist/internal/nodes/apiService.d.ts +12 -3
- package/dist/internal/nodes/apiService.js +53 -13
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +19 -2
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +1 -1
- package/dist/internal/nodes/cryptoService.js +1 -1
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +4 -4
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/errors.d.ts +4 -0
- package/dist/internal/nodes/errors.js +9 -0
- package/dist/internal/nodes/errors.js.map +1 -0
- package/dist/internal/nodes/index.test.js +1 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +4 -1
- package/dist/internal/nodes/nodesAccess.d.ts +3 -3
- package/dist/internal/nodes/nodesAccess.js +25 -15
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +48 -8
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +2 -0
- package/dist/internal/nodes/nodesManagement.js +86 -9
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +81 -5
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/photos/albums.d.ts +9 -7
- package/dist/internal/photos/albums.js +26 -13
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/apiService.d.ts +34 -3
- package/dist/internal/photos/apiService.js +96 -3
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/index.d.ts +20 -4
- package/dist/internal/photos/index.js +30 -7
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +25 -1
- package/dist/internal/photos/shares.d.ts +43 -0
- package/dist/internal/photos/shares.js +112 -0
- package/dist/internal/photos/shares.js.map +1 -0
- package/dist/internal/photos/timeline.d.ts +15 -0
- package/dist/internal/photos/timeline.js +22 -0
- package/dist/internal/photos/timeline.js.map +1 -0
- package/dist/internal/shares/manager.d.ts +1 -1
- package/dist/internal/shares/manager.js +4 -4
- package/dist/internal/shares/manager.js.map +1 -1
- package/dist/internal/shares/manager.test.js +7 -7
- package/dist/internal/shares/manager.test.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +1 -1
- package/dist/internal/sharing/sharingAccess.js +1 -1
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +1 -1
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +20 -3
- package/dist/protonDriveClient.js +23 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +86 -12
- package/dist/protonDrivePhotosClient.js +132 -29
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/package.json +1 -1
- package/src/internal/apiService/errorCodes.ts +1 -0
- package/src/internal/apiService/errors.ts +6 -0
- package/src/internal/devices/interface.ts +1 -1
- package/src/internal/devices/manager.test.ts +3 -3
- package/src/internal/devices/manager.ts +1 -1
- package/src/internal/events/apiService.ts +1 -1
- package/src/internal/events/coreEventManager.test.ts +21 -27
- package/src/internal/events/coreEventManager.ts +1 -1
- package/src/internal/events/index.ts +3 -4
- package/src/internal/events/interface.ts +4 -0
- package/src/internal/nodes/apiService.test.ts +35 -1
- package/src/internal/nodes/apiService.ts +103 -17
- package/src/internal/nodes/cryptoService.test.ts +8 -8
- package/src/internal/nodes/cryptoService.ts +1 -1
- package/src/internal/nodes/errors.ts +5 -0
- package/src/internal/nodes/index.test.ts +1 -1
- package/src/internal/nodes/interface.ts +5 -1
- package/src/internal/nodes/nodesAccess.test.ts +68 -8
- package/src/internal/nodes/nodesAccess.ts +42 -15
- package/src/internal/nodes/nodesManagement.test.ts +100 -5
- package/src/internal/nodes/nodesManagement.ts +100 -13
- package/src/internal/photos/albums.ts +31 -12
- package/src/internal/photos/apiService.ts +159 -4
- package/src/internal/photos/index.ts +54 -9
- package/src/internal/photos/interface.ts +23 -1
- package/src/internal/photos/shares.ts +134 -0
- package/src/internal/photos/timeline.ts +24 -0
- package/src/internal/shares/manager.test.ts +7 -7
- package/src/internal/shares/manager.ts +4 -4
- package/src/internal/sharing/interface.ts +1 -1
- package/src/internal/sharing/sharingAccess.test.ts +1 -1
- package/src/internal/sharing/sharingAccess.ts +1 -1
- package/src/protonDriveClient.ts +33 -4
- package/src/protonDrivePhotosClient.ts +211 -32
- package/dist/internal/photos/cache.d.ts +0 -6
- package/dist/internal/photos/cache.js +0 -15
- package/dist/internal/photos/cache.js.map +0 -1
- package/dist/internal/photos/photosTimeline.d.ts +0 -10
- package/dist/internal/photos/photosTimeline.js +0 -19
- package/dist/internal/photos/photosTimeline.js.map +0 -1
- package/src/internal/photos/cache.ts +0 -11
- package/src/internal/photos/photosTimeline.ts +0 -17
|
@@ -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) {
|
|
@@ -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
|
}
|
|
@@ -1,13 +1,168 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c } from 'ttag';
|
|
2
2
|
|
|
3
|
+
import { DriveAPIService, drivePaths, NotFoundAPIError } from '../apiService';
|
|
4
|
+
import { EncryptedRootShare, EncryptedShareCrypto, ShareType } from '../shares/interface';
|
|
5
|
+
import { makeNodeUid } from '../uids';
|
|
6
|
+
|
|
7
|
+
type GetVolumesResponse = drivePaths['/drive/volumes']['get']['responses']['200']['content']['application/json'];
|
|
8
|
+
|
|
9
|
+
type GetShareResponse = drivePaths['/drive/shares/{shareID}']['get']['responses']['200']['content']['application/json'];
|
|
10
|
+
|
|
11
|
+
type PostCreateVolumeRequest = Extract<
|
|
12
|
+
drivePaths['/drive/photos/volumes']['post']['requestBody'],
|
|
13
|
+
{ content: object }
|
|
14
|
+
>['content']['application/json'];
|
|
15
|
+
type PostCreateVolumeResponse =
|
|
16
|
+
drivePaths['/drive/photos/volumes']['post']['responses']['200']['content']['application/json'];
|
|
17
|
+
|
|
18
|
+
type GetTimelineResponse =
|
|
19
|
+
drivePaths['/drive/volumes/{volumeID}/photos']['get']['responses']['200']['content']['application/json'];
|
|
20
|
+
|
|
21
|
+
type GetAlbumsResponse =
|
|
22
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums']['get']['responses']['200']['content']['application/json'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Provides API communication for fetching and manipulating photos and albums
|
|
26
|
+
* metadata.
|
|
27
|
+
*
|
|
28
|
+
* The service is responsible for transforming local objects to API payloads
|
|
29
|
+
* and vice versa. It should not contain any business logic.
|
|
30
|
+
*/
|
|
3
31
|
export class PhotosAPIService {
|
|
4
32
|
constructor(private apiService: DriveAPIService) {
|
|
5
33
|
this.apiService = apiService;
|
|
6
34
|
}
|
|
7
35
|
|
|
8
|
-
async
|
|
36
|
+
async getPhotoShare(): Promise<EncryptedRootShare> {
|
|
37
|
+
// TODO: Switch to drive/v2/shares/photos once available.
|
|
38
|
+
|
|
39
|
+
const volumesResponse = await this.apiService.get<GetVolumesResponse>('drive/volumes');
|
|
40
|
+
|
|
41
|
+
const photoVolume = volumesResponse.Volumes.find((volume) => volume.Type === 2);
|
|
42
|
+
|
|
43
|
+
if (!photoVolume) {
|
|
44
|
+
throw new NotFoundAPIError(c('Error').t`Photo volume not found`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const response = await this.apiService.get<GetShareResponse>(`drive/shares/${photoVolume.Share.ShareID}`);
|
|
9
48
|
|
|
10
|
-
|
|
49
|
+
if (!response.AddressID) {
|
|
50
|
+
throw new Error('Photo root share has not address ID set');
|
|
51
|
+
}
|
|
11
52
|
|
|
12
|
-
|
|
53
|
+
return {
|
|
54
|
+
volumeId: response.VolumeID,
|
|
55
|
+
shareId: response.ShareID,
|
|
56
|
+
rootNodeId: response.LinkID,
|
|
57
|
+
creatorEmail: response.Creator,
|
|
58
|
+
encryptedCrypto: {
|
|
59
|
+
armoredKey: response.Key,
|
|
60
|
+
armoredPassphrase: response.Passphrase,
|
|
61
|
+
armoredPassphraseSignature: response.PassphraseSignature,
|
|
62
|
+
},
|
|
63
|
+
addressId: response.AddressID,
|
|
64
|
+
type: ShareType.Photo,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async createPhotoVolume(
|
|
69
|
+
share: {
|
|
70
|
+
addressId: string;
|
|
71
|
+
addressKeyId: string;
|
|
72
|
+
} & EncryptedShareCrypto,
|
|
73
|
+
node: {
|
|
74
|
+
encryptedName: string;
|
|
75
|
+
armoredKey: string;
|
|
76
|
+
armoredPassphrase: string;
|
|
77
|
+
armoredPassphraseSignature: string;
|
|
78
|
+
armoredHashKey: string;
|
|
79
|
+
},
|
|
80
|
+
): Promise<{ volumeId: string; shareId: string; rootNodeId: string }> {
|
|
81
|
+
const response = await this.apiService.post<PostCreateVolumeRequest, PostCreateVolumeResponse>(
|
|
82
|
+
'drive/photos/volumes',
|
|
83
|
+
{
|
|
84
|
+
Share: {
|
|
85
|
+
AddressID: share.addressId,
|
|
86
|
+
AddressKeyID: share.addressKeyId,
|
|
87
|
+
Key: share.armoredKey,
|
|
88
|
+
Passphrase: share.armoredPassphrase,
|
|
89
|
+
PassphraseSignature: share.armoredPassphraseSignature,
|
|
90
|
+
},
|
|
91
|
+
Link: {
|
|
92
|
+
Name: node.encryptedName,
|
|
93
|
+
NodeKey: node.armoredKey,
|
|
94
|
+
NodePassphrase: node.armoredPassphrase,
|
|
95
|
+
NodePassphraseSignature: node.armoredPassphraseSignature,
|
|
96
|
+
NodeHashKey: node.armoredHashKey,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
return {
|
|
101
|
+
volumeId: response.Volume.VolumeID,
|
|
102
|
+
shareId: response.Volume.Share.ShareID,
|
|
103
|
+
rootNodeId: response.Volume.Share.LinkID,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async *iterateTimeline(
|
|
108
|
+
volumeId: string,
|
|
109
|
+
signal?: AbortSignal,
|
|
110
|
+
): AsyncGenerator<{
|
|
111
|
+
nodeUid: string;
|
|
112
|
+
captureTime: Date;
|
|
113
|
+
tags: number[];
|
|
114
|
+
}> {
|
|
115
|
+
let anchor = '';
|
|
116
|
+
while (true) {
|
|
117
|
+
const response = await this.apiService.get<GetTimelineResponse>(
|
|
118
|
+
`drive/volumes/${volumeId}/photos?${anchor ? `PreviousPageLastLinkID=${anchor}` : ''}`,
|
|
119
|
+
signal,
|
|
120
|
+
);
|
|
121
|
+
for (const photo of response.Photos) {
|
|
122
|
+
const nodeUid = makeNodeUid(volumeId, photo.LinkID);
|
|
123
|
+
yield {
|
|
124
|
+
nodeUid,
|
|
125
|
+
captureTime: new Date(photo.CaptureTime * 1000),
|
|
126
|
+
tags: photo.Tags,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!response.Photos.length) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
anchor = response.Photos[response.Photos.length - 1].LinkID;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async *iterateAlbums(
|
|
138
|
+
volumeId: string,
|
|
139
|
+
signal?: AbortSignal,
|
|
140
|
+
): AsyncGenerator<{
|
|
141
|
+
albumUid: string;
|
|
142
|
+
coverNodeUid?: string;
|
|
143
|
+
photoCount: number;
|
|
144
|
+
lastActivityTime: Date;
|
|
145
|
+
}> {
|
|
146
|
+
let anchor = '';
|
|
147
|
+
while (true) {
|
|
148
|
+
const response = await this.apiService.get<GetAlbumsResponse>(
|
|
149
|
+
`drive/photos/volumes/${volumeId}/albums?${anchor ? `AnchorID=${anchor}` : ''}`,
|
|
150
|
+
signal,
|
|
151
|
+
);
|
|
152
|
+
for (const album of response.Albums) {
|
|
153
|
+
const albumUid = makeNodeUid(volumeId, album.LinkID);
|
|
154
|
+
yield {
|
|
155
|
+
albumUid,
|
|
156
|
+
coverNodeUid: album.CoverLinkID ? makeNodeUid(volumeId, album.CoverLinkID) : undefined,
|
|
157
|
+
photoCount: album.PhotoCount,
|
|
158
|
+
lastActivityTime: new Date(album.LastActivityTime * 1000),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!response.More || !response.AnchorID) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
anchor = response.AnchorID;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
13
168
|
}
|
|
@@ -1,23 +1,68 @@
|
|
|
1
1
|
import { DriveAPIService } from '../apiService';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { DriveCrypto } from '../../crypto';
|
|
3
|
+
import {
|
|
4
|
+
ProtonDriveAccount,
|
|
5
|
+
ProtonDriveCryptoCache,
|
|
6
|
+
ProtonDriveEntitiesCache,
|
|
7
|
+
ProtonDriveTelemetry,
|
|
8
|
+
} from '../../interface';
|
|
9
|
+
import { SharesCache } from '../shares/cache';
|
|
10
|
+
import { SharesCryptoCache } from '../shares/cryptoCache';
|
|
11
|
+
import { SharesCryptoService } from '../shares/cryptoService';
|
|
6
12
|
import { Albums } from './albums';
|
|
7
|
-
import {
|
|
13
|
+
import { PhotosAPIService } from './apiService';
|
|
14
|
+
import { NodesService, SharesService } from './interface';
|
|
15
|
+
import { PhotoSharesManager } from './shares';
|
|
16
|
+
import { PhotosTimeline } from './timeline';
|
|
8
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Provides facade for the whole photos module.
|
|
20
|
+
*
|
|
21
|
+
* The photos module is responsible for handling photos and albums metadata,
|
|
22
|
+
* including API communication, crypto, caching, and event handling.
|
|
23
|
+
*/
|
|
9
24
|
export function initPhotosModule(
|
|
10
25
|
apiService: DriveAPIService,
|
|
11
|
-
|
|
26
|
+
photoShares: PhotoSharesManager,
|
|
12
27
|
nodesService: NodesService,
|
|
13
28
|
) {
|
|
14
29
|
const api = new PhotosAPIService(apiService);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const albums = new Albums(api, cache, nodesService);
|
|
30
|
+
const timeline = new PhotosTimeline(api, photoShares);
|
|
31
|
+
const albums = new Albums(api, photoShares, nodesService);
|
|
18
32
|
|
|
19
33
|
return {
|
|
20
34
|
timeline,
|
|
21
35
|
albums,
|
|
22
36
|
};
|
|
23
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Provides facade for the photo share module.
|
|
41
|
+
*
|
|
42
|
+
* The photo share wraps the core share module, but uses photos volume instead
|
|
43
|
+
* of main volume. It provides the same interface so it can be used in the same
|
|
44
|
+
* way in various modules that use shares.
|
|
45
|
+
*/
|
|
46
|
+
export function initPhotoSharesModule(
|
|
47
|
+
telemetry: ProtonDriveTelemetry,
|
|
48
|
+
apiService: DriveAPIService,
|
|
49
|
+
driveEntitiesCache: ProtonDriveEntitiesCache,
|
|
50
|
+
driveCryptoCache: ProtonDriveCryptoCache,
|
|
51
|
+
account: ProtonDriveAccount,
|
|
52
|
+
crypto: DriveCrypto,
|
|
53
|
+
sharesService: SharesService,
|
|
54
|
+
) {
|
|
55
|
+
const api = new PhotosAPIService(apiService);
|
|
56
|
+
const cache = new SharesCache(telemetry.getLogger('shares-cache'), driveEntitiesCache);
|
|
57
|
+
const cryptoCache = new SharesCryptoCache(telemetry.getLogger('shares-cache'), driveCryptoCache);
|
|
58
|
+
const cryptoService = new SharesCryptoService(telemetry, crypto, account);
|
|
59
|
+
|
|
60
|
+
return new PhotoSharesManager(
|
|
61
|
+
telemetry.getLogger('photos-shares'),
|
|
62
|
+
api,
|
|
63
|
+
cache,
|
|
64
|
+
cryptoCache,
|
|
65
|
+
cryptoService,
|
|
66
|
+
sharesService,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PrivateKey } from '../../crypto';
|
|
2
|
+
import { MissingNode, MetricVolumeType } from '../../interface';
|
|
2
3
|
import { DecryptedNode } from '../nodes';
|
|
4
|
+
import { EncryptedShare } from '../shares';
|
|
5
|
+
|
|
6
|
+
export interface SharesService {
|
|
7
|
+
getOwnVolumeIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
|
|
8
|
+
loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
|
|
9
|
+
getSharePrivateKey(shareId: string): Promise<PrivateKey>;
|
|
10
|
+
getMyFilesShareMemberEmailKey(): Promise<{
|
|
11
|
+
email: string;
|
|
12
|
+
addressId: string;
|
|
13
|
+
addressKey: PrivateKey;
|
|
14
|
+
addressKeyId: string;
|
|
15
|
+
}>;
|
|
16
|
+
getContextShareMemberEmailKey(shareId: string): Promise<{
|
|
17
|
+
email: string;
|
|
18
|
+
addressId: string;
|
|
19
|
+
addressKey: PrivateKey;
|
|
20
|
+
addressKeyId: string;
|
|
21
|
+
}>;
|
|
22
|
+
isOwnVolume(volumeId: string): Promise<boolean>;
|
|
23
|
+
getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType>;
|
|
24
|
+
}
|
|
3
25
|
|
|
4
26
|
export interface NodesService {
|
|
5
27
|
getNode(nodeUid: string): Promise<DecryptedNode>;
|