@protontech/drive-sdk 0.0.12 → 0.0.13
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/errors.d.ts +7 -3
- package/dist/errors.js +9 -4
- package/dist/errors.js.map +1 -1
- package/dist/interface/index.d.ts +1 -1
- package/dist/interface/nodes.d.ts +12 -1
- package/dist/interface/nodes.js +11 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/upload.d.ts +51 -3
- package/dist/internal/apiService/driveTypes.d.ts +1341 -465
- package/dist/internal/apiService/errors.js +2 -2
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/transformers.js +2 -0
- package/dist/internal/apiService/transformers.js.map +1 -1
- package/dist/internal/asyncIteratorMap.d.ts +15 -0
- package/dist/internal/asyncIteratorMap.js +59 -0
- package/dist/internal/asyncIteratorMap.js.map +1 -0
- package/dist/internal/asyncIteratorMap.test.d.ts +1 -0
- package/dist/internal/asyncIteratorMap.test.js +120 -0
- package/dist/internal/asyncIteratorMap.test.js.map +1 -0
- package/dist/internal/nodes/apiService.d.ts +2 -2
- package/dist/internal/nodes/apiService.js +16 -6
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +30 -8
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +1 -0
- 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.test.js +34 -0
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/index.test.js +3 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +3 -1
- package/dist/internal/nodes/nodesAccess.js +28 -7
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +7 -6
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/sharing/apiService.js +19 -2
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +49 -53
- package/dist/internal/upload/fileUploader.js +91 -395
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +38 -292
- package/dist/internal/upload/fileUploader.test.js.map +1 -1
- package/dist/internal/upload/index.d.ts +3 -3
- package/dist/internal/upload/index.js +20 -41
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/manager.d.ts +1 -1
- package/dist/internal/upload/manager.js +16 -19
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +42 -83
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +62 -0
- package/dist/internal/upload/streamUploader.js +441 -0
- package/dist/internal/upload/streamUploader.js.map +1 -0
- package/dist/internal/upload/streamUploader.test.d.ts +1 -0
- package/dist/internal/upload/streamUploader.test.js +358 -0
- package/dist/internal/upload/streamUploader.test.js.map +1 -0
- package/dist/protonDriveClient.d.ts +4 -4
- package/dist/protonDriveClient.js +1 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +10 -4
- package/src/interface/index.ts +1 -1
- package/src/interface/nodes.ts +11 -0
- package/src/interface/upload.ts +53 -3
- package/src/internal/apiService/driveTypes.ts +1341 -465
- package/src/internal/apiService/errors.ts +3 -2
- package/src/internal/apiService/transformers.ts +2 -0
- package/src/internal/asyncIteratorMap.test.ts +150 -0
- package/src/internal/asyncIteratorMap.ts +64 -0
- package/src/internal/nodes/apiService.test.ts +36 -7
- package/src/internal/nodes/apiService.ts +19 -7
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/cache.ts +1 -0
- package/src/internal/nodes/cryptoService.test.ts +38 -0
- package/src/internal/nodes/index.test.ts +3 -1
- package/src/internal/nodes/interface.ts +4 -1
- package/src/internal/nodes/nodesAccess.test.ts +7 -6
- package/src/internal/nodes/nodesAccess.ts +30 -7
- package/src/internal/sharing/apiService.ts +24 -2
- package/src/internal/upload/fileUploader.test.ts +46 -376
- package/src/internal/upload/fileUploader.ts +114 -494
- package/src/internal/upload/index.ts +26 -50
- package/src/internal/upload/manager.test.ts +45 -92
- package/src/internal/upload/manager.ts +30 -32
- package/src/internal/upload/streamUploader.test.ts +469 -0
- package/src/internal/upload/streamUploader.ts +552 -0
- package/src/protonDriveClient.ts +5 -4
|
@@ -14,6 +14,7 @@ export function apiErrorFactory({ response, result }: { response: Response, resu
|
|
|
14
14
|
const typedResult = result as {
|
|
15
15
|
Code?: number;
|
|
16
16
|
Error?: string;
|
|
17
|
+
Details?: object;
|
|
17
18
|
exception?: string;
|
|
18
19
|
message?: string;
|
|
19
20
|
file?: string;
|
|
@@ -21,7 +22,7 @@ export function apiErrorFactory({ response, result }: { response: Response, resu
|
|
|
21
22
|
trace?: object;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
const [code, message] = [typedResult.Code || 0, typedResult.Error || c('Error').t`Unknown error
|
|
25
|
+
const [code, message, details] = [typedResult.Code || 0, typedResult.Error || c('Error').t`Unknown error`, typedResult.Details];
|
|
25
26
|
|
|
26
27
|
const debug = typedResult.exception ? {
|
|
27
28
|
exception: typedResult.exception,
|
|
@@ -55,7 +56,7 @@ export function apiErrorFactory({ response, result }: { response: Response, resu
|
|
|
55
56
|
case ErrorCode.INSUFFICIENT_SHARE_QUOTA:
|
|
56
57
|
case ErrorCode.INSUFFICIENT_SHARE_JOINED_QUOTA:
|
|
57
58
|
case ErrorCode.INSUFFICIENT_BOOKMARKS_QUOTA:
|
|
58
|
-
return new ValidationError(message, code);
|
|
59
|
+
return new ValidationError(message, code, details);
|
|
59
60
|
default:
|
|
60
61
|
return new APICodeError(message, code, debug);
|
|
61
62
|
}
|
|
@@ -6,6 +6,8 @@ export function nodeTypeNumberToNodeType(logger: Logger, nodeTypeNumber: number)
|
|
|
6
6
|
return NodeType.Folder;
|
|
7
7
|
case 2:
|
|
8
8
|
return NodeType.File;
|
|
9
|
+
case 3:
|
|
10
|
+
return NodeType.Album;
|
|
9
11
|
default:
|
|
10
12
|
logger.warn(`Unknown node type: ${nodeTypeNumber}`);
|
|
11
13
|
return NodeType.File;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { asyncIteratorMap } from './asyncIteratorMap';
|
|
2
|
+
|
|
3
|
+
// Helper function to create an async generator from array
|
|
4
|
+
async function* createAsyncGenerator<T>(items: T[]): AsyncGenerator<T> {
|
|
5
|
+
for (const item of items) {
|
|
6
|
+
yield item;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Helper function to collect all results from async generator
|
|
11
|
+
async function collectResults<T>(asyncGen: AsyncGenerator<T>): Promise<T[]> {
|
|
12
|
+
const results: T[] = [];
|
|
13
|
+
for await (const item of asyncGen) {
|
|
14
|
+
results.push(item);
|
|
15
|
+
}
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('asyncIteratorMap', () => {
|
|
20
|
+
test('works with empty input', async () => {
|
|
21
|
+
const inputGen = createAsyncGenerator([]);
|
|
22
|
+
const mapper = async (x: number) => x * 2;
|
|
23
|
+
|
|
24
|
+
const mappedGen = asyncIteratorMap(inputGen, mapper);
|
|
25
|
+
const results = await collectResults(mappedGen);
|
|
26
|
+
|
|
27
|
+
expect(results).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('works with single item', async () => {
|
|
31
|
+
const inputGen = createAsyncGenerator([42]);
|
|
32
|
+
const mapper = async (x: number) => x * 2;
|
|
33
|
+
|
|
34
|
+
const mappedGen = asyncIteratorMap(inputGen, mapper);
|
|
35
|
+
const results = await collectResults(mappedGen);
|
|
36
|
+
|
|
37
|
+
expect(results).toEqual([84]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('works with 5 values', async () => {
|
|
41
|
+
const inputGen = createAsyncGenerator([1, 2, 3, 4, 5]);
|
|
42
|
+
const mapper = async (x: number) => x * 2;
|
|
43
|
+
|
|
44
|
+
const mappedGen = asyncIteratorMap(inputGen, mapper);
|
|
45
|
+
const results = await collectResults(mappedGen);
|
|
46
|
+
|
|
47
|
+
expect(results).toEqual([2, 4, 6, 8, 10]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('works with slow mapper - finishes as fast as the longest delay', async () => {
|
|
51
|
+
const delays: { [key: number]: number } = { 1: 100, 2: 50, 3: 200, 4: 30, 5: 80 };
|
|
52
|
+
const inputGen = createAsyncGenerator(Object.keys(delays).map(Number));
|
|
53
|
+
|
|
54
|
+
const slowMapper = async (x: number) => {
|
|
55
|
+
await new Promise(resolve => setTimeout(resolve, delays[x]));
|
|
56
|
+
return x * 2;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
const mappedGen = asyncIteratorMap(inputGen, slowMapper, 5);
|
|
61
|
+
const results = await collectResults(mappedGen);
|
|
62
|
+
const endTime = Date.now();
|
|
63
|
+
|
|
64
|
+
// Should complete in roughly the time of the longest delay (200ms) plus some overhead
|
|
65
|
+
const executionTime = endTime - startTime;
|
|
66
|
+
expect(executionTime).toBeGreaterThanOrEqual(195); // We had failures with 199ms - JS is not precise.
|
|
67
|
+
expect(executionTime).toBeLessThan(250);
|
|
68
|
+
|
|
69
|
+
// Results should be in the order of the delays
|
|
70
|
+
expect(results).toEqual([8, 4, 10, 2, 6]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('handles errors from input iterator properly', async () => {
|
|
74
|
+
const throwingInputGen = async function*() {
|
|
75
|
+
yield 1;
|
|
76
|
+
yield 2;
|
|
77
|
+
throw new Error('Error providing value: 3');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const mapper = async (x: number) => x * 2;
|
|
81
|
+
|
|
82
|
+
const mappedGen = asyncIteratorMap(throwingInputGen(), mapper);
|
|
83
|
+
|
|
84
|
+
const results: number[] = [];
|
|
85
|
+
let caughtError: Error | null = null;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
for await (const item of mappedGen) {
|
|
89
|
+
results.push(item);
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
caughtError = error as Error;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expect(caughtError?.message).toBe('Error providing value: 3');
|
|
96
|
+
expect(results).toEqual([2, 4]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('handles errors from mapper properly', async () => {
|
|
100
|
+
const inputGen = createAsyncGenerator([1, 2, 3, 4, 5]);
|
|
101
|
+
|
|
102
|
+
const throwingMapper = async (x: number) => {
|
|
103
|
+
if (x === 3) {
|
|
104
|
+
throw new Error(`Error processing value: ${x}`);
|
|
105
|
+
}
|
|
106
|
+
return x * 2;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const mappedGen = asyncIteratorMap(inputGen, throwingMapper);
|
|
110
|
+
|
|
111
|
+
const results: number[] = [];
|
|
112
|
+
let caughtError: Error | null = null;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
for await (const item of mappedGen) {
|
|
116
|
+
results.push(item);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
caughtError = error as Error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
expect(caughtError?.message).toBe('Error processing value: 3');
|
|
123
|
+
expect(results).toEqual([2, 4]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('respects concurrency limit', async () => {
|
|
127
|
+
const inputGen = createAsyncGenerator([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
128
|
+
|
|
129
|
+
let concurrentExecutions = 0;
|
|
130
|
+
let maxConcurrentExecutions = 0;
|
|
131
|
+
|
|
132
|
+
const mapper = async (x: number) => {
|
|
133
|
+
concurrentExecutions++;
|
|
134
|
+
maxConcurrentExecutions = Math.max(maxConcurrentExecutions, concurrentExecutions);
|
|
135
|
+
|
|
136
|
+
// Wait for 100ms to simulate work
|
|
137
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
138
|
+
|
|
139
|
+
concurrentExecutions--;
|
|
140
|
+
return x * 2;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const concurrencyLimit = 3;
|
|
144
|
+
const mappedGen = asyncIteratorMap(inputGen, mapper, concurrencyLimit);
|
|
145
|
+
const results = await collectResults(mappedGen);
|
|
146
|
+
|
|
147
|
+
expect(maxConcurrentExecutions).toBe(concurrencyLimit);
|
|
148
|
+
expect(results).toEqual([2, 4, 6, 8, 10, 12, 14, 16]);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const DEFAULT_CONCURRENCY = 10;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps values from an input iterator and produces a new iterator.
|
|
5
|
+
* The mapper function is not awaited immediately to allow for parallel
|
|
6
|
+
* execution. The order of the items in the output iterator is not the
|
|
7
|
+
* same as the order of the items in the input iterator.
|
|
8
|
+
*
|
|
9
|
+
* Any error from the input iterator or the mapper function is propagated
|
|
10
|
+
* to the output iterator.
|
|
11
|
+
*
|
|
12
|
+
* @param inputIterator - The input async iterator.
|
|
13
|
+
* @param mapper - The mapper function that maps the input values to output values.
|
|
14
|
+
* @param concurrency - The concurrency limit. How many parallel async mapper calls are allowed.
|
|
15
|
+
* @returns An async iterator that yields the mapped values.
|
|
16
|
+
*/
|
|
17
|
+
export async function* asyncIteratorMap<I, O>(
|
|
18
|
+
inputIterator: AsyncGenerator<I>,
|
|
19
|
+
mapper: (item: I) => Promise<O>,
|
|
20
|
+
concurrency: number = DEFAULT_CONCURRENCY,
|
|
21
|
+
): AsyncGenerator<O> {
|
|
22
|
+
let done = false;
|
|
23
|
+
|
|
24
|
+
const executing = new Set<Promise<void>>();
|
|
25
|
+
const results: Array<Promise<O>> = [];
|
|
26
|
+
|
|
27
|
+
const pump = async () => {
|
|
28
|
+
let next;
|
|
29
|
+
try {
|
|
30
|
+
next = await inputIterator.next();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
results.push(Promise.reject(error));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (next.done) {
|
|
37
|
+
done = true;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const promise = mapper(next.value)
|
|
42
|
+
.then((result) => {
|
|
43
|
+
results.push(Promise.resolve(result));
|
|
44
|
+
})
|
|
45
|
+
.catch((error) => {
|
|
46
|
+
results.push(Promise.reject(error));
|
|
47
|
+
});
|
|
48
|
+
executing.add(promise);
|
|
49
|
+
void promise.finally(() => executing.delete(promise));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
while (!done || executing.size > 0 || results.length > 0) {
|
|
53
|
+
while (!done && executing.size < concurrency) {
|
|
54
|
+
await pump();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (results.length > 0) {
|
|
58
|
+
yield await results.shift()!;
|
|
59
|
+
} else if (executing.size > 0) {
|
|
60
|
+
// Wait for at least one task to complete
|
|
61
|
+
await Promise.race(Array.from(executing));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -44,6 +44,18 @@ function generateAPIFolderNode(linkOverrides = {}, overrides = {}) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function generateAPIAlbumNode(linkOverrides = {}, overrides = {}) {
|
|
48
|
+
const node = generateAPINode();
|
|
49
|
+
return {
|
|
50
|
+
Link: {
|
|
51
|
+
...node.Link,
|
|
52
|
+
Type: 3,
|
|
53
|
+
...linkOverrides,
|
|
54
|
+
},
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
function generateAPINode() {
|
|
48
60
|
return {
|
|
49
61
|
Link: {
|
|
@@ -107,6 +119,15 @@ function generateFolderNode(overrides = {}) {
|
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
function generateAlbumNode(overrides = {}) {
|
|
123
|
+
const node = generateNode();
|
|
124
|
+
return {
|
|
125
|
+
...node,
|
|
126
|
+
type: NodeType.Album,
|
|
127
|
+
...overrides
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
110
131
|
function generateNode() {
|
|
111
132
|
return {
|
|
112
133
|
hash: "nameHash",
|
|
@@ -119,7 +140,7 @@ function generateNode() {
|
|
|
119
140
|
|
|
120
141
|
shareId: undefined,
|
|
121
142
|
isShared: false,
|
|
122
|
-
directMemberRole: MemberRole.
|
|
143
|
+
directMemberRole: MemberRole.Admin,
|
|
123
144
|
|
|
124
145
|
encryptedCrypto: {
|
|
125
146
|
armoredKey: "nodeKey",
|
|
@@ -149,13 +170,13 @@ describe("nodeAPIService", () => {
|
|
|
149
170
|
});
|
|
150
171
|
|
|
151
172
|
describe('iterateNodes', () => {
|
|
152
|
-
async function testIterateNodes(mockedLink: any, expectedNode: any) {
|
|
173
|
+
async function testIterateNodes(mockedLink: any, expectedNode: any, ownVolumeId = 'volumeId') {
|
|
153
174
|
// @ts-expect-error Mocking for testing purposes
|
|
154
175
|
apiMock.post = jest.fn(async () => Promise.resolve({
|
|
155
176
|
Links: [mockedLink],
|
|
156
177
|
}));
|
|
157
178
|
|
|
158
|
-
const nodes = await Array.fromAsync(api.iterateNodes(['volumeId~nodeId']));
|
|
179
|
+
const nodes = await Array.fromAsync(api.iterateNodes(['volumeId~nodeId'], ownVolumeId));
|
|
159
180
|
expect(nodes).toStrictEqual([expectedNode]);
|
|
160
181
|
}
|
|
161
182
|
|
|
@@ -180,6 +201,13 @@ describe("nodeAPIService", () => {
|
|
|
180
201
|
);
|
|
181
202
|
});
|
|
182
203
|
|
|
204
|
+
it('should get album node', async () => {
|
|
205
|
+
await testIterateNodes(
|
|
206
|
+
generateAPIAlbumNode(),
|
|
207
|
+
generateAlbumNode(),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
183
211
|
it('should get shared node', async () => {
|
|
184
212
|
await testIterateNodes(
|
|
185
213
|
generateAPIFolderNode({}, {
|
|
@@ -213,6 +241,7 @@ describe("nodeAPIService", () => {
|
|
|
213
241
|
shareId: 'shareId',
|
|
214
242
|
directMemberRole: MemberRole.Viewer,
|
|
215
243
|
}),
|
|
244
|
+
'myVolumeId',
|
|
216
245
|
);
|
|
217
246
|
});
|
|
218
247
|
|
|
@@ -240,7 +269,7 @@ describe("nodeAPIService", () => {
|
|
|
240
269
|
],
|
|
241
270
|
}));
|
|
242
271
|
|
|
243
|
-
const generator = api.iterateNodes(['volumeId~nodeId']);
|
|
272
|
+
const generator = api.iterateNodes(['volumeId~nodeId'], 'volumeId');
|
|
244
273
|
|
|
245
274
|
const node1 = await generator.next();
|
|
246
275
|
expect(node1.value).toStrictEqual(generateFolderNode());
|
|
@@ -272,10 +301,10 @@ describe("nodeAPIService", () => {
|
|
|
272
301
|
],
|
|
273
302
|
}));
|
|
274
303
|
|
|
275
|
-
const nodes = await Array.fromAsync(api.iterateNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2']));
|
|
304
|
+
const nodes = await Array.fromAsync(api.iterateNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2'], 'volumeId1'));
|
|
276
305
|
expect(nodes).toStrictEqual([
|
|
277
|
-
generateFolderNode({ uid: 'volumeId1~nodeId1', parentUid: 'volumeId1~parentNodeId1' }),
|
|
278
|
-
generateFolderNode({ uid: 'volumeId2~nodeId2', parentUid: 'volumeId2~parentNodeId2' }),
|
|
306
|
+
generateFolderNode({ uid: 'volumeId1~nodeId1', parentUid: 'volumeId1~parentNodeId1', directMemberRole: MemberRole.Admin }),
|
|
307
|
+
generateFolderNode({ uid: 'volumeId2~nodeId2', parentUid: 'volumeId2~parentNodeId2', directMemberRole: MemberRole.Inherited }),
|
|
279
308
|
]);
|
|
280
309
|
});
|
|
281
310
|
});
|
|
@@ -2,7 +2,7 @@ import { c } from "ttag";
|
|
|
2
2
|
|
|
3
3
|
import { ProtonDriveError, ValidationError } from "../../errors";
|
|
4
4
|
import { Logger, NodeResult } from "../../interface";
|
|
5
|
-
import { RevisionState } from "../../interface/nodes";
|
|
5
|
+
import { MemberRole, RevisionState } from "../../interface/nodes";
|
|
6
6
|
import { DriveAPIService, drivePaths, isCodeOk, nodeTypeNumberToNodeType, permissionsToDirectMemberRole } from "../apiService";
|
|
7
7
|
import { splitNodeUid, makeNodeUid, makeNodeRevisionUid, splitNodeRevisionUid, makeNodeThumbnailUid } from "../uids";
|
|
8
8
|
import { EncryptedNode, EncryptedRevision, Thumbnail } from "./interface";
|
|
@@ -56,15 +56,15 @@ export class NodeAPIService {
|
|
|
56
56
|
this.apiService = apiService;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
async getNode(nodeUid: string, signal?: AbortSignal): Promise<EncryptedNode> {
|
|
60
|
-
const nodesGenerator = this.iterateNodes([nodeUid], signal);
|
|
59
|
+
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
|
|
60
|
+
const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, signal);
|
|
61
61
|
const result = await nodesGenerator.next();
|
|
62
62
|
await nodesGenerator.return("finish");
|
|
63
63
|
return result.value;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Improvement requested: split into multiple calls for many nodes.
|
|
67
|
-
async* iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
|
|
67
|
+
async* iterateNodes(nodeUids: string[], ownVolumeId: string, signal?: AbortSignal): AsyncGenerator<EncryptedNode> {
|
|
68
68
|
const allNodeIds = nodeUids.map(splitNodeUid);
|
|
69
69
|
|
|
70
70
|
const nodeIdsByVolumeId = new Map<string, string[]>();
|
|
@@ -81,13 +81,15 @@ export class NodeAPIService {
|
|
|
81
81
|
const errors = [];
|
|
82
82
|
|
|
83
83
|
for (const [volumeId, nodeIds] of nodeIdsByVolumeId.entries()) {
|
|
84
|
+
const isAdmin = volumeId === ownVolumeId;
|
|
85
|
+
|
|
84
86
|
const response = await this.apiService.post<PostLoadLinksMetadataRequest, PostLoadLinksMetadataResponse>(`drive/v2/volumes/${volumeId}/links`, {
|
|
85
87
|
LinkIDs: nodeIds,
|
|
86
88
|
}, signal);
|
|
87
89
|
|
|
88
90
|
for (const link of response.Links) {
|
|
89
91
|
try {
|
|
90
|
-
yield linkToEncryptedNode(this.logger, volumeId, link);
|
|
92
|
+
yield linkToEncryptedNode(this.logger, volumeId, link, isAdmin);
|
|
91
93
|
} catch (error: unknown) {
|
|
92
94
|
this.logger.error(`Failed to transform node ${link.Link.LinkID}`, error);
|
|
93
95
|
errors.push(error);
|
|
@@ -363,7 +365,7 @@ function* handleResponseErrors(nodeUids: string[], volumeId: string, responses:
|
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
|
|
366
|
-
function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLinksMetadataResponse['Links'][0]): EncryptedNode {
|
|
368
|
+
function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLinksMetadataResponse['Links'][0], isAdmin: boolean): EncryptedNode {
|
|
367
369
|
const baseNodeMetadata = {
|
|
368
370
|
// Internal metadata
|
|
369
371
|
hash: link.Link.NameHash || undefined,
|
|
@@ -379,7 +381,7 @@ function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLin
|
|
|
379
381
|
// Sharing node metadata
|
|
380
382
|
shareId: link.Sharing?.ShareID || undefined,
|
|
381
383
|
isShared: !!link.Sharing,
|
|
382
|
-
directMemberRole: permissionsToDirectMemberRole(logger, link.Membership?.Permissions),
|
|
384
|
+
directMemberRole: isAdmin ? MemberRole.Admin : permissionsToDirectMemberRole(logger, link.Membership?.Permissions),
|
|
383
385
|
}
|
|
384
386
|
const baseCryptoNodeMetadata = {
|
|
385
387
|
signatureEmail: link.Link.SignatureEmail || undefined,
|
|
@@ -426,6 +428,15 @@ function linkToEncryptedNode(logger: Logger, volumeId: string, link: PostLoadLin
|
|
|
426
428
|
}
|
|
427
429
|
}
|
|
428
430
|
|
|
431
|
+
if (link.Link.Type === 3) {
|
|
432
|
+
return {
|
|
433
|
+
...baseNodeMetadata,
|
|
434
|
+
encryptedCrypto: {
|
|
435
|
+
...baseCryptoNodeMetadata,
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
429
440
|
throw new Error(`Unknown node type: ${link.Link.Type}`);
|
|
430
441
|
}
|
|
431
442
|
|
|
@@ -437,6 +448,7 @@ function transformRevisionResponse(
|
|
|
437
448
|
return {
|
|
438
449
|
uid: makeNodeRevisionUid(volumeId, nodeId, revision.ID),
|
|
439
450
|
state: revision.State === APIRevisionState.Active ? RevisionState.Active : RevisionState.Superseded,
|
|
451
|
+
// @ts-expect-error: API doc is wrong, CreateTime is not optional.
|
|
440
452
|
creationTime: new Date(revision.CreateTime*1000),
|
|
441
453
|
storageSize: revision.Size,
|
|
442
454
|
signatureEmail: revision.SignatureEmail || undefined,
|
|
@@ -108,6 +108,7 @@ describe('nodesCache', () => {
|
|
|
108
108
|
creationTime: new Date('2021-01-01'),
|
|
109
109
|
storageSize: 100,
|
|
110
110
|
contentAuthor: resultOk('test@test.com'),
|
|
111
|
+
claimedModificationTime: new Date('2021-02-01')
|
|
111
112
|
});
|
|
112
113
|
const node = generateNode('node1', '', { activeRevision });
|
|
113
114
|
|
|
@@ -261,6 +261,7 @@ function deserialiseRevision(revision: any): Result<DecryptedRevision, Error> {
|
|
|
261
261
|
return resultOk({
|
|
262
262
|
...revision.value,
|
|
263
263
|
creationTime: new Date(revision.value.creationTime),
|
|
264
|
+
claimedModificationTime: new Date(revision.value.claimedModificationTime)
|
|
264
265
|
});
|
|
265
266
|
}
|
|
266
267
|
|
|
@@ -578,6 +578,44 @@ describe("nodesCryptoService", () => {
|
|
|
578
578
|
});
|
|
579
579
|
});
|
|
580
580
|
|
|
581
|
+
describe("album node", () => {
|
|
582
|
+
const encryptedNode = {
|
|
583
|
+
uid: "volumeId~nodeId",
|
|
584
|
+
parentUid: "volumeId~parentId",
|
|
585
|
+
encryptedCrypto: {
|
|
586
|
+
signatureEmail: "signatureEmail",
|
|
587
|
+
nameSignatureEmail: "nameSignatureEmail",
|
|
588
|
+
armoredKey: "armoredKey",
|
|
589
|
+
armoredNodePassphrase: "armoredNodePassphrase",
|
|
590
|
+
armoredNodePassphraseSignature: "armoredNodePassphraseSignature",
|
|
591
|
+
},
|
|
592
|
+
} as EncryptedNode;
|
|
593
|
+
|
|
594
|
+
it("should decrypt successfuly", async () => {
|
|
595
|
+
const result = await cryptoService.decryptNode(encryptedNode, parentKey);
|
|
596
|
+
|
|
597
|
+
expect(result).toMatchObject({
|
|
598
|
+
node: {
|
|
599
|
+
name: { ok: true, value: "name" },
|
|
600
|
+
keyAuthor: { ok: true, value: "signatureEmail" },
|
|
601
|
+
nameAuthor: { ok: true, value: "nameSignatureEmail" },
|
|
602
|
+
folder: undefined,
|
|
603
|
+
activeRevision: undefined,
|
|
604
|
+
errors: undefined,
|
|
605
|
+
},
|
|
606
|
+
keys: {
|
|
607
|
+
passphrase: "pass",
|
|
608
|
+
key: "decryptedKey",
|
|
609
|
+
passphraseSessionKey: "passphraseSessionKey",
|
|
610
|
+
hashKey: new Uint8Array(),
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(account.getPublicKeys).toHaveBeenCalledTimes(2);
|
|
615
|
+
expect(telemetry.logEvent).not.toHaveBeenCalled();
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
581
619
|
describe("anonymous node", () => {
|
|
582
620
|
const encryptedNode = {
|
|
583
621
|
uid: "volumeId~nodeId",
|
|
@@ -55,7 +55,9 @@ describe('nodesModules integration tests', () => {
|
|
|
55
55
|
}),
|
|
56
56
|
}
|
|
57
57
|
// @ts-expect-error No need to implement all methods for mocking
|
|
58
|
-
sharesService = {
|
|
58
|
+
sharesService = {
|
|
59
|
+
getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
60
|
+
}
|
|
59
61
|
|
|
60
62
|
nodesModule = initNodesModule(
|
|
61
63
|
getMockTelemetry(),
|
|
@@ -30,7 +30,7 @@ interface BaseNode {
|
|
|
30
30
|
* Outside of the module, the decrypted node interface should be used.
|
|
31
31
|
*/
|
|
32
32
|
export interface EncryptedNode extends BaseNode {
|
|
33
|
-
encryptedCrypto: EncryptedNodeFolderCrypto | EncryptedNodeFileCrypto;
|
|
33
|
+
encryptedCrypto: EncryptedNodeFolderCrypto | EncryptedNodeFileCrypto | EncryptedNodeAlbumCrypto;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface EncryptedNodeCrypto {
|
|
@@ -56,6 +56,9 @@ export interface EncryptedNodeFolderCrypto extends EncryptedNodeCrypto {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
60
|
+
export interface EncryptedNodeAlbumCrypto extends EncryptedNodeCrypto {}
|
|
61
|
+
|
|
59
62
|
/**
|
|
60
63
|
* Interface used only internally in the nodes module.
|
|
61
64
|
*
|
|
@@ -46,6 +46,7 @@ describe('nodesAccess', () => {
|
|
|
46
46
|
}
|
|
47
47
|
// @ts-expect-error No need to implement all methods for mocking
|
|
48
48
|
shareService = {
|
|
49
|
+
getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
49
50
|
getSharePrivateKey: jest.fn(),
|
|
50
51
|
};
|
|
51
52
|
|
|
@@ -81,7 +82,7 @@ describe('nodesAccess', () => {
|
|
|
81
82
|
|
|
82
83
|
const result = await access.getNode('nodeId');
|
|
83
84
|
expect(result).toEqual(decryptedNode);
|
|
84
|
-
expect(apiService.getNode).toHaveBeenCalledWith('nodeId');
|
|
85
|
+
expect(apiService.getNode).toHaveBeenCalledWith('nodeId', 'volumeId');
|
|
85
86
|
expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('parentUid');
|
|
86
87
|
expect(cryptoService.decryptNode).toHaveBeenCalledWith(encryptedNode, 'parentKey');
|
|
87
88
|
expect(cache.setNode).toHaveBeenCalledWith(decryptedNode);
|
|
@@ -107,7 +108,7 @@ describe('nodesAccess', () => {
|
|
|
107
108
|
|
|
108
109
|
const result = await access.getNode('nodeId');
|
|
109
110
|
expect(result).toEqual(decryptedNode);
|
|
110
|
-
expect(apiService.getNode).toHaveBeenCalledWith('nodeId');
|
|
111
|
+
expect(apiService.getNode).toHaveBeenCalledWith('nodeId', 'volumeId');
|
|
111
112
|
expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('parentUid');
|
|
112
113
|
expect(cryptoService.decryptNode).toHaveBeenCalledWith(encryptedNode, 'parentKey');
|
|
113
114
|
expect(cache.setNode).toHaveBeenCalledWith(decryptedNode);
|
|
@@ -179,7 +180,7 @@ describe('nodesAccess', () => {
|
|
|
179
180
|
|
|
180
181
|
const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
|
|
181
182
|
expect(result).toMatchObject([node1, node4, node2, node3]);
|
|
182
|
-
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], undefined);
|
|
183
|
+
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], 'volumeId', undefined);
|
|
183
184
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(2);
|
|
184
185
|
expect(cache.setNode).toHaveBeenCalledTimes(2);
|
|
185
186
|
expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(2);
|
|
@@ -218,7 +219,7 @@ describe('nodesAccess', () => {
|
|
|
218
219
|
const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
|
|
219
220
|
expect(result).toMatchObject([node1, node2, node3, node4]);
|
|
220
221
|
expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('parentUid', undefined);
|
|
221
|
-
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], undefined);
|
|
222
|
+
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], 'volumeId', undefined);
|
|
222
223
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
|
|
223
224
|
expect(cache.setNode).toHaveBeenCalledTimes(4);
|
|
224
225
|
expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(4);
|
|
@@ -320,7 +321,7 @@ describe('nodesAccess', () => {
|
|
|
320
321
|
const result = await Array.fromAsync(access.iterateTrashedNodes());
|
|
321
322
|
expect(result).toMatchObject([node1, node2, node3, node4]);
|
|
322
323
|
expect(apiService.iterateTrashedNodeUids).toHaveBeenCalledWith(volumeId, undefined);
|
|
323
|
-
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], undefined);
|
|
324
|
+
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], volumeId, undefined);
|
|
324
325
|
expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
|
|
325
326
|
expect(cache.setNode).toHaveBeenCalledTimes(4);
|
|
326
327
|
expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(4);
|
|
@@ -370,7 +371,7 @@ describe('nodesAccess', () => {
|
|
|
370
371
|
|
|
371
372
|
const result = await Array.fromAsync(access.iterateNodes(['node1', 'node2', 'node3', 'node4']));
|
|
372
373
|
expect(result).toMatchObject([node1, node4, node2, node3]);
|
|
373
|
-
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], undefined);
|
|
374
|
+
expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], 'volumeId', undefined);
|
|
374
375
|
});
|
|
375
376
|
|
|
376
377
|
it('should remove from cache if missing on API and return back to caller', async () => {
|