@protontech/drive-sdk 0.4.0 → 0.5.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/diagnostic/sdkDiagnostic.js +1 -1
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/interface/download.d.ts +4 -4
- package/dist/interface/nodes.d.ts +4 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/upload.d.ts +6 -3
- package/dist/internal/apiService/apiService.d.ts +3 -0
- package/dist/internal/apiService/apiService.js +25 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +38 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +31 -48
- package/dist/internal/apiService/errors.js +3 -0
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/errors.test.js +15 -7
- package/dist/internal/apiService/errors.test.js.map +1 -1
- package/dist/internal/asyncIteratorMap.d.ts +1 -1
- package/dist/internal/asyncIteratorMap.js +6 -1
- package/dist/internal/asyncIteratorMap.js.map +1 -1
- package/dist/internal/asyncIteratorMap.test.js +9 -0
- package/dist/internal/asyncIteratorMap.test.js.map +1 -1
- package/dist/internal/download/fileDownloader.d.ts +3 -3
- package/dist/internal/download/fileDownloader.js +5 -5
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +8 -8
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +6 -1
- package/dist/internal/nodes/apiService.js +45 -32
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +164 -17
- 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/debouncer.d.ts +23 -0
- package/dist/internal/nodes/debouncer.js +80 -0
- package/dist/internal/nodes/debouncer.js.map +1 -0
- package/dist/internal/nodes/debouncer.test.d.ts +1 -0
- package/dist/internal/nodes/debouncer.test.js +100 -0
- package/dist/internal/nodes/debouncer.test.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 +1 -0
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +1 -0
- package/dist/internal/nodes/nodesAccess.d.ts +2 -1
- package/dist/internal/nodes/nodesAccess.js +24 -5
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +2 -2
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +1 -0
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/index.d.ts +11 -0
- package/dist/internal/photos/index.js +27 -0
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +60 -0
- package/dist/internal/photos/upload.js +104 -0
- package/dist/internal/photos/upload.js.map +1 -0
- package/dist/internal/sharingPublic/apiService.d.ts +2 -2
- package/dist/internal/sharingPublic/apiService.js +2 -62
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -4
- package/dist/internal/sharingPublic/cryptoCache.js +0 -28
- package/dist/internal/sharingPublic/cryptoCache.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
- package/dist/internal/sharingPublic/cryptoReporter.js +44 -0
- package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
- package/dist/internal/sharingPublic/cryptoService.d.ts +3 -4
- package/dist/internal/sharingPublic/cryptoService.js +5 -43
- package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +21 -3
- package/dist/internal/sharingPublic/index.js +43 -12
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/interface.d.ts +0 -1
- package/dist/internal/sharingPublic/nodes.d.ts +13 -0
- package/dist/internal/sharingPublic/nodes.js +28 -0
- package/dist/internal/sharingPublic/nodes.js.map +1 -0
- package/dist/internal/sharingPublic/session/session.d.ts +3 -3
- package/dist/internal/sharingPublic/session/url.test.js +3 -3
- package/dist/internal/sharingPublic/shares.d.ts +34 -0
- package/dist/internal/sharingPublic/shares.js +69 -0
- package/dist/internal/sharingPublic/shares.js.map +1 -0
- package/dist/internal/upload/apiService.d.ts +2 -2
- package/dist/internal/upload/apiService.js +11 -2
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/controller.d.ts +8 -2
- package/dist/internal/upload/controller.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 +7 -3
- package/dist/internal/upload/fileUploader.js +6 -3
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +23 -11
- package/dist/internal/upload/fileUploader.test.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 +40 -26
- package/dist/internal/upload/streamUploader.js +15 -8
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +11 -7
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +3 -3
- package/dist/protonDriveClient.js +4 -4
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +18 -2
- package/dist/protonDrivePhotosClient.js +19 -2
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +31 -4
- package/dist/protonDrivePublicLinkClient.js +52 -9
- package/dist/protonDrivePublicLinkClient.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/diagnostic/sdkDiagnostic.ts +1 -1
- package/src/interface/download.ts +4 -4
- package/src/interface/nodes.ts +4 -0
- package/src/interface/upload.ts +3 -3
- package/src/internal/apiService/apiService.test.ts +50 -0
- package/src/internal/apiService/apiService.ts +33 -2
- package/src/internal/apiService/driveTypes.ts +31 -48
- package/src/internal/apiService/errors.test.ts +10 -0
- package/src/internal/apiService/errors.ts +5 -1
- package/src/internal/asyncIteratorMap.test.ts +12 -0
- package/src/internal/asyncIteratorMap.ts +8 -0
- package/src/internal/download/fileDownloader.test.ts +8 -8
- package/src/internal/download/fileDownloader.ts +5 -5
- package/src/internal/nodes/apiService.test.ts +222 -16
- package/src/internal/nodes/apiService.ts +63 -49
- package/src/internal/nodes/cache.test.ts +1 -0
- package/src/internal/nodes/debouncer.test.ts +129 -0
- package/src/internal/nodes/debouncer.ts +93 -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 +1 -0
- package/src/internal/nodes/interface.ts +1 -0
- package/src/internal/nodes/nodesAccess.test.ts +2 -2
- package/src/internal/nodes/nodesAccess.ts +30 -5
- package/src/internal/nodes/nodesManagement.ts +1 -0
- package/src/internal/photos/index.ts +62 -0
- package/src/internal/photos/upload.ts +212 -0
- package/src/internal/sharingPublic/apiService.ts +5 -86
- package/src/internal/sharingPublic/cryptoCache.ts +0 -34
- package/src/internal/sharingPublic/cryptoReporter.ts +73 -0
- package/src/internal/sharingPublic/cryptoService.ts +4 -80
- package/src/internal/sharingPublic/index.ts +68 -6
- package/src/internal/sharingPublic/interface.ts +0 -9
- package/src/internal/sharingPublic/nodes.ts +37 -0
- package/src/internal/sharingPublic/session/apiService.ts +1 -1
- package/src/internal/sharingPublic/session/session.ts +3 -3
- package/src/internal/sharingPublic/session/url.test.ts +3 -3
- package/src/internal/sharingPublic/shares.ts +86 -0
- package/src/internal/upload/apiService.ts +15 -4
- package/src/internal/upload/controller.ts +2 -2
- package/src/internal/upload/cryptoService.ts +2 -2
- package/src/internal/upload/fileUploader.test.ts +25 -11
- package/src/internal/upload/fileUploader.ts +16 -3
- 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 +32 -15
- package/src/internal/upload/streamUploader.ts +43 -30
- package/src/protonDriveClient.ts +4 -4
- package/src/protonDrivePhotosClient.ts +46 -6
- package/src/protonDrivePublicLinkClient.ts +93 -12
- package/src/transformers.ts +2 -0
- package/dist/internal/sharingPublic/manager.d.ts +0 -19
- package/dist/internal/sharingPublic/manager.js +0 -81
- package/dist/internal/sharingPublic/manager.js.map +0 -1
- package/src/internal/sharingPublic/manager.ts +0 -86
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Logger } from "../../interface";
|
|
2
|
+
import { LoggerWithPrefix } from '../../telemetry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The timeout for which the node is considered to be loading.
|
|
6
|
+
* If the node is not loaded after this timeout, it is considered to be
|
|
7
|
+
* loaded or failed to be loaded, and allowed other places to proceed.
|
|
8
|
+
*
|
|
9
|
+
* Decrypting many nodes in parallel can take a lot of time, so we allow
|
|
10
|
+
* more time for this.
|
|
11
|
+
*/
|
|
12
|
+
const DEBOUNCE_TIMEOUT = 5000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper to avoid loading the same node twice.
|
|
16
|
+
*
|
|
17
|
+
* Each place that loads a node should report it is being loaded,
|
|
18
|
+
* and when it is finished, it should report it is finished.
|
|
19
|
+
* The finish must be called even if the node fails to be loaded
|
|
20
|
+
* to clear the promise.
|
|
21
|
+
*
|
|
22
|
+
* Each place that loads a node from cache should first wait for
|
|
23
|
+
* the node to be loaded if that is the case.
|
|
24
|
+
*/
|
|
25
|
+
export class NodesDebouncer {
|
|
26
|
+
private promises: Map<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
promise: Promise<void>;
|
|
30
|
+
resolve: () => void;
|
|
31
|
+
timeout: NodeJS.Timeout;
|
|
32
|
+
}
|
|
33
|
+
> = new Map();
|
|
34
|
+
|
|
35
|
+
constructor(private logger: Logger) {
|
|
36
|
+
this.logger = new LoggerWithPrefix(logger, 'debouncer');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
loadingNodes(nodeUids: string[]) {
|
|
40
|
+
for (const nodeUid of nodeUids) {
|
|
41
|
+
this.loadingNode(nodeUid);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
loadingNode(nodeUid: string) {
|
|
46
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
47
|
+
if (this.promises.has(nodeUid)) {
|
|
48
|
+
this.logger.warn(`Loading twice for: ${nodeUid}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const timeout = setTimeout(() => {
|
|
53
|
+
this.logger.warn(`Timeout for: ${nodeUid}`);
|
|
54
|
+
this.finishedLoadingNode(nodeUid);
|
|
55
|
+
}, DEBOUNCE_TIMEOUT);
|
|
56
|
+
this.promises.set(nodeUid, { promise, resolve, timeout });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
finishedLoadingNodes(nodeUids: string[]) {
|
|
60
|
+
for (const nodeUid of nodeUids) {
|
|
61
|
+
this.finishedLoadingNode(nodeUid);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
finishedLoadingNode(nodeUid: string) {
|
|
66
|
+
const result = this.promises.get(nodeUid);
|
|
67
|
+
if (!result) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clearTimeout(result.timeout);
|
|
72
|
+
result.resolve();
|
|
73
|
+
this.promises.delete(nodeUid);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async waitForLoadingNode(nodeUid: string) {
|
|
77
|
+
const result = this.promises.get(nodeUid);
|
|
78
|
+
if (!result) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.logger.debug(`Wait for: ${nodeUid}`);
|
|
83
|
+
await result.promise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
clear() {
|
|
87
|
+
for (const result of this.promises.values()) {
|
|
88
|
+
clearTimeout(result.timeout);
|
|
89
|
+
result.resolve();
|
|
90
|
+
}
|
|
91
|
+
this.promises.clear();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -50,7 +50,7 @@ describe('extended attrbiutes', () => {
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
describe('should generate file attributes', () => {
|
|
53
|
+
describe('should generate file attributes without additional metadata', () => {
|
|
54
54
|
const testCases: [object, string | undefined][] = [
|
|
55
55
|
[{}, undefined],
|
|
56
56
|
[
|
|
@@ -82,6 +82,28 @@ describe('extended attrbiutes', () => {
|
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
describe('should generate file attributes with additional metadata', () => {
|
|
86
|
+
const testCases: [object, string | undefined][] = [
|
|
87
|
+
[{}, '{"Media":{"Width":100,"Height":100}}'],
|
|
88
|
+
[{ size: undefined }, '{"Media":{"Width":100,"Height":100}}'],
|
|
89
|
+
[{ size: 123 }, '{"Common":{"Size":123},"Media":{"Width":100,"Height":100}}'],
|
|
90
|
+
];
|
|
91
|
+
testCases.forEach(([input, expectedAttributes]) => {
|
|
92
|
+
it(`should generate ${input}`, () => {
|
|
93
|
+
const output = generateFileExtendedAttributes(input, { Media: { Width: 100, Height: 100 } });
|
|
94
|
+
expect(output).toBe(expectedAttributes);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('should throw an error if additional metadata contains common attributes', () => {
|
|
100
|
+
it('should throw an error', () => {
|
|
101
|
+
expect(() => generateFileExtendedAttributes({ size: 123 }, { Common: { Hello: 'World' } })).toThrow(
|
|
102
|
+
'Common attributes are not allowed in additional metadata',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
85
107
|
describe('should parses file attributes', () => {
|
|
86
108
|
const testCases: [Date, string, FileExtendedAttributesParsed][] = [
|
|
87
109
|
[new Date('2025-01-01'), '', {}],
|
|
@@ -83,34 +83,42 @@ export function parseFolderExtendedAttributes(logger: Logger, extendedAttributes
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function generateFileExtendedAttributes(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
86
|
+
export function generateFileExtendedAttributes(
|
|
87
|
+
common: {
|
|
88
|
+
modificationTime?: Date;
|
|
89
|
+
size?: number;
|
|
90
|
+
blockSizes?: number[];
|
|
91
|
+
digests?: {
|
|
92
|
+
sha1?: string;
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
additionalMetadata?: object,
|
|
96
|
+
): string | undefined {
|
|
97
|
+
if (additionalMetadata && 'Common' in additionalMetadata) {
|
|
98
|
+
throw new Error('Common attributes are not allowed in additional metadata');
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
const commonAttributes: FileExtendedAttributesSchema['Common'] = {};
|
|
95
|
-
if (
|
|
96
|
-
commonAttributes.ModificationTime = dateToIsoString(
|
|
102
|
+
if (common.modificationTime) {
|
|
103
|
+
commonAttributes.ModificationTime = dateToIsoString(common.modificationTime);
|
|
97
104
|
}
|
|
98
|
-
if (
|
|
99
|
-
commonAttributes.Size =
|
|
105
|
+
if (common.size !== undefined) {
|
|
106
|
+
commonAttributes.Size = common.size;
|
|
100
107
|
}
|
|
101
|
-
if (
|
|
102
|
-
commonAttributes.BlockSizes =
|
|
108
|
+
if (common.blockSizes?.length) {
|
|
109
|
+
commonAttributes.BlockSizes = common.blockSizes;
|
|
103
110
|
}
|
|
104
|
-
if (
|
|
111
|
+
if (common.digests?.sha1) {
|
|
105
112
|
commonAttributes.Digests = {
|
|
106
|
-
SHA1:
|
|
113
|
+
SHA1: common.digests.sha1,
|
|
107
114
|
};
|
|
108
115
|
}
|
|
109
|
-
if (!Object.keys(commonAttributes).length) {
|
|
116
|
+
if (!Object.keys(commonAttributes).length && !additionalMetadata) {
|
|
110
117
|
return undefined;
|
|
111
118
|
}
|
|
112
119
|
return JSON.stringify({
|
|
113
|
-
Common: commonAttributes,
|
|
120
|
+
...(Object.keys(commonAttributes).length ? { Common: commonAttributes } : {}),
|
|
121
|
+
...(additionalMetadata ? { ...additionalMetadata } : {}),
|
|
114
122
|
});
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -352,7 +352,7 @@ describe('nodesAccess', () => {
|
|
|
352
352
|
expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
it
|
|
355
|
+
it('should return only filtered nodes from API', async () => {
|
|
356
356
|
cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(false);
|
|
357
357
|
cache.getNode = jest.fn().mockImplementation((uid: string) => {
|
|
358
358
|
if (uid === parentNode.uid) {
|
|
@@ -444,7 +444,7 @@ describe('nodesAccess', () => {
|
|
|
444
444
|
const node1 = { uid: 'volumeId~node1', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
445
445
|
const node2 = { uid: 'volumeId~node2', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
446
446
|
const node3 = { uid: 'volumeId~node3', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
447
|
-
const node4 = { uid: '
|
|
447
|
+
const node4 = { uid: 'volumeId~node4', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
|
|
448
448
|
|
|
449
449
|
it('should serve fully from cache', async () => {
|
|
450
450
|
cache.iterateNodes = jest.fn().mockImplementation(async function* () {
|
|
@@ -11,6 +11,7 @@ import { NodeAPIService } from './apiService';
|
|
|
11
11
|
import { NodesCache } from './cache';
|
|
12
12
|
import { NodesCryptoCache } from './cryptoCache';
|
|
13
13
|
import { NodesCryptoService } from './cryptoService';
|
|
14
|
+
import { NodesDebouncer } from './debouncer';
|
|
14
15
|
import { parseFileExtendedAttributes, parseFolderExtendedAttributes } from './extendedAttributes';
|
|
15
16
|
import {
|
|
16
17
|
SharesService,
|
|
@@ -40,13 +41,18 @@ const DECRYPTION_CONCURRENCY = 30;
|
|
|
40
41
|
* nodes metadata.
|
|
41
42
|
*/
|
|
42
43
|
export class NodesAccess {
|
|
44
|
+
private debouncer: NodesDebouncer;
|
|
45
|
+
|
|
43
46
|
constructor(
|
|
44
47
|
private logger: Logger,
|
|
45
48
|
private apiService: NodeAPIService,
|
|
46
49
|
private cache: NodesCache,
|
|
47
50
|
private cryptoCache: NodesCryptoCache,
|
|
48
51
|
private cryptoService: NodesCryptoService,
|
|
49
|
-
private shareService:
|
|
52
|
+
private shareService: Pick<
|
|
53
|
+
SharesService,
|
|
54
|
+
'getOwnVolumeIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
|
|
55
|
+
>,
|
|
50
56
|
) {
|
|
51
57
|
this.logger = logger;
|
|
52
58
|
this.apiService = apiService;
|
|
@@ -54,6 +60,7 @@ export class NodesAccess {
|
|
|
54
60
|
this.cryptoCache = cryptoCache;
|
|
55
61
|
this.cryptoService = cryptoService;
|
|
56
62
|
this.shareService = shareService;
|
|
63
|
+
this.debouncer = new NodesDebouncer(this.logger);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
async getVolumeRootFolder() {
|
|
@@ -65,6 +72,7 @@ export class NodesAccess {
|
|
|
65
72
|
async getNode(nodeUid: string): Promise<DecryptedNode> {
|
|
66
73
|
let cachedNode;
|
|
67
74
|
try {
|
|
75
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
68
76
|
cachedNode = await this.cache.getNode(nodeUid);
|
|
69
77
|
} catch {}
|
|
70
78
|
|
|
@@ -112,6 +120,7 @@ export class NodesAccess {
|
|
|
112
120
|
for await (const nodeUid of this.apiService.iterateChildrenNodeUids(parentNode.uid, onlyFolders, signal)) {
|
|
113
121
|
let node;
|
|
114
122
|
try {
|
|
123
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
115
124
|
node = await this.cache.getNode(nodeUid);
|
|
116
125
|
} catch {}
|
|
117
126
|
|
|
@@ -143,6 +152,7 @@ export class NodesAccess {
|
|
|
143
152
|
for await (const nodeUid of this.apiService.iterateTrashedNodeUids(volumeId, signal)) {
|
|
144
153
|
let node;
|
|
145
154
|
try {
|
|
155
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
146
156
|
node = await this.cache.getNode(nodeUid);
|
|
147
157
|
} catch {}
|
|
148
158
|
|
|
@@ -208,9 +218,14 @@ export class NodesAccess {
|
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
this.debouncer.loadingNode(nodeUid);
|
|
222
|
+
try {
|
|
223
|
+
const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
|
|
224
|
+
const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
|
|
225
|
+
return this.decryptNode(encryptedNode);
|
|
226
|
+
} finally {
|
|
227
|
+
this.debouncer.finishedLoadingNode(nodeUid);
|
|
228
|
+
}
|
|
214
229
|
}
|
|
215
230
|
|
|
216
231
|
private async *loadNodes(
|
|
@@ -236,7 +251,14 @@ export class NodesAccess {
|
|
|
236
251
|
|
|
237
252
|
const { volumeId: ownVolumeId } = await this.shareService.getOwnVolumeIDs();
|
|
238
253
|
|
|
239
|
-
const
|
|
254
|
+
const apiNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
|
|
255
|
+
|
|
256
|
+
const debouncedNodeMapper = async (encryptedNode: EncryptedNode): Promise<EncryptedNode> => {
|
|
257
|
+
this.debouncer.loadingNode(encryptedNode.uid);
|
|
258
|
+
return encryptedNode;
|
|
259
|
+
};
|
|
260
|
+
const encryptedNodesIterator = asyncIteratorMap(apiNodesIterator, debouncedNodeMapper, 1);
|
|
261
|
+
|
|
240
262
|
const decryptNodeMapper = async (encryptedNode: EncryptedNode): Promise<Result<DecryptedNode, unknown>> => {
|
|
241
263
|
returnedNodeUids.push(encryptedNode.uid);
|
|
242
264
|
try {
|
|
@@ -250,6 +272,7 @@ export class NodesAccess {
|
|
|
250
272
|
encryptedNodesIterator,
|
|
251
273
|
decryptNodeMapper,
|
|
252
274
|
DECRYPTION_CONCURRENCY,
|
|
275
|
+
signal,
|
|
253
276
|
);
|
|
254
277
|
for await (const node of decryptedNodesIterator) {
|
|
255
278
|
if (node.ok) {
|
|
@@ -329,6 +352,7 @@ export class NodesAccess {
|
|
|
329
352
|
this.logger.error(`Failed to cache node keys ${node.uid}`, error);
|
|
330
353
|
}
|
|
331
354
|
}
|
|
355
|
+
this.debouncer.finishedLoadingNode(node.uid);
|
|
332
356
|
return { node, keys };
|
|
333
357
|
}
|
|
334
358
|
|
|
@@ -360,6 +384,7 @@ export class NodesAccess {
|
|
|
360
384
|
|
|
361
385
|
async getNodeKeys(nodeUid: string): Promise<DecryptedNodeKeys> {
|
|
362
386
|
try {
|
|
387
|
+
await this.debouncer.waitForLoadingNode(nodeUid);
|
|
363
388
|
return await this.cryptoCache.getNodeKeys(nodeUid);
|
|
364
389
|
} catch {
|
|
365
390
|
const { keys } = await this.loadNode(nodeUid);
|
|
@@ -9,11 +9,21 @@ import {
|
|
|
9
9
|
import { SharesCache } from '../shares/cache';
|
|
10
10
|
import { SharesCryptoCache } from '../shares/cryptoCache';
|
|
11
11
|
import { SharesCryptoService } from '../shares/cryptoService';
|
|
12
|
+
import { NodesService as UploadNodesService } from '../upload/interface';
|
|
13
|
+
import { UploadTelemetry } from '../upload/telemetry';
|
|
14
|
+
import { UploadQueue } from '../upload/queue';
|
|
12
15
|
import { Albums } from './albums';
|
|
13
16
|
import { PhotosAPIService } from './apiService';
|
|
14
17
|
import { NodesService, SharesService } from './interface';
|
|
15
18
|
import { PhotoSharesManager } from './shares';
|
|
16
19
|
import { PhotosTimeline } from './timeline';
|
|
20
|
+
import {
|
|
21
|
+
PhotoFileUploader,
|
|
22
|
+
PhotoUploadAPIService,
|
|
23
|
+
PhotoUploadCryptoService,
|
|
24
|
+
PhotoUploadManager,
|
|
25
|
+
PhotoUploadMetadata,
|
|
26
|
+
} from './upload';
|
|
17
27
|
|
|
18
28
|
/**
|
|
19
29
|
* Provides facade for the whole photos module.
|
|
@@ -66,3 +76,55 @@ export function initPhotoSharesModule(
|
|
|
66
76
|
sharesService,
|
|
67
77
|
);
|
|
68
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Provides facade for the photo upload module.
|
|
82
|
+
*
|
|
83
|
+
* The photo upload wraps the core upload module and adds photo specific metadata.
|
|
84
|
+
* It provides the same interface so it can be used in the same way.
|
|
85
|
+
*/
|
|
86
|
+
export function initPhotoUploadModule(
|
|
87
|
+
telemetry: ProtonDriveTelemetry,
|
|
88
|
+
apiService: DriveAPIService,
|
|
89
|
+
driveCrypto: DriveCrypto,
|
|
90
|
+
sharesService: SharesService,
|
|
91
|
+
nodesService: UploadNodesService,
|
|
92
|
+
clientUid?: string,
|
|
93
|
+
) {
|
|
94
|
+
const api = new PhotoUploadAPIService(apiService, clientUid);
|
|
95
|
+
const cryptoService = new PhotoUploadCryptoService(driveCrypto, nodesService);
|
|
96
|
+
|
|
97
|
+
const uploadTelemetry = new UploadTelemetry(telemetry, sharesService);
|
|
98
|
+
const manager = new PhotoUploadManager(telemetry, api, cryptoService, nodesService, clientUid);
|
|
99
|
+
|
|
100
|
+
const queue = new UploadQueue();
|
|
101
|
+
|
|
102
|
+
async function getFileUploader(
|
|
103
|
+
parentFolderUid: string,
|
|
104
|
+
name: string,
|
|
105
|
+
metadata: PhotoUploadMetadata,
|
|
106
|
+
signal?: AbortSignal,
|
|
107
|
+
): Promise<PhotoFileUploader> {
|
|
108
|
+
await queue.waitForCapacity(signal);
|
|
109
|
+
|
|
110
|
+
const onFinish = () => {
|
|
111
|
+
queue.releaseCapacity();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return new PhotoFileUploader(
|
|
115
|
+
uploadTelemetry,
|
|
116
|
+
api,
|
|
117
|
+
cryptoService,
|
|
118
|
+
manager,
|
|
119
|
+
parentFolderUid,
|
|
120
|
+
name,
|
|
121
|
+
metadata,
|
|
122
|
+
onFinish,
|
|
123
|
+
signal,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
getFileUploader,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { DriveCrypto } from '../../crypto';
|
|
2
|
+
import { ProtonDriveTelemetry, UploadMetadata, Thumbnail } from '../../interface';
|
|
3
|
+
import { DriveAPIService, drivePaths } from '../apiService';
|
|
4
|
+
import { generateFileExtendedAttributes } from '../nodes';
|
|
5
|
+
import { splitNodeRevisionUid } from '../uids';
|
|
6
|
+
import { UploadAPIService } from '../upload/apiService';
|
|
7
|
+
import { BlockVerifier } from '../upload/blockVerifier';
|
|
8
|
+
import { UploadController } from '../upload/controller';
|
|
9
|
+
import { UploadCryptoService } from '../upload/cryptoService';
|
|
10
|
+
import { FileUploader } from '../upload/fileUploader';
|
|
11
|
+
import { NodeRevisionDraft, NodesService } from '../upload/interface';
|
|
12
|
+
import { UploadManager } from '../upload/manager';
|
|
13
|
+
import { StreamUploader } from '../upload/streamUploader';
|
|
14
|
+
import { UploadTelemetry } from '../upload/telemetry';
|
|
15
|
+
|
|
16
|
+
type PostCommitRevisionRequest = Extract<
|
|
17
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['put']['requestBody'],
|
|
18
|
+
{ content: object }
|
|
19
|
+
>['content']['application/json'];
|
|
20
|
+
type PostCommitRevisionResponse =
|
|
21
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['put']['responses']['200']['content']['application/json'];
|
|
22
|
+
|
|
23
|
+
export type PhotoUploadMetadata = UploadMetadata & {
|
|
24
|
+
captureTime?: Date;
|
|
25
|
+
mainPhotoLinkID?: string;
|
|
26
|
+
// TODO: handle tags enum in the SDK
|
|
27
|
+
tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export class PhotoFileUploader extends FileUploader {
|
|
31
|
+
private photoApiService: PhotoUploadAPIService;
|
|
32
|
+
private photoManager: PhotoUploadManager;
|
|
33
|
+
private photoMetadata: PhotoUploadMetadata;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
telemetry: UploadTelemetry,
|
|
37
|
+
apiService: PhotoUploadAPIService,
|
|
38
|
+
cryptoService: UploadCryptoService,
|
|
39
|
+
manager: PhotoUploadManager,
|
|
40
|
+
parentFolderUid: string,
|
|
41
|
+
name: string,
|
|
42
|
+
metadata: PhotoUploadMetadata,
|
|
43
|
+
onFinish: () => void,
|
|
44
|
+
signal?: AbortSignal,
|
|
45
|
+
) {
|
|
46
|
+
super(telemetry, apiService, cryptoService, manager, parentFolderUid, name, metadata, onFinish, signal);
|
|
47
|
+
this.photoApiService = apiService;
|
|
48
|
+
this.photoManager = manager;
|
|
49
|
+
this.photoMetadata = metadata;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected async newStreamUploader(
|
|
53
|
+
blockVerifier: BlockVerifier,
|
|
54
|
+
revisionDraft: NodeRevisionDraft,
|
|
55
|
+
onFinish: (failure: boolean) => Promise<void>,
|
|
56
|
+
): Promise<StreamUploader> {
|
|
57
|
+
return new PhotoStreamUploader(
|
|
58
|
+
this.telemetry,
|
|
59
|
+
this.photoApiService,
|
|
60
|
+
this.cryptoService,
|
|
61
|
+
this.photoManager,
|
|
62
|
+
blockVerifier,
|
|
63
|
+
revisionDraft,
|
|
64
|
+
this.photoMetadata,
|
|
65
|
+
onFinish,
|
|
66
|
+
this.controller,
|
|
67
|
+
this.signal,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class PhotoStreamUploader extends StreamUploader {
|
|
73
|
+
private photoUploadManager: PhotoUploadManager;
|
|
74
|
+
private photoMetadata: PhotoUploadMetadata;
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
telemetry: UploadTelemetry,
|
|
78
|
+
apiService: PhotoUploadAPIService,
|
|
79
|
+
cryptoService: UploadCryptoService,
|
|
80
|
+
uploadManager: PhotoUploadManager,
|
|
81
|
+
blockVerifier: BlockVerifier,
|
|
82
|
+
revisionDraft: NodeRevisionDraft,
|
|
83
|
+
metadata: PhotoUploadMetadata,
|
|
84
|
+
onFinish: (failure: boolean) => Promise<void>,
|
|
85
|
+
controller: UploadController,
|
|
86
|
+
signal?: AbortSignal,
|
|
87
|
+
) {
|
|
88
|
+
super(telemetry, apiService, cryptoService, uploadManager, blockVerifier, revisionDraft, metadata, onFinish, controller, signal);
|
|
89
|
+
this.photoUploadManager = uploadManager;
|
|
90
|
+
this.photoMetadata = metadata;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async commitFile(thumbnails: Thumbnail[]) {
|
|
94
|
+
this.verifyIntegrity(thumbnails);
|
|
95
|
+
|
|
96
|
+
const extendedAttributes = {
|
|
97
|
+
modificationTime: this.metadata.modificationTime,
|
|
98
|
+
size: this.metadata.expectedSize,
|
|
99
|
+
blockSizes: this.uploadedBlockSizes,
|
|
100
|
+
digests: this.digests.digests(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await this.photoUploadManager.commitDraftPhoto(this.revisionDraft, this.manifest, extendedAttributes, this.photoMetadata);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class PhotoUploadManager extends UploadManager {
|
|
108
|
+
private photoApiService: PhotoUploadAPIService;
|
|
109
|
+
private photoCryptoService: PhotoUploadCryptoService;
|
|
110
|
+
|
|
111
|
+
constructor(
|
|
112
|
+
telemetry: ProtonDriveTelemetry,
|
|
113
|
+
apiService: PhotoUploadAPIService,
|
|
114
|
+
cryptoService: PhotoUploadCryptoService,
|
|
115
|
+
nodesService: NodesService,
|
|
116
|
+
clientUid: string | undefined,
|
|
117
|
+
) {
|
|
118
|
+
super(telemetry, apiService, cryptoService, nodesService, clientUid);
|
|
119
|
+
this.photoApiService = apiService;
|
|
120
|
+
this.photoCryptoService = cryptoService;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async commitDraftPhoto(
|
|
124
|
+
nodeRevisionDraft: NodeRevisionDraft,
|
|
125
|
+
manifest: Uint8Array,
|
|
126
|
+
extendedAttributes: {
|
|
127
|
+
modificationTime?: Date;
|
|
128
|
+
size: number;
|
|
129
|
+
blockSizes: number[];
|
|
130
|
+
digests: {
|
|
131
|
+
sha1: string;
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
uploadMetadata: PhotoUploadMetadata,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
if (!nodeRevisionDraft.parentNodeKeys) {
|
|
137
|
+
throw new Error('Parent node keys are required for photo upload');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// TODO: handle photo extended attributes in the SDK - now it must be passed from the client
|
|
141
|
+
const generatedExtendedAttributes = generateFileExtendedAttributes(extendedAttributes, uploadMetadata.additionalMetadata);
|
|
142
|
+
const nodeCommitCrypto = await this.cryptoService.commitFile(
|
|
143
|
+
nodeRevisionDraft.nodeKeys,
|
|
144
|
+
manifest,
|
|
145
|
+
generatedExtendedAttributes,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const sha1 = extendedAttributes.digests.sha1;
|
|
149
|
+
const contentHash = await this.photoCryptoService.generateContentHash(sha1, nodeRevisionDraft.parentNodeKeys?.hashKey);
|
|
150
|
+
const photo = {
|
|
151
|
+
contentHash,
|
|
152
|
+
captureTime: uploadMetadata.captureTime || extendedAttributes.modificationTime,
|
|
153
|
+
mainPhotoLinkID: uploadMetadata.mainPhotoLinkID,
|
|
154
|
+
tags: uploadMetadata.tags,
|
|
155
|
+
}
|
|
156
|
+
await this.photoApiService.commitDraftPhoto(nodeRevisionDraft.nodeRevisionUid, nodeCommitCrypto, photo);
|
|
157
|
+
await this.notifyNodeUploaded(nodeRevisionDraft);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export class PhotoUploadCryptoService extends UploadCryptoService {
|
|
162
|
+
constructor(
|
|
163
|
+
driveCrypto: DriveCrypto,
|
|
164
|
+
nodesService: NodesService,
|
|
165
|
+
) {
|
|
166
|
+
super(driveCrypto, nodesService);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async generateContentHash(sha1: string, parentHashKey: Uint8Array): Promise<string> {
|
|
170
|
+
return this.driveCrypto.generateLookupHash(sha1, parentHashKey);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class PhotoUploadAPIService extends UploadAPIService {
|
|
175
|
+
constructor(apiService: DriveAPIService, clientUid: string | undefined) {
|
|
176
|
+
super(apiService, clientUid);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async commitDraftPhoto(
|
|
180
|
+
draftNodeRevisionUid: string,
|
|
181
|
+
options: {
|
|
182
|
+
armoredManifestSignature: string;
|
|
183
|
+
signatureEmail: string;
|
|
184
|
+
armoredExtendedAttributes?: string;
|
|
185
|
+
},
|
|
186
|
+
photo: {
|
|
187
|
+
contentHash: string;
|
|
188
|
+
captureTime?: Date;
|
|
189
|
+
mainPhotoLinkID?: string;
|
|
190
|
+
// TODO: handle tags enum in the SDK
|
|
191
|
+
tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
|
|
192
|
+
},
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
|
|
195
|
+
await this.apiService.put<
|
|
196
|
+
// TODO: Deprected fields but not properly marked in the types.
|
|
197
|
+
Omit<PostCommitRevisionRequest, 'BlockNumber' | 'BlockList' | 'ThumbnailToken' | 'State'>,
|
|
198
|
+
PostCommitRevisionResponse
|
|
199
|
+
>(`drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/${revisionId}`, {
|
|
200
|
+
ManifestSignature: options.armoredManifestSignature,
|
|
201
|
+
SignatureAddress: options.signatureEmail,
|
|
202
|
+
XAttr: options.armoredExtendedAttributes || null,
|
|
203
|
+
Photo: {
|
|
204
|
+
ContentHash: photo.contentHash,
|
|
205
|
+
CaptureTime: photo.captureTime?.getTime() || 0,
|
|
206
|
+
MainPhotoLinkID: photo.mainPhotoLinkID || null,
|
|
207
|
+
Tags: photo.tags || [],
|
|
208
|
+
Exif: null, // Deprecated field, not used.
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|