@protontech/drive-sdk 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/diagnostic/diagnostic.d.ts +7 -4
- package/dist/diagnostic/diagnostic.js +16 -8
- package/dist/diagnostic/diagnostic.js.map +1 -1
- package/dist/diagnostic/index.d.ts +1 -1
- package/dist/diagnostic/index.js +9 -1
- package/dist/diagnostic/index.js.map +1 -1
- package/dist/diagnostic/interface.d.ts +24 -9
- package/dist/diagnostic/nodeUtils.d.ts +13 -0
- package/dist/diagnostic/nodeUtils.js +90 -0
- package/dist/diagnostic/nodeUtils.js.map +1 -0
- package/dist/diagnostic/sdkDiagnosticBase.d.ts +36 -0
- package/dist/diagnostic/sdkDiagnosticBase.js +305 -0
- package/dist/diagnostic/sdkDiagnosticBase.js.map +1 -0
- package/dist/diagnostic/sdkDiagnosticMain.d.ts +16 -0
- package/dist/diagnostic/sdkDiagnosticMain.js +79 -0
- package/dist/diagnostic/sdkDiagnosticMain.js.map +1 -0
- package/dist/diagnostic/sdkDiagnosticPhotos.d.ts +13 -0
- package/dist/diagnostic/sdkDiagnosticPhotos.js +65 -0
- package/dist/diagnostic/sdkDiagnosticPhotos.js.map +1 -0
- package/dist/interface/index.d.ts +1 -1
- package/dist/interface/upload.d.ts +1 -12
- 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/errors.d.ts +5 -0
- package/dist/internal/errors.js +23 -0
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/errors.test.js +53 -2
- package/dist/internal/errors.test.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +11 -1
- package/dist/internal/nodes/apiService.js +20 -1
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +1 -1
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoReporter.js +3 -0
- package/dist/internal/nodes/cryptoReporter.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +4 -0
- package/dist/internal/nodes/cryptoService.js +6 -0
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/index.d.ts +1 -1
- package/dist/internal/nodes/index.js +2 -2
- package/dist/internal/nodes/index.js.map +1 -1
- package/dist/internal/nodes/index.test.js +2 -2
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +1 -1
- package/dist/internal/nodes/nodeName.d.ts +8 -0
- package/dist/internal/nodes/nodeName.js +30 -0
- package/dist/internal/nodes/nodeName.js.map +1 -0
- package/dist/internal/nodes/nodeName.test.d.ts +1 -0
- package/dist/internal/nodes/nodeName.test.js +50 -0
- package/dist/internal/nodes/nodeName.test.js.map +1 -0
- package/dist/internal/nodes/nodesAccess.d.ts +1 -1
- package/dist/internal/nodes/nodesAccess.js +4 -4
- 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.d.ts +1 -0
- package/dist/internal/nodes/nodesManagement.js +30 -1
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +61 -0
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/photos/albums.js +1 -1
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/apiService.d.ts +6 -0
- package/dist/internal/photos/apiService.js +16 -0
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/index.d.ts +3 -1
- package/dist/internal/photos/index.js +6 -2
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +4 -1
- package/dist/internal/photos/shares.d.ts +1 -1
- package/dist/internal/photos/shares.js +3 -3
- package/dist/internal/photos/shares.js.map +1 -1
- package/dist/internal/photos/timeline.d.ts +8 -1
- package/dist/internal/photos/timeline.js +36 -2
- package/dist/internal/photos/timeline.js.map +1 -1
- package/dist/internal/photos/timeline.test.d.ts +1 -0
- package/dist/internal/photos/timeline.test.js +99 -0
- package/dist/internal/photos/timeline.test.js.map +1 -0
- package/dist/internal/shares/cryptoService.js +3 -0
- package/dist/internal/shares/cryptoService.js.map +1 -1
- package/dist/internal/shares/index.d.ts +1 -0
- package/dist/internal/shares/index.js +3 -0
- package/dist/internal/shares/index.js.map +1 -1
- package/dist/internal/shares/interface.d.ts +8 -0
- package/dist/internal/shares/interface.js +10 -1
- package/dist/internal/shares/interface.js.map +1 -1
- 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/apiService.d.ts +3 -1
- package/dist/internal/sharing/apiService.js +16 -12
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/index.d.ts +2 -1
- package/dist/internal/sharing/index.js +6 -2
- package/dist/internal/sharing/index.js.map +1 -1
- package/dist/internal/sharing/interface.d.ts +1 -1
- package/dist/internal/sharing/sharingAccess.js +1 -1
- package/dist/internal/sharing/sharingAccess.js.map +1 -1
- package/dist/internal/sharing/sharingAccess.test.js +1 -1
- package/dist/internal/sharing/sharingAccess.test.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.js +32 -14
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.test.js +46 -1
- package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoReporter.js +3 -0
- package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -1
- package/dist/internal/sharingPublic/index.d.ts +3 -0
- package/dist/internal/sharingPublic/index.js +5 -1
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/shares.d.ts +1 -1
- package/dist/internal/sharingPublic/shares.js +1 -2
- package/dist/internal/sharingPublic/shares.js.map +1 -1
- package/dist/internal/upload/apiService.d.ts +0 -9
- package/dist/internal/upload/apiService.js +0 -16
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +0 -4
- package/dist/internal/upload/cryptoService.js +0 -6
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +0 -1
- package/dist/internal/upload/fileUploader.js +0 -4
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/manager.d.ts +0 -1
- package/dist/internal/upload/manager.js +0 -51
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +0 -61
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +17 -2
- package/dist/protonDriveClient.js +19 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +119 -4
- package/dist/protonDrivePhotosClient.js +183 -10
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +33 -1
- package/dist/protonDrivePublicLinkClient.js +51 -2
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/package.json +1 -1
- package/src/diagnostic/diagnostic.ts +27 -8
- package/src/diagnostic/index.ts +17 -2
- package/src/diagnostic/interface.ts +35 -9
- package/src/diagnostic/nodeUtils.ts +100 -0
- package/src/diagnostic/{sdkDiagnostic.ts → sdkDiagnosticBase.ts} +204 -204
- package/src/diagnostic/sdkDiagnosticMain.ts +95 -0
- package/src/diagnostic/sdkDiagnosticPhotos.ts +70 -0
- package/src/interface/index.ts +1 -1
- package/src/interface/upload.ts +1 -13
- 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/errors.test.ts +62 -1
- package/src/internal/errors.ts +27 -0
- package/src/internal/nodes/apiService.test.ts +1 -1
- package/src/internal/nodes/apiService.ts +42 -0
- package/src/internal/nodes/cryptoReporter.ts +6 -5
- package/src/internal/nodes/cryptoService.ts +9 -0
- package/src/internal/nodes/index.test.ts +2 -1
- package/src/internal/nodes/index.ts +2 -1
- package/src/internal/nodes/interface.ts +1 -1
- package/src/internal/nodes/nodeName.test.ts +57 -0
- package/src/internal/nodes/nodeName.ts +26 -0
- package/src/internal/nodes/nodesAccess.test.ts +2 -2
- package/src/internal/nodes/nodesAccess.ts +5 -5
- package/src/internal/nodes/nodesManagement.test.ts +65 -0
- package/src/internal/nodes/nodesManagement.ts +43 -1
- package/src/internal/photos/albums.ts +1 -1
- package/src/internal/photos/apiService.ts +40 -0
- package/src/internal/photos/index.ts +13 -1
- package/src/internal/photos/interface.ts +4 -1
- package/src/internal/photos/shares.ts +3 -3
- package/src/internal/photos/timeline.test.ts +116 -0
- package/src/internal/photos/timeline.ts +47 -2
- package/src/internal/shares/cryptoService.ts +5 -1
- package/src/internal/shares/index.ts +1 -0
- package/src/internal/shares/interface.ts +9 -0
- package/src/internal/shares/manager.test.ts +7 -7
- package/src/internal/shares/manager.ts +4 -4
- package/src/internal/sharing/apiService.ts +15 -12
- package/src/internal/sharing/index.ts +7 -1
- package/src/internal/sharing/interface.ts +1 -1
- package/src/internal/sharing/sharingAccess.test.ts +1 -1
- package/src/internal/sharing/sharingAccess.ts +1 -1
- package/src/internal/sharing/sharingManagement.test.ts +59 -1
- package/src/internal/sharing/sharingManagement.ts +33 -14
- package/src/internal/sharingPublic/cryptoReporter.ts +5 -1
- package/src/internal/sharingPublic/index.ts +5 -1
- package/src/internal/sharingPublic/shares.ts +1 -2
- package/src/internal/upload/apiService.ts +0 -39
- package/src/internal/upload/cryptoService.ts +0 -9
- package/src/internal/upload/fileUploader.ts +0 -5
- package/src/internal/upload/manager.test.ts +0 -65
- package/src/internal/upload/manager.ts +0 -64
- package/src/protonDriveClient.ts +21 -2
- package/src/protonDrivePhotosClient.ts +217 -9
- package/src/protonDrivePublicLinkClient.ts +77 -2
- package/dist/diagnostic/sdkDiagnostic.d.ts +0 -23
- package/dist/diagnostic/sdkDiagnostic.js +0 -320
- package/dist/diagnostic/sdkDiagnostic.js.map +0 -1
package/src/interface/upload.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type UploadMetadata = {
|
|
|
32
32
|
overrideExistingDraftByOtherClient?: boolean;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
export interface
|
|
35
|
+
export interface FileUploader {
|
|
36
36
|
/**
|
|
37
37
|
* Uploads a file from a stream.
|
|
38
38
|
*
|
|
@@ -64,18 +64,6 @@ export interface FileRevisionUploader {
|
|
|
64
64
|
): Promise<UploadController>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export interface FileUploader extends FileRevisionUploader {
|
|
68
|
-
/**
|
|
69
|
-
* Returns the available name for the file.
|
|
70
|
-
*
|
|
71
|
-
* The function will return a name that includes the original name with the
|
|
72
|
-
* available index. The name is guaranteed to be unique in the parent folder.
|
|
73
|
-
*
|
|
74
|
-
* Example new name: `file (2).txt`.
|
|
75
|
-
*/
|
|
76
|
-
getAvailableName(): Promise<string>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
67
|
export interface UploadController {
|
|
80
68
|
pause(): void;
|
|
81
69
|
resume(): void;
|
|
@@ -13,7 +13,7 @@ export type DeviceMetadata = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export interface SharesService {
|
|
16
|
-
|
|
16
|
+
getRootIDs(): 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
|
+
getRootIDs: 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.getRootIDs.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.getRootIDs).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.getRootIDs();
|
|
51
51
|
const { address, shareKey, node } = await this.cryptoService.createDevice(name);
|
|
52
52
|
|
|
53
53
|
const device = await this.apiService.createDevice(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { VERIFICATION_STATUS } from '../crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { AbortError, ConnectionError, RateLimitedError, ValidationError } from '../errors';
|
|
3
|
+
import { getVerificationMessage, isNotApplicationError } from './errors';
|
|
3
4
|
|
|
4
5
|
describe('getVerificationMessage', () => {
|
|
5
6
|
const testCases: [VERIFICATION_STATUS, Error[] | undefined, string | undefined, boolean, string][] = [
|
|
@@ -53,3 +54,63 @@ describe('getVerificationMessage', () => {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
});
|
|
57
|
+
|
|
58
|
+
describe('isNotApplicationError', () => {
|
|
59
|
+
describe('SDK errors that should be ignored', () => {
|
|
60
|
+
it('returns true for AbortError', () => {
|
|
61
|
+
const error = new AbortError('Operation aborted');
|
|
62
|
+
expect(isNotApplicationError(error)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns true for ValidationError', () => {
|
|
66
|
+
const error = new ValidationError('Validation failed');
|
|
67
|
+
expect(isNotApplicationError(error)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns true for RateLimitedError', () => {
|
|
71
|
+
const error = new RateLimitedError('Rate limited');
|
|
72
|
+
expect(isNotApplicationError(error)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns true for ConnectionError', () => {
|
|
76
|
+
const error = new ConnectionError('Connection failed');
|
|
77
|
+
expect(isNotApplicationError(error)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('General errors with specific names that should be ignored', () => {
|
|
82
|
+
it('returns true for Error with name AbortError', () => {
|
|
83
|
+
const error = new Error('Aborted');
|
|
84
|
+
error.name = 'AbortError';
|
|
85
|
+
expect(isNotApplicationError(error)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns true for Error with name OfflineError', () => {
|
|
89
|
+
const error = new Error('Offline');
|
|
90
|
+
error.name = 'OfflineError';
|
|
91
|
+
expect(isNotApplicationError(error)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns true for Error with name TimeoutError', () => {
|
|
95
|
+
const error = new Error('Timeout');
|
|
96
|
+
error.name = 'TimeoutError';
|
|
97
|
+
expect(isNotApplicationError(error)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Errors that should not be ignored', () => {
|
|
102
|
+
it('returns false for regular Error', () => {
|
|
103
|
+
const error = new Error('Regular error');
|
|
104
|
+
expect(isNotApplicationError(error)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns false for undefined', () => {
|
|
108
|
+
expect(isNotApplicationError(undefined)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns false for non-Error object', () => {
|
|
112
|
+
const error = { message: 'Not an error' };
|
|
113
|
+
expect(isNotApplicationError(error)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/internal/errors.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { VERIFICATION_STATUS } from '../crypto';
|
|
4
|
+
import { AbortError, ConnectionError, RateLimitedError, ValidationError } from '../errors';
|
|
4
5
|
|
|
5
6
|
export function getErrorMessage(error: unknown): string {
|
|
6
7
|
return error instanceof Error ? error.message : c('Error').t`Unknown error`;
|
|
@@ -36,3 +37,29 @@ export function getVerificationMessage(
|
|
|
36
37
|
? c('Error').t`Signature verification for ${signatureType} failed`
|
|
37
38
|
: c('Error').t`Signature verification failed`;
|
|
38
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns true if the error is not an application error (it is for example
|
|
43
|
+
* a network error failing to fetch keys) and can be ignored for telemetry.
|
|
44
|
+
*/
|
|
45
|
+
export function isNotApplicationError(error?: unknown): boolean {
|
|
46
|
+
// SDK errors.
|
|
47
|
+
if (
|
|
48
|
+
error instanceof AbortError ||
|
|
49
|
+
error instanceof ValidationError ||
|
|
50
|
+
error instanceof RateLimitedError ||
|
|
51
|
+
error instanceof ConnectionError
|
|
52
|
+
) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// General errors that can come from the SDK dependencies (notably Account
|
|
57
|
+
// dependency which loads the keys for the crypto services).
|
|
58
|
+
if (error instanceof Error) {
|
|
59
|
+
if (error.name === 'AbortError' || error.name === 'OfflineError' || error.name === 'TimeoutError') {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
@@ -102,6 +102,14 @@ type PostRestoreRevisionResponse =
|
|
|
102
102
|
type DeleteRevisionResponse =
|
|
103
103
|
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/{revisionID}']['delete']['responses']['200']['content']['application/json'];
|
|
104
104
|
|
|
105
|
+
|
|
106
|
+
type PostCheckAvailableHashesRequest = Extract<
|
|
107
|
+
drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/checkAvailableHashes']['post']['requestBody'],
|
|
108
|
+
{ content: object }
|
|
109
|
+
>['content']['application/json'];
|
|
110
|
+
type PostCheckAvailableHashesResponse =
|
|
111
|
+
drivePaths['/drive/v2/volumes/{volumeID}/links/{linkID}/checkAvailableHashes']['post']['responses']['200']['content']['application/json'];
|
|
112
|
+
|
|
105
113
|
/**
|
|
106
114
|
* Provides API communication for fetching and manipulating nodes metadata.
|
|
107
115
|
*
|
|
@@ -112,9 +120,11 @@ export class NodeAPIService {
|
|
|
112
120
|
constructor(
|
|
113
121
|
private logger: Logger,
|
|
114
122
|
private apiService: DriveAPIService,
|
|
123
|
+
private clientUid: string | undefined,
|
|
115
124
|
) {
|
|
116
125
|
this.logger = logger;
|
|
117
126
|
this.apiService = apiService;
|
|
127
|
+
this.clientUid = clientUid;
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
async getNode(nodeUid: string, ownVolumeId: string, signal?: AbortSignal): Promise<EncryptedNode> {
|
|
@@ -526,6 +536,38 @@ export class NodeAPIService {
|
|
|
526
536
|
`drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/${revisionId}`,
|
|
527
537
|
);
|
|
528
538
|
}
|
|
539
|
+
|
|
540
|
+
async checkAvailableHashes(
|
|
541
|
+
parentNodeUid: string,
|
|
542
|
+
hashes: string[],
|
|
543
|
+
): Promise<{
|
|
544
|
+
availableHashes: string[];
|
|
545
|
+
pendingHashes: {
|
|
546
|
+
hash: string;
|
|
547
|
+
nodeUid: string;
|
|
548
|
+
revisionUid: string;
|
|
549
|
+
clientUid?: string;
|
|
550
|
+
}[];
|
|
551
|
+
}> {
|
|
552
|
+
const { volumeId, nodeId: parentNodeId } = splitNodeUid(parentNodeUid);
|
|
553
|
+
const result = await this.apiService.post<PostCheckAvailableHashesRequest, PostCheckAvailableHashesResponse>(
|
|
554
|
+
`drive/v2/volumes/${volumeId}/links/${parentNodeId}/checkAvailableHashes`,
|
|
555
|
+
{
|
|
556
|
+
Hashes: hashes,
|
|
557
|
+
ClientUID: this.clientUid ? [this.clientUid] : null,
|
|
558
|
+
},
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
availableHashes: result.AvailableHashes,
|
|
563
|
+
pendingHashes: result.PendingHashes.map((hash) => ({
|
|
564
|
+
hash: hash.Hash,
|
|
565
|
+
nodeUid: makeNodeUid(volumeId, hash.LinkID),
|
|
566
|
+
revisionUid: makeNodeRevisionUid(volumeId, hash.LinkID, hash.RevisionID),
|
|
567
|
+
clientUid: hash.ClientUID || undefined,
|
|
568
|
+
})),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
529
571
|
}
|
|
530
572
|
|
|
531
573
|
type LinkResponse = {
|
|
@@ -9,12 +9,9 @@ import {
|
|
|
9
9
|
MetricsDecryptionErrorField,
|
|
10
10
|
MetricVerificationErrorField,
|
|
11
11
|
} from '../../interface';
|
|
12
|
-
import { getVerificationMessage } from '../errors';
|
|
12
|
+
import { getVerificationMessage, isNotApplicationError } from '../errors';
|
|
13
13
|
import { splitNodeUid } from '../uids';
|
|
14
|
-
import {
|
|
15
|
-
EncryptedNode,
|
|
16
|
-
SharesService,
|
|
17
|
-
} from './interface';
|
|
14
|
+
import { EncryptedNode, SharesService } from './interface';
|
|
18
15
|
|
|
19
16
|
export class NodesCryptoReporter {
|
|
20
17
|
private logger: Logger;
|
|
@@ -92,6 +89,10 @@ export class NodesCryptoReporter {
|
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
async reportDecryptionError(node: EncryptedNode, field: MetricsDecryptionErrorField, error: unknown) {
|
|
92
|
+
if (isNotApplicationError(error)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
95
96
|
if (this.reportedDecryptionErrors.has(node.uid)) {
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
@@ -645,6 +645,15 @@ export class NodesCryptoService {
|
|
|
645
645
|
nameSignatureEmail: email,
|
|
646
646
|
};
|
|
647
647
|
}
|
|
648
|
+
|
|
649
|
+
async generateNameHashes(parentHashKey: Uint8Array, names: string[]): Promise<{ name: string; hash: string }[]> {
|
|
650
|
+
return Promise.all(
|
|
651
|
+
names.map(async (name) => ({
|
|
652
|
+
name,
|
|
653
|
+
hash: await this.driveCrypto.generateLookupHash(name, parentHashKey),
|
|
654
|
+
})),
|
|
655
|
+
);
|
|
656
|
+
}
|
|
648
657
|
}
|
|
649
658
|
|
|
650
659
|
function getClaimedAuthor(
|
|
@@ -53,7 +53,7 @@ describe('nodesModules integration tests', () => {
|
|
|
53
53
|
driveCrypto = {};
|
|
54
54
|
// @ts-expect-error No need to implement all methods for mocking
|
|
55
55
|
sharesService = {
|
|
56
|
-
|
|
56
|
+
getRootIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
nodesModule = initNodesModule(
|
|
@@ -64,6 +64,7 @@ describe('nodesModules integration tests', () => {
|
|
|
64
64
|
account,
|
|
65
65
|
driveCrypto,
|
|
66
66
|
sharesService,
|
|
67
|
+
'clientUid',
|
|
67
68
|
);
|
|
68
69
|
|
|
69
70
|
nodesCache = new NodesCache(getMockLogger(), driveEntitiesCache);
|
|
@@ -37,8 +37,9 @@ export function initNodesModule(
|
|
|
37
37
|
account: ProtonDriveAccount,
|
|
38
38
|
driveCrypto: DriveCrypto,
|
|
39
39
|
sharesService: SharesService,
|
|
40
|
+
clientUid: string | undefined,
|
|
40
41
|
) {
|
|
41
|
-
const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService);
|
|
42
|
+
const api = new NodeAPIService(telemetry.getLogger('nodes-api'), apiService, clientUid);
|
|
42
43
|
const cache = new NodesCache(telemetry.getLogger('nodes-cache'), driveEntitiesCache);
|
|
43
44
|
const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
|
|
44
45
|
const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
|
|
@@ -177,7 +177,7 @@ export interface DecryptedRevision extends Revision {
|
|
|
177
177
|
* Interface describing the dependencies to the shares module.
|
|
178
178
|
*/
|
|
179
179
|
export interface SharesService {
|
|
180
|
-
|
|
180
|
+
getRootIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
|
|
181
181
|
getSharePrivateKey(shareId: string): Promise<PrivateKey>;
|
|
182
182
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
183
183
|
email: string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { splitExtension, joinNameAndExtension } from './nodeName';
|
|
2
|
+
|
|
3
|
+
describe('nodeName', () => {
|
|
4
|
+
describe('splitExtension', () => {
|
|
5
|
+
it('should handle empty string', () => {
|
|
6
|
+
const result = splitExtension('');
|
|
7
|
+
expect(result).toEqual(['', '']);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should split filename with extension correctly', () => {
|
|
11
|
+
const result = splitExtension('document.pdf');
|
|
12
|
+
expect(result).toEqual(['document', 'pdf']);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should handle filename without extension', () => {
|
|
16
|
+
const result = splitExtension('folder');
|
|
17
|
+
expect(result).toEqual(['folder', '']);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should split filename with multiple dots correctly', () => {
|
|
21
|
+
const result = splitExtension('my.file.name.txt');
|
|
22
|
+
expect(result).toEqual(['my.file.name', 'txt']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle filename ending with dot', () => {
|
|
26
|
+
const result = splitExtension('dot.');
|
|
27
|
+
expect(result).toEqual(['dot.', '']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle filename with only extension', () => {
|
|
31
|
+
const result = splitExtension('.gitignore');
|
|
32
|
+
expect(result).toEqual(['.gitignore', '']);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('joinNameAndExtension', () => {
|
|
37
|
+
it('should join name, index, and extension correctly', () => {
|
|
38
|
+
const result = joinNameAndExtension('document', 1, 'pdf');
|
|
39
|
+
expect(result).toBe('document (1).pdf');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle empty name with extension', () => {
|
|
43
|
+
const result = joinNameAndExtension('', 2, 'txt');
|
|
44
|
+
expect(result).toBe('(2).txt');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle name with empty extension', () => {
|
|
48
|
+
const result = joinNameAndExtension('document', 3, '');
|
|
49
|
+
expect(result).toBe('document (3)');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle both name and extension empty', () => {
|
|
53
|
+
const result = joinNameAndExtension('', 4, '');
|
|
54
|
+
expect(result).toBe('(4)');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a filename into `[name, extension]`
|
|
3
|
+
*/
|
|
4
|
+
export function splitExtension(filename = ''): [string, string] {
|
|
5
|
+
const endIdx = filename.lastIndexOf('.');
|
|
6
|
+
if (endIdx === -1 || endIdx === 0 || endIdx === filename.length - 1) {
|
|
7
|
+
return [filename, ''];
|
|
8
|
+
}
|
|
9
|
+
return [filename.slice(0, endIdx), filename.slice(endIdx + 1)];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Join a filename into `name (index).extension`
|
|
14
|
+
*/
|
|
15
|
+
export function joinNameAndExtension(name: string, index: number, extension: string): string {
|
|
16
|
+
if (!name && !extension) {
|
|
17
|
+
return `(${index})`;
|
|
18
|
+
}
|
|
19
|
+
if (!name) {
|
|
20
|
+
return `(${index}).${extension}`;
|
|
21
|
+
}
|
|
22
|
+
if (!extension) {
|
|
23
|
+
return `${name} (${index})`;
|
|
24
|
+
}
|
|
25
|
+
return `${name} (${index}).${extension}`;
|
|
26
|
+
}
|
|
@@ -46,7 +46,7 @@ describe('nodesAccess', () => {
|
|
|
46
46
|
};
|
|
47
47
|
// @ts-expect-error No need to implement all methods for mocking
|
|
48
48
|
shareService = {
|
|
49
|
-
|
|
49
|
+
getRootIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
50
50
|
getSharePrivateKey: jest.fn(),
|
|
51
51
|
};
|
|
52
52
|
|
|
@@ -388,7 +388,7 @@ describe('nodesAccess', () => {
|
|
|
388
388
|
const node4 = { uid: 'volumeId~node4', isStale: false } as DecryptedNode;
|
|
389
389
|
|
|
390
390
|
beforeEach(() => {
|
|
391
|
-
shareService.
|
|
391
|
+
shareService.getRootIDs = jest.fn().mockResolvedValue({ volumeId });
|
|
392
392
|
apiService.iterateTrashedNodeUids = jest.fn().mockImplementation(async function* () {
|
|
393
393
|
yield node1.uid;
|
|
394
394
|
yield node2.uid;
|
|
@@ -61,7 +61,7 @@ export class NodesAccess {
|
|
|
61
61
|
private cryptoService: NodesCryptoService,
|
|
62
62
|
private shareService: Pick<
|
|
63
63
|
SharesService,
|
|
64
|
-
'
|
|
64
|
+
'getRootIDs' | 'getSharePrivateKey' | 'getContextShareMemberEmailKey'
|
|
65
65
|
>,
|
|
66
66
|
) {
|
|
67
67
|
this.logger = telemetry.getLogger('nodes');
|
|
@@ -74,7 +74,7 @@ export class NodesAccess {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async getVolumeRootFolder() {
|
|
77
|
-
const { volumeId, rootNodeId } = await this.shareService.
|
|
77
|
+
const { volumeId, rootNodeId } = await this.shareService.getRootIDs();
|
|
78
78
|
const nodeUid = makeNodeUid(volumeId, rootNodeId);
|
|
79
79
|
return this.getNode(nodeUid);
|
|
80
80
|
}
|
|
@@ -154,7 +154,7 @@ export class NodesAccess {
|
|
|
154
154
|
|
|
155
155
|
// Improvement requested: keep status of loaded trash and leverage cache.
|
|
156
156
|
async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
|
|
157
|
-
const { volumeId } = await this.shareService.
|
|
157
|
+
const { volumeId } = await this.shareService.getRootIDs();
|
|
158
158
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
159
159
|
iterateItems: (nodeUids) => this.loadNodes(nodeUids, undefined, signal),
|
|
160
160
|
batchSize: BATCH_LOADING_SIZE,
|
|
@@ -230,7 +230,7 @@ export class NodesAccess {
|
|
|
230
230
|
private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode; keys?: DecryptedNodeKeys }> {
|
|
231
231
|
this.debouncer.loadingNode(nodeUid);
|
|
232
232
|
try {
|
|
233
|
-
const { volumeId: ownVolumeId } = await this.shareService.
|
|
233
|
+
const { volumeId: ownVolumeId } = await this.shareService.getRootIDs();
|
|
234
234
|
const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
|
|
235
235
|
return this.decryptNode(encryptedNode);
|
|
236
236
|
} finally {
|
|
@@ -259,7 +259,7 @@ export class NodesAccess {
|
|
|
259
259
|
const returnedNodeUids: string[] = [];
|
|
260
260
|
const errors = [];
|
|
261
261
|
|
|
262
|
-
const { volumeId: ownVolumeId } = await this.shareService.
|
|
262
|
+
const { volumeId: ownVolumeId } = await this.shareService.getRootIDs();
|
|
263
263
|
|
|
264
264
|
const apiNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, filterOptions, signal);
|
|
265
265
|
|
|
@@ -61,6 +61,10 @@ describe('NodesManagement', () => {
|
|
|
61
61
|
yield* uids.map((uid) => ({ ok: true, uid }) as NodeResult);
|
|
62
62
|
}),
|
|
63
63
|
createFolder: jest.fn(),
|
|
64
|
+
checkAvailableHashes: jest.fn().mockResolvedValue({
|
|
65
|
+
availableHashes: ['name1Hash'],
|
|
66
|
+
pendingHashes: [],
|
|
67
|
+
}),
|
|
64
68
|
};
|
|
65
69
|
// @ts-expect-error No need to implement all methods for mocking
|
|
66
70
|
cryptoCache = {
|
|
@@ -75,6 +79,20 @@ describe('NodesManagement', () => {
|
|
|
75
79
|
}),
|
|
76
80
|
encryptNodeWithNewParent: jest.fn(),
|
|
77
81
|
createFolder: jest.fn(),
|
|
82
|
+
generateNameHashes: jest.fn().mockResolvedValue([
|
|
83
|
+
{
|
|
84
|
+
name: 'name1',
|
|
85
|
+
hash: 'name1Hash',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'name2',
|
|
89
|
+
hash: 'name2Hash',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'name3',
|
|
93
|
+
hash: 'name3Hash',
|
|
94
|
+
},
|
|
95
|
+
]),
|
|
78
96
|
};
|
|
79
97
|
// @ts-expect-error No need to implement all methods for mocking
|
|
80
98
|
nodesAccess = {
|
|
@@ -340,4 +358,51 @@ describe('NodesManagement', () => {
|
|
|
340
358
|
expect(restored).toEqual(new Set(uids));
|
|
341
359
|
expect(nodesAccess.notifyNodeChanged).toHaveBeenCalledTimes(2);
|
|
342
360
|
});
|
|
361
|
+
|
|
362
|
+
describe('findAvailableName', () => {
|
|
363
|
+
it('should find available name', async () => {
|
|
364
|
+
apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
|
|
365
|
+
return {
|
|
366
|
+
availableHashes: ['name3Hash'],
|
|
367
|
+
pendingHashes: [],
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const result = await management.findAvailableName('parentUid', 'name');
|
|
372
|
+
expect(result).toBe('name3');
|
|
373
|
+
expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(1);
|
|
374
|
+
expect(apiService.checkAvailableHashes).toHaveBeenCalledWith('parentUid', [
|
|
375
|
+
'name1Hash',
|
|
376
|
+
'name2Hash',
|
|
377
|
+
'name3Hash',
|
|
378
|
+
]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should find available name with multiple pages', async () => {
|
|
382
|
+
let firstCall = false;
|
|
383
|
+
apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
|
|
384
|
+
if (!firstCall) {
|
|
385
|
+
firstCall = true;
|
|
386
|
+
return {
|
|
387
|
+
// First page has no available hashes
|
|
388
|
+
availableHashes: [],
|
|
389
|
+
pendingHashes: [],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
availableHashes: ['name3Hash'],
|
|
394
|
+
pendingHashes: [],
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const result = await management.findAvailableName('parentUid', 'name');
|
|
399
|
+
expect(result).toBe('name3');
|
|
400
|
+
expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(2);
|
|
401
|
+
expect(apiService.checkAvailableHashes).toHaveBeenCalledWith('parentUid', [
|
|
402
|
+
'name1Hash',
|
|
403
|
+
'name2Hash',
|
|
404
|
+
'name3Hash',
|
|
405
|
+
]);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
343
408
|
});
|
|
@@ -8,10 +8,14 @@ import { NodeAPIService } from './apiService';
|
|
|
8
8
|
import { NodesCryptoCache } from './cryptoCache';
|
|
9
9
|
import { NodesCryptoService } from './cryptoService';
|
|
10
10
|
import { NodeOutOfSyncError } from './errors';
|
|
11
|
+
import { generateFolderExtendedAttributes } from './extendedAttributes';
|
|
11
12
|
import { DecryptedNode } from './interface';
|
|
13
|
+
import { splitExtension, joinNameAndExtension } from './nodeName';
|
|
12
14
|
import { NodesAccess } from './nodesAccess';
|
|
13
15
|
import { validateNodeName } from './validations';
|
|
14
|
-
|
|
16
|
+
|
|
17
|
+
const AVAILABLE_NAME_BATCH_SIZE = 10;
|
|
18
|
+
const AVAILABLE_NAME_LIMIT = 1000;
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Provides high-level actions for managing nodes.
|
|
@@ -349,4 +353,42 @@ export class NodesManagement {
|
|
|
349
353
|
await this.cryptoCache.setNodeKeys(nodeUid, keys);
|
|
350
354
|
return node;
|
|
351
355
|
}
|
|
356
|
+
|
|
357
|
+
async findAvailableName(parentFolderUid: string, name: string): Promise<string> {
|
|
358
|
+
const { hashKey: parentHashKey } = await this.nodesAccess.getNodeKeys(parentFolderUid);
|
|
359
|
+
if (!parentHashKey) {
|
|
360
|
+
throw new ValidationError(c('Error').t`Creating files in non-folders is not allowed`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const [namePart, extension] = splitExtension(name);
|
|
364
|
+
|
|
365
|
+
let startIndex = 1;
|
|
366
|
+
while (startIndex < AVAILABLE_NAME_LIMIT) {
|
|
367
|
+
const namesToCheck = startIndex === 1 ? [name] : [];
|
|
368
|
+
for (let i = startIndex; i < startIndex + AVAILABLE_NAME_BATCH_SIZE; i++) {
|
|
369
|
+
namesToCheck.push(joinNameAndExtension(namePart, i, extension));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const hashesToCheck = await this.cryptoService.generateNameHashes(parentHashKey, namesToCheck);
|
|
373
|
+
|
|
374
|
+
const { availableHashes } = await this.apiService.checkAvailableHashes(
|
|
375
|
+
parentFolderUid,
|
|
376
|
+
hashesToCheck.map(({ hash }) => hash),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if (!availableHashes.length) {
|
|
380
|
+
startIndex += AVAILABLE_NAME_BATCH_SIZE;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const availableHash = hashesToCheck.find(({ hash }) => hash === availableHashes[0]);
|
|
385
|
+
if (!availableHash) {
|
|
386
|
+
throw Error('Backend returned unexpected hash');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return availableHash.name;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
throw new ValidationError(c('Error').t`No available name found`);
|
|
393
|
+
}
|
|
352
394
|
}
|
|
@@ -21,7 +21,7 @@ export class Albums {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async *iterateAlbums(signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
|
|
24
|
-
const { volumeId } = await this.photoShares.
|
|
24
|
+
const { volumeId } = await this.photoShares.getRootIDs();
|
|
25
25
|
|
|
26
26
|
const batchLoading = new BatchLoading<string, DecryptedNode>({
|
|
27
27
|
iterateItems: (nodeUids) => this.iterateNodesAndIgnoreMissingOnes(nodeUids, signal),
|