@protontech/drive-sdk 0.3.1 → 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/crypto/driveCrypto.d.ts +1 -1
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/interface.d.ts +1 -1
- package/dist/crypto/openPGPCrypto.d.ts +1 -1
- package/dist/crypto/openPGPCrypto.js +4 -1
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/internal/apiService/errorCodes.d.ts +1 -0
- package/dist/internal/apiService/errors.d.ts +3 -0
- package/dist/internal/apiService/errors.js +7 -1
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/devices/interface.d.ts +1 -1
- package/dist/internal/devices/manager.js +1 -1
- package/dist/internal/devices/manager.js.map +1 -1
- package/dist/internal/devices/manager.test.js +3 -3
- package/dist/internal/devices/manager.test.js.map +1 -1
- package/dist/internal/download/cryptoService.js +2 -2
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.js +2 -2
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +3 -1
- package/dist/internal/download/fileDownloader.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/cache.js +3 -1
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cryptoReporter.d.ts +20 -0
- package/dist/internal/nodes/cryptoReporter.js +96 -0
- package/dist/internal/nodes/cryptoReporter.js.map +1 -0
- package/dist/internal/nodes/cryptoService.d.ts +18 -13
- package/dist/internal/nodes/cryptoService.js +18 -98
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +7 -5
- 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.js +3 -1
- package/dist/internal/nodes/index.js.map +1 -1
- 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 +5 -2
- package/dist/internal/nodes/nodesAccess.d.ts +4 -4
- package/dist/internal/nodes/nodesAccess.js +77 -69
- 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/cache.d.ts +3 -0
- package/dist/internal/sharing/cache.js +17 -2
- package/dist/internal/sharing/cache.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +2 -2
- package/dist/internal/sharing/interface.js +1 -1
- package/dist/internal/sharing/sharingAccess.js +7 -1
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +243 -34
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/sharingPublic/apiService.d.ts +1 -1
- package/dist/internal/sharingPublic/apiService.js +9 -2
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoService.d.ts +6 -20
- package/dist/internal/sharingPublic/cryptoService.js +40 -103
- package/dist/internal/sharingPublic/cryptoService.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +2 -2
- package/dist/internal/sharingPublic/index.js +2 -2
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/interface.d.ts +1 -43
- package/dist/internal/sharingPublic/manager.d.ts +1 -1
- package/dist/internal/sharingPublic/manager.js +9 -7
- package/dist/internal/sharingPublic/manager.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +1 -1
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +3 -1
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +20 -3
- package/dist/protonDriveClient.js +24 -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/dist/protonDrivePublicLinkClient.d.ts +13 -4
- package/dist/protonDrivePublicLinkClient.js +13 -11
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +1 -1
- package/src/crypto/interface.ts +1 -1
- package/src/crypto/openPGPCrypto.ts +5 -2
- 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/download/cryptoService.ts +2 -2
- package/src/internal/download/fileDownloader.test.ts +3 -1
- package/src/internal/download/fileDownloader.ts +2 -2
- 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/cache.ts +3 -1
- package/src/internal/nodes/cryptoReporter.ts +145 -0
- package/src/internal/nodes/cryptoService.test.ts +11 -9
- package/src/internal/nodes/cryptoService.ts +45 -138
- package/src/internal/nodes/errors.ts +5 -0
- package/src/internal/nodes/index.test.ts +1 -1
- package/src/internal/nodes/index.ts +3 -1
- package/src/internal/nodes/interface.ts +6 -2
- package/src/internal/nodes/nodesAccess.test.ts +68 -8
- package/src/internal/nodes/nodesAccess.ts +101 -76
- 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/cache.ts +19 -2
- package/src/internal/sharing/interface.ts +2 -2
- package/src/internal/sharing/sharingAccess.test.ts +283 -35
- package/src/internal/sharing/sharingAccess.ts +7 -1
- package/src/internal/sharingPublic/apiService.ts +11 -2
- package/src/internal/sharingPublic/cryptoService.ts +71 -135
- package/src/internal/sharingPublic/index.ts +3 -2
- package/src/internal/sharingPublic/interface.ts +8 -53
- package/src/internal/sharingPublic/manager.ts +9 -8
- package/src/internal/upload/streamUploader.test.ts +3 -1
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/protonDriveClient.ts +34 -4
- package/src/protonDrivePhotosClient.ts +211 -32
- package/src/protonDrivePublicLinkClient.ts +26 -12
- 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
|
@@ -57,6 +57,8 @@ export function apiErrorFactory({
|
|
|
57
57
|
// Here we convert only general enough codes. Specific cases that are
|
|
58
58
|
// not clear from the code itself must be handled by each module
|
|
59
59
|
// separately.
|
|
60
|
+
case ErrorCode.INVALID_REQUIREMENTS:
|
|
61
|
+
return new InvalidRequirementsAPIError(message, code, details);
|
|
60
62
|
case ErrorCode.INVALID_VALUE:
|
|
61
63
|
case ErrorCode.NOT_ENOUGH_PERMISSIONS:
|
|
62
64
|
case ErrorCode.NOT_ENOUGH_PERMISSIONS_TO_GRANT_PERMISSIONS:
|
|
@@ -108,3 +110,7 @@ export class APICodeError extends ServerError {
|
|
|
108
110
|
export class NotFoundAPIError extends ValidationError {
|
|
109
111
|
name = 'NotFoundAPIError';
|
|
110
112
|
}
|
|
113
|
+
|
|
114
|
+
export class InvalidRequirementsAPIError extends ValidationError {
|
|
115
|
+
name = 'InvalidRequirementsAPIError';
|
|
116
|
+
}
|
|
@@ -13,7 +13,7 @@ export type DeviceMetadata = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export interface SharesService {
|
|
16
|
-
|
|
16
|
+
getOwnVolumeIDs(): Promise<{ volumeId: string }>;
|
|
17
17
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
18
18
|
addressId: string;
|
|
19
19
|
email: string;
|
|
@@ -30,7 +30,7 @@ describe('DevicesManager', () => {
|
|
|
30
30
|
};
|
|
31
31
|
// @ts-expect-error No need to implement all methods for mocking
|
|
32
32
|
sharesService = {
|
|
33
|
-
|
|
33
|
+
getOwnVolumeIDs: jest.fn(),
|
|
34
34
|
};
|
|
35
35
|
// @ts-expect-error No need to implement all methods for mocking
|
|
36
36
|
nodesService = {};
|
|
@@ -74,13 +74,13 @@ describe('DevicesManager', () => {
|
|
|
74
74
|
shareId: 'shareid',
|
|
75
75
|
} as DeviceMetadata;
|
|
76
76
|
|
|
77
|
-
sharesService.
|
|
77
|
+
sharesService.getOwnVolumeIDs.mockResolvedValue({ volumeId });
|
|
78
78
|
cryptoService.createDevice.mockResolvedValue({ address, shareKey, node });
|
|
79
79
|
apiService.createDevice.mockResolvedValue(createdDevice);
|
|
80
80
|
|
|
81
81
|
const result = await manager.createDevice(name, deviceType);
|
|
82
82
|
|
|
83
|
-
expect(sharesService.
|
|
83
|
+
expect(sharesService.getOwnVolumeIDs).toHaveBeenCalled();
|
|
84
84
|
expect(cryptoService.createDevice).toHaveBeenCalledWith(name);
|
|
85
85
|
expect(apiService.createDevice).toHaveBeenCalledWith(
|
|
86
86
|
{ volumeId, type: deviceType },
|
|
@@ -47,7 +47,7 @@ export class DevicesManager {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async createDevice(name: string, deviceType: DeviceType): Promise<Device> {
|
|
50
|
-
const { volumeId } = await this.sharesService.
|
|
50
|
+
const { volumeId } = await this.sharesService.getOwnVolumeIDs();
|
|
51
51
|
const { address, shareKey, node } = await this.cryptoService.createDevice(name);
|
|
52
52
|
|
|
53
53
|
const device = await this.apiService.createDevice(
|
|
@@ -49,7 +49,7 @@ export class DownloadCryptoService {
|
|
|
49
49
|
);
|
|
50
50
|
} catch (error: unknown) {
|
|
51
51
|
const message = getErrorMessage(error);
|
|
52
|
-
throw new DecryptionError(c('Error').t`Failed to decrypt block: ${message}
|
|
52
|
+
throw new DecryptionError(c('Error').t`Failed to decrypt block: ${message}`, { cause: error });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return decryptedBlock;
|
|
@@ -66,7 +66,7 @@ export class DownloadCryptoService {
|
|
|
66
66
|
decryptedBlock = result.decryptedThumbnail;
|
|
67
67
|
} catch (error: unknown) {
|
|
68
68
|
const message = getErrorMessage(error);
|
|
69
|
-
throw new DecryptionError(c('Error').t`Failed to decrypt thumbnail: ${message}
|
|
69
|
+
throw new DecryptionError(c('Error').t`Failed to decrypt thumbnail: ${message}`, { cause: error });
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
return decryptedBlock;
|
|
@@ -123,8 +123,10 @@ describe('FileDownloader', () => {
|
|
|
123
123
|
|
|
124
124
|
const verifyOnProgress = async (downloadedBytes: number[]) => {
|
|
125
125
|
expect(onProgress).toHaveBeenCalledTimes(downloadedBytes.length);
|
|
126
|
+
let fileProgress = 0;
|
|
126
127
|
for (let i = 0; i < downloadedBytes.length; i++) {
|
|
127
|
-
|
|
128
|
+
fileProgress += downloadedBytes[i];
|
|
129
|
+
expect(onProgress).toHaveBeenNthCalledWith(i + 1, fileProgress);
|
|
128
130
|
}
|
|
129
131
|
};
|
|
130
132
|
|
|
@@ -134,7 +134,7 @@ export class FileDownloader {
|
|
|
134
134
|
const blockData = await this.downloadBlockData(blockMetadata, true, cryptoKeys);
|
|
135
135
|
return blockData.slice(blockOffset);
|
|
136
136
|
} catch (error: unknown) {
|
|
137
|
-
return error instanceof Error ? error : new Error(`Unknown error: ${error}
|
|
137
|
+
return error instanceof Error ? error : new Error(`Unknown error: ${error}`, { cause: error });
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -193,7 +193,7 @@ export class FileDownloader {
|
|
|
193
193
|
cryptoKeys,
|
|
194
194
|
(downloadedBytes) => {
|
|
195
195
|
fileProgress += downloadedBytes;
|
|
196
|
-
onProgress?.(
|
|
196
|
+
onProgress?.(fileProgress);
|
|
197
197
|
},
|
|
198
198
|
);
|
|
199
199
|
this.ongoingDownloads.set(blockMetadata.index, { downloadPromise });
|
|
@@ -46,29 +46,6 @@ describe('CoreEventManager', () => {
|
|
|
46
46
|
const eventId = 'event1';
|
|
47
47
|
const latestEventId = 'event2';
|
|
48
48
|
|
|
49
|
-
it('should yield ShareWithMeUpdated event when refresh is true', async () => {
|
|
50
|
-
const mockEvents: DriveEventsListWithStatus = {
|
|
51
|
-
latestEventId,
|
|
52
|
-
more: false,
|
|
53
|
-
refresh: true,
|
|
54
|
-
events: [],
|
|
55
|
-
};
|
|
56
|
-
mockApiService.getCoreEvents.mockResolvedValue(mockEvents);
|
|
57
|
-
|
|
58
|
-
const events = [];
|
|
59
|
-
for await (const event of coreEventManager.getEvents(eventId)) {
|
|
60
|
-
events.push(event);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
expect(events).toHaveLength(1);
|
|
64
|
-
expect(events[0]).toEqual({
|
|
65
|
-
type: DriveEventType.SharedWithMeUpdated,
|
|
66
|
-
treeEventScopeId: 'core',
|
|
67
|
-
eventId: latestEventId,
|
|
68
|
-
});
|
|
69
|
-
expect(mockApiService.getCoreEvents).toHaveBeenCalledWith(eventId);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
49
|
it('should yield all events when there are actual events', async () => {
|
|
73
50
|
const mockEvent1: DriveEvent = {
|
|
74
51
|
type: DriveEventType.SharedWithMeUpdated,
|
|
@@ -88,14 +65,31 @@ describe('CoreEventManager', () => {
|
|
|
88
65
|
};
|
|
89
66
|
mockApiService.getCoreEvents.mockResolvedValue(mockEvents);
|
|
90
67
|
|
|
91
|
-
const events =
|
|
92
|
-
for await (const event of coreEventManager.getEvents(eventId)) {
|
|
93
|
-
events.push(event);
|
|
94
|
-
}
|
|
68
|
+
const events = await Array.fromAsync(coreEventManager.getEvents(eventId));
|
|
95
69
|
|
|
96
70
|
expect(events).toHaveLength(2);
|
|
97
71
|
expect(events[0]).toEqual(mockEvent1);
|
|
98
72
|
expect(events[1]).toEqual(mockEvent2);
|
|
99
73
|
});
|
|
74
|
+
|
|
75
|
+
it('should yield FastForward event there are no events but lastEventId changed', async () => {
|
|
76
|
+
const mockEvents: DriveEventsListWithStatus = {
|
|
77
|
+
latestEventId,
|
|
78
|
+
more: false,
|
|
79
|
+
refresh: false,
|
|
80
|
+
events: [],
|
|
81
|
+
};
|
|
82
|
+
mockApiService.getCoreEvents.mockResolvedValue(mockEvents);
|
|
83
|
+
|
|
84
|
+
const events = await Array.fromAsync(coreEventManager.getEvents(eventId));
|
|
85
|
+
|
|
86
|
+
expect(events).toHaveLength(1);
|
|
87
|
+
expect(events[0]).toEqual({
|
|
88
|
+
type: DriveEventType.FastForward,
|
|
89
|
+
treeEventScopeId: 'core',
|
|
90
|
+
eventId: latestEventId,
|
|
91
|
+
});
|
|
92
|
+
expect(mockApiService.getCoreEvents).toHaveBeenCalledWith(eventId);
|
|
93
|
+
});
|
|
100
94
|
});
|
|
101
95
|
});
|
|
@@ -32,7 +32,7 @@ export class CoreEventManager implements EventManagerInterface<DriveEvent> {
|
|
|
32
32
|
const events = await this.apiService.getCoreEvents(eventId);
|
|
33
33
|
if (events.events.length === 0 && events.latestEventId !== eventId) {
|
|
34
34
|
yield {
|
|
35
|
-
type: DriveEventType.
|
|
35
|
+
type: DriveEventType.FastForward,
|
|
36
36
|
treeEventScopeId: 'core',
|
|
37
37
|
eventId: events.latestEventId,
|
|
38
38
|
};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Logger, ProtonDriveTelemetry } from '../../interface';
|
|
2
2
|
import { DriveAPIService } from '../apiService';
|
|
3
|
-
import { DriveEvent, DriveListener, EventSubscription, LatestEventIdProvider } from './interface';
|
|
3
|
+
import { DriveEvent, DriveListener, EventSubscription, LatestEventIdProvider, SharesService } from './interface';
|
|
4
4
|
import { EventsAPIService } from './apiService';
|
|
5
5
|
import { CoreEventManager } from './coreEventManager';
|
|
6
6
|
import { VolumeEventManager } from './volumeEventManager';
|
|
7
7
|
import { EventManager } from './eventManager';
|
|
8
|
-
import { SharesManager } from '../shares/manager';
|
|
9
8
|
|
|
10
9
|
export type { DriveEvent, DriveListener, EventSubscription } from './interface';
|
|
11
10
|
export { DriveEventType } from './interface';
|
|
@@ -28,7 +27,7 @@ export class DriveEventsService {
|
|
|
28
27
|
constructor(
|
|
29
28
|
private telemetry: ProtonDriveTelemetry,
|
|
30
29
|
apiService: DriveAPIService,
|
|
31
|
-
private
|
|
30
|
+
private sharesService: SharesService,
|
|
32
31
|
private cacheEventListeners: DriveListener[] = [],
|
|
33
32
|
private latestEventIdProvider?: LatestEventIdProvider,
|
|
34
33
|
) {
|
|
@@ -104,7 +103,7 @@ export class DriveEventsService {
|
|
|
104
103
|
this.logger.debug(`Creating volume event manager for volume ${volumeId}`);
|
|
105
104
|
const volumeEventManager = new VolumeEventManager(this.logger, this.apiService, volumeId);
|
|
106
105
|
|
|
107
|
-
const isOwnVolume = await this.
|
|
106
|
+
const isOwnVolume = await this.sharesService.isOwnVolume(volumeId);
|
|
108
107
|
const pollingInterval = this.getDefaultVolumePollingInterval(isOwnVolume);
|
|
109
108
|
const latestEventId = this.latestEventIdProvider.getLatestEventId(volumeId);
|
|
110
109
|
const eventManager = new EventManager<DriveEvent>(volumeEventManager, pollingInterval, latestEventId);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { MemberRole, NodeType } from '../../interface';
|
|
2
2
|
import { getMockLogger } from '../../tests/logger';
|
|
3
|
-
import { DriveAPIService, ErrorCode } from '../apiService';
|
|
3
|
+
import { DriveAPIService, ErrorCode, InvalidRequirementsAPIError } from '../apiService';
|
|
4
4
|
import { NodeAPIService } from './apiService';
|
|
5
|
+
import { NodeOutOfSyncError } from './errors';
|
|
5
6
|
|
|
6
7
|
function generateAPIFileNode(linkOverrides = {}, overrides = {}) {
|
|
7
8
|
const node = generateAPINode();
|
|
@@ -542,4 +543,37 @@ describe('nodeAPIService', () => {
|
|
|
542
543
|
}
|
|
543
544
|
});
|
|
544
545
|
});
|
|
546
|
+
|
|
547
|
+
describe('renameNode', () => {
|
|
548
|
+
it('should rename node', async () => {
|
|
549
|
+
await api.renameNode(
|
|
550
|
+
'volumeId~nodeId1',
|
|
551
|
+
{ hash: 'originalHash' },
|
|
552
|
+
{ encryptedName: 'encryptedName1', nameSignatureEmail: 'nameSignatureEmail1', hash: 'newHash' },
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
expect(apiMock.put).toHaveBeenCalledWith(
|
|
556
|
+
'drive/v2/volumes/volumeId/links/nodeId1/rename',
|
|
557
|
+
{
|
|
558
|
+
Name: 'encryptedName1',
|
|
559
|
+
NameSignatureEmail: 'nameSignatureEmail1',
|
|
560
|
+
Hash: 'newHash',
|
|
561
|
+
OriginalHash: 'originalHash',
|
|
562
|
+
},
|
|
563
|
+
undefined,
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should throw error if node is out of sync', async () => {
|
|
568
|
+
apiMock.put = jest.fn().mockRejectedValue(new InvalidRequirementsAPIError('Node is out of sync'));
|
|
569
|
+
|
|
570
|
+
await expect(
|
|
571
|
+
api.renameNode(
|
|
572
|
+
'volumeId~nodeId1',
|
|
573
|
+
{ hash: 'originalHash' },
|
|
574
|
+
{ encryptedName: 'encryptedName1', nameSignatureEmail: 'nameSignatureEmail1', hash: 'newHash' },
|
|
575
|
+
),
|
|
576
|
+
).rejects.toThrow(new NodeOutOfSyncError('Node is out of sync'));
|
|
577
|
+
});
|
|
578
|
+
});
|
|
545
579
|
});
|
|
@@ -6,6 +6,7 @@ import { MemberRole, RevisionState } from '../../interface/nodes';
|
|
|
6
6
|
import {
|
|
7
7
|
DriveAPIService,
|
|
8
8
|
drivePaths,
|
|
9
|
+
InvalidRequirementsAPIError,
|
|
9
10
|
isCodeOk,
|
|
10
11
|
nodeTypeNumberToNodeType,
|
|
11
12
|
permissionsToMemberRole,
|
|
@@ -13,7 +14,8 @@ import {
|
|
|
13
14
|
import { asyncIteratorRace } from '../asyncIteratorRace';
|
|
14
15
|
import { batch } from '../batch';
|
|
15
16
|
import { splitNodeUid, makeNodeUid, makeNodeRevisionUid, splitNodeRevisionUid, makeNodeThumbnailUid } from '../uids';
|
|
16
|
-
import {
|
|
17
|
+
import { NodeOutOfSyncError } from './errors';
|
|
18
|
+
import { EncryptedNode, EncryptedRevision, FilterOptions, Thumbnail } from './interface';
|
|
17
19
|
|
|
18
20
|
// This is the number of calls to the API that are made in parallel.
|
|
19
21
|
const API_CONCURRENCY = 15;
|
|
@@ -48,6 +50,13 @@ type PutMoveNodeRequest = Extract<
|
|
|
48
50
|
type PutMoveNodeResponse =
|
|
49
51
|
drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/move']['put']['responses']['200']['content']['application/json'];
|
|
50
52
|
|
|
53
|
+
type PostCopyNodeRequest = Extract<
|
|
54
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['requestBody'],
|
|
55
|
+
{ content: object }
|
|
56
|
+
>['content']['application/json'];
|
|
57
|
+
type PostCopyNodeResponse =
|
|
58
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['responses']['200']['content']['application/json'];
|
|
59
|
+
|
|
51
60
|
type PostTrashNodesRequest = Extract<
|
|
52
61
|
drivePaths['/drive/v2/volumes/{volumeID}/trash_multiple']['post']['requestBody'],
|
|
53
62
|
{ content: object }
|
|
@@ -108,7 +117,7 @@ export class NodeAPIService {
|
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
|
|
111
|
-
const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, signal);
|
|
120
|
+
const nodesGenerator = this.iterateNodes([nodeUid], ownVolumeId, undefined, signal);
|
|
112
121
|
const result = await nodesGenerator.next();
|
|
113
122
|
if (!result.value) {
|
|
114
123
|
throw new ValidationError(c('Error').t`Node not found`);
|
|
@@ -117,7 +126,12 @@ export class NodeAPIService {
|
|
|
117
126
|
return result.value;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
async *iterateNodes(
|
|
129
|
+
async *iterateNodes(
|
|
130
|
+
nodeUids: string[],
|
|
131
|
+
ownVolumeId: string,
|
|
132
|
+
filterOptions?: FilterOptions,
|
|
133
|
+
signal?: AbortSignal,
|
|
134
|
+
): AsyncGenerator<EncryptedNode> {
|
|
121
135
|
const allNodeIds = nodeUids.map(splitNodeUid);
|
|
122
136
|
|
|
123
137
|
const nodeIdsByVolumeId = new Map<string, string[]>();
|
|
@@ -139,7 +153,13 @@ export class NodeAPIService {
|
|
|
139
153
|
const isAdmin = volumeId === ownVolumeId;
|
|
140
154
|
|
|
141
155
|
yield (async function* () {
|
|
142
|
-
const errorsPerVolume = yield* iterateNodesPerVolume(
|
|
156
|
+
const errorsPerVolume = yield* iterateNodesPerVolume(
|
|
157
|
+
volumeId,
|
|
158
|
+
nodeIds,
|
|
159
|
+
isAdmin,
|
|
160
|
+
filterOptions,
|
|
161
|
+
signal,
|
|
162
|
+
);
|
|
143
163
|
if (errorsPerVolume.length) {
|
|
144
164
|
errors.push(...errorsPerVolume);
|
|
145
165
|
}
|
|
@@ -159,6 +179,7 @@ export class NodeAPIService {
|
|
|
159
179
|
volumeId: string,
|
|
160
180
|
nodeIds: string[],
|
|
161
181
|
isOwnVolumeId: boolean,
|
|
182
|
+
filterOptions?: FilterOptions,
|
|
162
183
|
signal?: AbortSignal,
|
|
163
184
|
): AsyncGenerator<EncryptedNode, unknown[]> {
|
|
164
185
|
const errors: unknown[] = [];
|
|
@@ -174,7 +195,11 @@ export class NodeAPIService {
|
|
|
174
195
|
|
|
175
196
|
for (const link of response.Links) {
|
|
176
197
|
try {
|
|
177
|
-
|
|
198
|
+
const encryptedNode = linkToEncryptedNode(this.logger, volumeId, link, isOwnVolumeId);
|
|
199
|
+
if (filterOptions?.type && encryptedNode.type !== filterOptions.type) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
yield encryptedNode;
|
|
178
203
|
} catch (error: unknown) {
|
|
179
204
|
this.logger.error(`Failed to transform node ${link.Link.LinkID}`, error);
|
|
180
205
|
errors.push(error);
|
|
@@ -186,13 +211,25 @@ export class NodeAPIService {
|
|
|
186
211
|
}
|
|
187
212
|
|
|
188
213
|
// Improvement requested: load next page sooner before all IDs are yielded.
|
|
189
|
-
async *iterateChildrenNodeUids(
|
|
214
|
+
async *iterateChildrenNodeUids(
|
|
215
|
+
parentNodeUid: string,
|
|
216
|
+
onlyFolders: boolean = false,
|
|
217
|
+
signal?: AbortSignal,
|
|
218
|
+
): AsyncGenerator<string> {
|
|
190
219
|
const { volumeId, nodeId } = splitNodeUid(parentNodeUid);
|
|
191
220
|
|
|
192
221
|
let anchor = '';
|
|
193
222
|
while (true) {
|
|
223
|
+
const queryParams = new URLSearchParams();
|
|
224
|
+
if (onlyFolders) {
|
|
225
|
+
queryParams.set('FoldersOnly', '1');
|
|
226
|
+
}
|
|
227
|
+
if (anchor) {
|
|
228
|
+
queryParams.set('AnchorID', anchor);
|
|
229
|
+
}
|
|
230
|
+
|
|
194
231
|
const response = await this.apiService.get<GetChildrenResponse>(
|
|
195
|
-
`drive/v2/volumes/${volumeId}/folders/${nodeId}/children?${
|
|
232
|
+
`drive/v2/volumes/${volumeId}/folders/${nodeId}/children?${queryParams.toString()}`,
|
|
196
233
|
signal,
|
|
197
234
|
);
|
|
198
235
|
for (const linkID of response.LinkIDs) {
|
|
@@ -251,16 +288,28 @@ export class NodeAPIService {
|
|
|
251
288
|
): Promise<void> {
|
|
252
289
|
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
253
290
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
try {
|
|
292
|
+
await this.apiService.put<
|
|
293
|
+
Omit<PutRenameNodeRequest, 'SignatureAddress' | 'MIMEType'>,
|
|
294
|
+
PutRenameNodeResponse
|
|
295
|
+
>(
|
|
296
|
+
`drive/v2/volumes/${volumeId}/links/${nodeId}/rename`,
|
|
297
|
+
{
|
|
298
|
+
Name: newNode.encryptedName,
|
|
299
|
+
NameSignatureEmail: newNode.nameSignatureEmail,
|
|
300
|
+
Hash: newNode.hash,
|
|
301
|
+
OriginalHash: originalNode.hash || null,
|
|
302
|
+
},
|
|
303
|
+
signal,
|
|
304
|
+
);
|
|
305
|
+
} catch (error: unknown) {
|
|
306
|
+
// API returns generic code 2000 when node is out of sync.
|
|
307
|
+
// We map this to specific error for clarity.
|
|
308
|
+
if (error instanceof InvalidRequirementsAPIError) {
|
|
309
|
+
throw new NodeOutOfSyncError(error.message, error.code, { cause: error });
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
264
313
|
}
|
|
265
314
|
|
|
266
315
|
async moveNode(
|
|
@@ -303,6 +352,43 @@ export class NodeAPIService {
|
|
|
303
352
|
);
|
|
304
353
|
}
|
|
305
354
|
|
|
355
|
+
async copyNode(
|
|
356
|
+
nodeUid: string,
|
|
357
|
+
newNode: {
|
|
358
|
+
parentUid: string;
|
|
359
|
+
armoredNodePassphrase: string;
|
|
360
|
+
armoredNodePassphraseSignature?: string;
|
|
361
|
+
signatureEmail?: string;
|
|
362
|
+
encryptedName: string;
|
|
363
|
+
nameSignatureEmail?: string;
|
|
364
|
+
hash: string;
|
|
365
|
+
},
|
|
366
|
+
signal?: AbortSignal,
|
|
367
|
+
): Promise<string> {
|
|
368
|
+
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
369
|
+
const { volumeId: parentVolumeId, nodeId: parentNodeId } = splitNodeUid(newNode.parentUid);
|
|
370
|
+
|
|
371
|
+
const response = await this.apiService.post<PostCopyNodeRequest, PostCopyNodeResponse>(
|
|
372
|
+
`drive/volumes/${volumeId}/links/${nodeId}/copy`,
|
|
373
|
+
{
|
|
374
|
+
TargetVolumeID: parentVolumeId,
|
|
375
|
+
TargetParentLinkID: parentNodeId,
|
|
376
|
+
NodePassphrase: newNode.armoredNodePassphrase,
|
|
377
|
+
// @ts-expect-error: API accepts NodePassphraseSignature as optional.
|
|
378
|
+
NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
|
|
379
|
+
// @ts-expect-error: API accepts SignatureEmail as optional.
|
|
380
|
+
SignatureEmail: newNode.signatureEmail,
|
|
381
|
+
Name: newNode.encryptedName,
|
|
382
|
+
// @ts-expect-error: API accepts NameSignatureEmail as optional.
|
|
383
|
+
NameSignatureEmail: newNode.nameSignatureEmail,
|
|
384
|
+
Hash: newNode.hash,
|
|
385
|
+
},
|
|
386
|
+
signal,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return makeNodeUid(volumeId, response.LinkID);
|
|
390
|
+
}
|
|
391
|
+
|
|
306
392
|
// Improvement requested: split into multiple calls for many nodes.
|
|
307
393
|
async *trashNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
|
|
308
394
|
const nodeIds = nodeUids.map(splitNodeUid);
|
|
@@ -53,7 +53,9 @@ export class NodesCache {
|
|
|
53
53
|
return deserialiseNode(nodeData);
|
|
54
54
|
} catch (error: unknown) {
|
|
55
55
|
await this.removeCorruptedNode({ nodeUid }, error);
|
|
56
|
-
throw new Error(`Failed to deserialise node: ${error instanceof Error ? error.message : error}
|
|
56
|
+
throw new Error(`Failed to deserialise node: ${error instanceof Error ? error.message : error}`, {
|
|
57
|
+
cause: error,
|
|
58
|
+
});
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { VERIFICATION_STATUS } from '../../crypto';
|
|
2
|
+
import {
|
|
3
|
+
resultOk,
|
|
4
|
+
resultError,
|
|
5
|
+
Author,
|
|
6
|
+
AnonymousUser,
|
|
7
|
+
ProtonDriveTelemetry,
|
|
8
|
+
Logger,
|
|
9
|
+
MetricsDecryptionErrorField,
|
|
10
|
+
MetricVerificationErrorField,
|
|
11
|
+
} from '../../interface';
|
|
12
|
+
import { getVerificationMessage } from '../errors';
|
|
13
|
+
import { splitNodeUid } from '../uids';
|
|
14
|
+
import {
|
|
15
|
+
EncryptedNode,
|
|
16
|
+
SharesService,
|
|
17
|
+
} from './interface';
|
|
18
|
+
|
|
19
|
+
export class NodesCryptoReporter {
|
|
20
|
+
private logger: Logger;
|
|
21
|
+
|
|
22
|
+
private reportedDecryptionErrors = new Set<string>();
|
|
23
|
+
private reportedVerificationErrors = new Set<string>();
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private telemetry: ProtonDriveTelemetry,
|
|
27
|
+
private shareService: SharesService,
|
|
28
|
+
) {
|
|
29
|
+
this.telemetry = telemetry;
|
|
30
|
+
this.logger = telemetry.getLogger('nodes-crypto');
|
|
31
|
+
this.shareService = shareService;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async handleClaimedAuthor(
|
|
35
|
+
node: { uid: string; creationTime: Date },
|
|
36
|
+
field: MetricVerificationErrorField,
|
|
37
|
+
signatureType: string,
|
|
38
|
+
verified: VERIFICATION_STATUS,
|
|
39
|
+
verificationErrors?: Error[],
|
|
40
|
+
claimedAuthor?: string,
|
|
41
|
+
notAvailableVerificationKeys = false,
|
|
42
|
+
): Promise<Author> {
|
|
43
|
+
const author = handleClaimedAuthor(
|
|
44
|
+
signatureType,
|
|
45
|
+
verified,
|
|
46
|
+
verificationErrors,
|
|
47
|
+
claimedAuthor,
|
|
48
|
+
notAvailableVerificationKeys,
|
|
49
|
+
);
|
|
50
|
+
if (!author.ok) {
|
|
51
|
+
void this.reportVerificationError(node, field, verificationErrors, claimedAuthor);
|
|
52
|
+
}
|
|
53
|
+
return author;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async reportVerificationError(
|
|
57
|
+
node: { uid: string; creationTime: Date },
|
|
58
|
+
field: MetricVerificationErrorField,
|
|
59
|
+
verificationErrors?: Error[],
|
|
60
|
+
claimedAuthor?: string,
|
|
61
|
+
) {
|
|
62
|
+
if (this.reportedVerificationErrors.has(node.uid)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.reportedVerificationErrors.add(node.uid);
|
|
66
|
+
|
|
67
|
+
const fromBefore2024 = node.creationTime < new Date('2024-01-01');
|
|
68
|
+
|
|
69
|
+
let addressMatchingDefaultShare, volumeType;
|
|
70
|
+
try {
|
|
71
|
+
const { volumeId } = splitNodeUid(node.uid);
|
|
72
|
+
const { email } = await this.shareService.getMyFilesShareMemberEmailKey();
|
|
73
|
+
addressMatchingDefaultShare = claimedAuthor ? claimedAuthor === email : undefined;
|
|
74
|
+
volumeType = await this.shareService.getVolumeMetricContext(volumeId);
|
|
75
|
+
} catch (error: unknown) {
|
|
76
|
+
this.logger.error('Failed to check if claimed author matches default share', error);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.logger.warn(
|
|
80
|
+
`Failed to verify ${field} for node ${node.uid} (from before 2024: ${fromBefore2024}, matching address: ${addressMatchingDefaultShare})`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
this.telemetry.recordMetric({
|
|
84
|
+
eventName: 'verificationError',
|
|
85
|
+
volumeType,
|
|
86
|
+
field,
|
|
87
|
+
addressMatchingDefaultShare,
|
|
88
|
+
fromBefore2024,
|
|
89
|
+
error: verificationErrors?.map((e) => e.message).join(', '),
|
|
90
|
+
uid: node.uid,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
|
|
95
|
+
if (this.reportedDecryptionErrors.has(node.uid)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const fromBefore2024 = node.creationTime < new Date('2024-01-01');
|
|
100
|
+
|
|
101
|
+
let volumeType;
|
|
102
|
+
try {
|
|
103
|
+
const { volumeId } = splitNodeUid(node.uid);
|
|
104
|
+
volumeType = await this.shareService.getVolumeMetricContext(volumeId);
|
|
105
|
+
} catch (error: unknown) {
|
|
106
|
+
this.logger.error('Failed to get metric context', error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.logger.error(`Failed to decrypt node ${node.uid} (from before 2024: ${fromBefore2024})`, error);
|
|
110
|
+
|
|
111
|
+
this.telemetry.recordMetric({
|
|
112
|
+
eventName: 'decryptionError',
|
|
113
|
+
volumeType,
|
|
114
|
+
field,
|
|
115
|
+
fromBefore2024,
|
|
116
|
+
error,
|
|
117
|
+
uid: node.uid,
|
|
118
|
+
});
|
|
119
|
+
this.reportedDecryptionErrors.add(node.uid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param signatureType - Must be translated before calling this function.
|
|
125
|
+
*/
|
|
126
|
+
function handleClaimedAuthor(
|
|
127
|
+
signatureType: string,
|
|
128
|
+
verified: VERIFICATION_STATUS,
|
|
129
|
+
verificationErrors?: Error[],
|
|
130
|
+
claimedAuthor?: string,
|
|
131
|
+
notAvailableVerificationKeys = false,
|
|
132
|
+
): Author {
|
|
133
|
+
if (!claimedAuthor && notAvailableVerificationKeys) {
|
|
134
|
+
return resultOk(null as AnonymousUser);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (verified === VERIFICATION_STATUS.SIGNED_AND_VALID) {
|
|
138
|
+
return resultOk(claimedAuthor || (null as AnonymousUser));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return resultError({
|
|
142
|
+
claimedAuthor,
|
|
143
|
+
error: getVerificationMessage(verified, verificationErrors, signatureType, notAvailableVerificationKeys),
|
|
144
|
+
});
|
|
145
|
+
}
|