@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
|
@@ -18,6 +18,13 @@ type GetTimelineResponse =
|
|
|
18
18
|
type GetAlbumsResponse =
|
|
19
19
|
drivePaths['/drive/photos/volumes/{volumeID}/albums']['get']['responses']['200']['content']['application/json'];
|
|
20
20
|
|
|
21
|
+
type PostPhotoDuplicateRequest = Extract<
|
|
22
|
+
drivePaths['/drive/volumes/{volumeID}/photos/duplicates']['post']['requestBody'],
|
|
23
|
+
{ content: object }
|
|
24
|
+
>['content']['application/json'];
|
|
25
|
+
type PostPhotoDuplicateResponse =
|
|
26
|
+
drivePaths['/drive/volumes/{volumeID}/photos/duplicates']['post']['responses']['200']['content']['application/json'];
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
29
|
* Provides API communication for fetching and manipulating photos and albums
|
|
23
30
|
* metadata.
|
|
@@ -148,4 +155,37 @@ export class PhotosAPIService {
|
|
|
148
155
|
anchor = response.AnchorID;
|
|
149
156
|
}
|
|
150
157
|
}
|
|
158
|
+
|
|
159
|
+
async checkPhotoDuplicates(
|
|
160
|
+
volumeId: string,
|
|
161
|
+
nameHashes: string[],
|
|
162
|
+
signal?: AbortSignal,
|
|
163
|
+
): Promise<
|
|
164
|
+
{
|
|
165
|
+
nameHash: string;
|
|
166
|
+
contentHash: string;
|
|
167
|
+
nodeUid: string;
|
|
168
|
+
clientUid?: string;
|
|
169
|
+
}[]
|
|
170
|
+
> {
|
|
171
|
+
const response = await this.apiService.post<PostPhotoDuplicateRequest, PostPhotoDuplicateResponse>(
|
|
172
|
+
`drive/volumes/${volumeId}/photos/duplicates`,
|
|
173
|
+
{
|
|
174
|
+
NameHashes: nameHashes,
|
|
175
|
+
},
|
|
176
|
+
signal,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return response.DuplicateHashes.map((duplicate) => {
|
|
180
|
+
if (!duplicate.Hash || !duplicate.ContentHash || duplicate.LinkState !== 1 /* Active */) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
nameHash: duplicate.Hash,
|
|
185
|
+
contentHash: duplicate.ContentHash,
|
|
186
|
+
nodeUid: makeNodeUid(volumeId, duplicate.LinkID),
|
|
187
|
+
clientUid: duplicate.ClientUID || undefined,
|
|
188
|
+
};
|
|
189
|
+
}).filter((duplicate) => duplicate !== undefined);
|
|
190
|
+
}
|
|
151
191
|
}
|
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
PhotoUploadManager,
|
|
25
25
|
PhotoUploadMetadata,
|
|
26
26
|
} from './upload';
|
|
27
|
+
import { ShareTargetType } from '../shares';
|
|
28
|
+
|
|
29
|
+
// Only photos and albums can be shared in photos volume.
|
|
30
|
+
export const PHOTOS_SHARE_TARGET_TYPES = [ShareTargetType.Photo, ShareTargetType.Album];
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Provides facade for the whole photos module.
|
|
@@ -32,12 +36,20 @@ import {
|
|
|
32
36
|
* including API communication, crypto, caching, and event handling.
|
|
33
37
|
*/
|
|
34
38
|
export function initPhotosModule(
|
|
39
|
+
telemetry: ProtonDriveTelemetry,
|
|
35
40
|
apiService: DriveAPIService,
|
|
41
|
+
driveCrypto: DriveCrypto,
|
|
36
42
|
photoShares: PhotoSharesManager,
|
|
37
43
|
nodesService: NodesService,
|
|
38
44
|
) {
|
|
39
45
|
const api = new PhotosAPIService(apiService);
|
|
40
|
-
const timeline = new PhotosTimeline(
|
|
46
|
+
const timeline = new PhotosTimeline(
|
|
47
|
+
telemetry.getLogger('photos-timeline'),
|
|
48
|
+
api,
|
|
49
|
+
driveCrypto,
|
|
50
|
+
photoShares,
|
|
51
|
+
nodesService,
|
|
52
|
+
);
|
|
41
53
|
const albums = new Albums(api, photoShares, nodesService);
|
|
42
54
|
|
|
43
55
|
return {
|
|
@@ -4,7 +4,7 @@ import { DecryptedNode } from '../nodes';
|
|
|
4
4
|
import { EncryptedShare } from '../shares';
|
|
5
5
|
|
|
6
6
|
export interface SharesService {
|
|
7
|
-
|
|
7
|
+
getRootIDs(): Promise<{ volumeId: string; rootNodeId: string }>;
|
|
8
8
|
loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
|
|
9
9
|
getSharePrivateKey(shareId: string): Promise<PrivateKey>;
|
|
10
10
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
@@ -26,4 +26,7 @@ export interface SharesService {
|
|
|
26
26
|
export interface NodesService {
|
|
27
27
|
getNode(nodeUid: string): Promise<DecryptedNode>;
|
|
28
28
|
iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode>;
|
|
29
|
+
getNodeKeys(nodeUid: string): Promise<{
|
|
30
|
+
hashKey?: Uint8Array;
|
|
31
|
+
}>;
|
|
29
32
|
}
|
|
@@ -34,7 +34,7 @@ export class PhotoSharesManager {
|
|
|
34
34
|
this.sharesService = sharesService;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
async
|
|
37
|
+
async getRootIDs(): Promise<VolumeShareNodeIDs> {
|
|
38
38
|
if (this.photoRootIds) {
|
|
39
39
|
return this.photoRootIds;
|
|
40
40
|
}
|
|
@@ -113,7 +113,7 @@ export class PhotoSharesManager {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
async isOwnVolume(volumeId: string): Promise<boolean> {
|
|
116
|
-
const { volumeId: myVolumeId } = await this.
|
|
116
|
+
const { volumeId: myVolumeId } = await this.getRootIDs();
|
|
117
117
|
if (volumeId === myVolumeId) {
|
|
118
118
|
return true;
|
|
119
119
|
}
|
|
@@ -121,7 +121,7 @@ export class PhotoSharesManager {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
async getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType> {
|
|
124
|
-
const { volumeId: myVolumeId } = await this.
|
|
124
|
+
const { volumeId: myVolumeId } = await this.getRootIDs();
|
|
125
125
|
if (volumeId === myVolumeId) {
|
|
126
126
|
return MetricVolumeType.OwnVolume;
|
|
127
127
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { getMockLogger } from '../../tests/logger';
|
|
2
|
+
import { DriveCrypto } from '../../crypto';
|
|
3
|
+
import { makeNodeUid } from '../uids';
|
|
4
|
+
import { PhotosAPIService } from './apiService';
|
|
5
|
+
import { NodesService } from './interface';
|
|
6
|
+
import { PhotoSharesManager } from './shares';
|
|
7
|
+
import { PhotosTimeline } from './timeline';
|
|
8
|
+
|
|
9
|
+
describe('PhotosTimeline', () => {
|
|
10
|
+
let logger: ReturnType<typeof getMockLogger>;
|
|
11
|
+
let apiService: PhotosAPIService;
|
|
12
|
+
let driveCrypto: DriveCrypto;
|
|
13
|
+
let photoShares: PhotoSharesManager;
|
|
14
|
+
let nodesService: NodesService;
|
|
15
|
+
let timeline: PhotosTimeline;
|
|
16
|
+
|
|
17
|
+
const volumeId = 'volumeId';
|
|
18
|
+
const rootNodeId = 'rootNodeId';
|
|
19
|
+
const rootNodeUid = makeNodeUid(volumeId, rootNodeId);
|
|
20
|
+
const hashKey = new Uint8Array([1, 2, 3]);
|
|
21
|
+
const name = 'photo.jpg';
|
|
22
|
+
const nameHash = 'nameHash123';
|
|
23
|
+
const sha1 = 'sha1Hash123';
|
|
24
|
+
const contentHash = 'contentHash123';
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
logger = getMockLogger();
|
|
28
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
29
|
+
apiService = {
|
|
30
|
+
checkPhotoDuplicates: jest.fn(),
|
|
31
|
+
};
|
|
32
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
33
|
+
driveCrypto = {
|
|
34
|
+
generateLookupHash: jest.fn(),
|
|
35
|
+
};
|
|
36
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
37
|
+
photoShares = {
|
|
38
|
+
getRootIDs: jest.fn().mockResolvedValue({ volumeId, rootNodeId }),
|
|
39
|
+
};
|
|
40
|
+
// @ts-expect-error No need to implement all methods for mocking
|
|
41
|
+
nodesService = {
|
|
42
|
+
getNodeKeys: jest.fn().mockResolvedValue({ hashKey }),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
timeline = new PhotosTimeline(logger, apiService, driveCrypto, photoShares, nodesService);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('isDuplicatePhoto', () => {
|
|
49
|
+
it('should not call sha1 callback when there is no name hash match', async () => {
|
|
50
|
+
const generateSha1 = jest.fn();
|
|
51
|
+
apiService.checkPhotoDuplicates = jest.fn().mockResolvedValue([]);
|
|
52
|
+
driveCrypto.generateLookupHash = jest.fn().mockResolvedValue(nameHash);
|
|
53
|
+
|
|
54
|
+
const result = await timeline.isDuplicatePhoto(name, generateSha1);
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
expect(generateSha1).not.toHaveBeenCalled();
|
|
58
|
+
expect(photoShares.getRootIDs).toHaveBeenCalled();
|
|
59
|
+
expect(nodesService.getNodeKeys).toHaveBeenCalledWith(rootNodeUid);
|
|
60
|
+
expect(driveCrypto.generateLookupHash).toHaveBeenCalledWith(name, hashKey);
|
|
61
|
+
expect(apiService.checkPhotoDuplicates).toHaveBeenCalledWith(volumeId, [nameHash], undefined);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should call sha1 callback and not logger when name hash match but content hash does not', async () => {
|
|
65
|
+
const generateSha1 = jest.fn().mockResolvedValue(sha1);
|
|
66
|
+
const duplicates = [
|
|
67
|
+
{
|
|
68
|
+
nameHash: nameHash,
|
|
69
|
+
contentHash: 'differentContentHash',
|
|
70
|
+
nodeUid: 'volumeId~node1',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
apiService.checkPhotoDuplicates = jest.fn().mockResolvedValue(duplicates);
|
|
74
|
+
driveCrypto.generateLookupHash = jest
|
|
75
|
+
.fn()
|
|
76
|
+
.mockResolvedValueOnce(nameHash)
|
|
77
|
+
.mockResolvedValueOnce(contentHash);
|
|
78
|
+
|
|
79
|
+
const result = await timeline.isDuplicatePhoto(name, generateSha1);
|
|
80
|
+
|
|
81
|
+
expect(result).toBe(false);
|
|
82
|
+
expect(generateSha1).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(driveCrypto.generateLookupHash).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect(driveCrypto.generateLookupHash).toHaveBeenNthCalledWith(1, name, hashKey);
|
|
85
|
+
expect(driveCrypto.generateLookupHash).toHaveBeenNthCalledWith(2, sha1, hashKey);
|
|
86
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should call sha1 and logger when name and content hashes match', async () => {
|
|
90
|
+
const generateSha1 = jest.fn().mockResolvedValue(sha1);
|
|
91
|
+
const nodeUid1 = 'volumeId~node1';
|
|
92
|
+
const duplicates = [
|
|
93
|
+
{
|
|
94
|
+
nameHash: nameHash,
|
|
95
|
+
contentHash: contentHash,
|
|
96
|
+
nodeUid: nodeUid1,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
apiService.checkPhotoDuplicates = jest.fn().mockResolvedValue(duplicates);
|
|
100
|
+
driveCrypto.generateLookupHash = jest
|
|
101
|
+
.fn()
|
|
102
|
+
.mockResolvedValueOnce(nameHash)
|
|
103
|
+
.mockResolvedValueOnce(contentHash);
|
|
104
|
+
|
|
105
|
+
const result = await timeline.isDuplicatePhoto(name, generateSha1);
|
|
106
|
+
|
|
107
|
+
expect(result).toBe(true);
|
|
108
|
+
expect(generateSha1).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(logger.debug).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
111
|
+
`Duplicate photo found: name hash: ${nameHash}, content hash: ${contentHash}, node uids: ${nodeUid1}`,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { DriveCrypto } from '../../crypto';
|
|
2
|
+
import { Logger } from '../../interface';
|
|
3
|
+
import { makeNodeUid } from '../uids';
|
|
1
4
|
import { PhotosAPIService } from './apiService';
|
|
5
|
+
import { NodesService } from './interface';
|
|
2
6
|
import { PhotoSharesManager } from './shares';
|
|
3
7
|
|
|
4
8
|
/**
|
|
@@ -6,19 +10,60 @@ import { PhotoSharesManager } from './shares';
|
|
|
6
10
|
*/
|
|
7
11
|
export class PhotosTimeline {
|
|
8
12
|
constructor(
|
|
13
|
+
private logger: Logger,
|
|
9
14
|
private apiService: PhotosAPIService,
|
|
15
|
+
private driveCrypto: DriveCrypto,
|
|
10
16
|
private photoShares: PhotoSharesManager,
|
|
17
|
+
private nodesService: NodesService,
|
|
11
18
|
) {
|
|
19
|
+
this.logger = logger;
|
|
12
20
|
this.apiService = apiService;
|
|
21
|
+
this.driveCrypto = driveCrypto;
|
|
13
22
|
this.photoShares = photoShares;
|
|
23
|
+
this.nodesService = nodesService;
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
async*
|
|
26
|
+
async *iterateTimeline(signal?: AbortSignal): AsyncGenerator<{
|
|
17
27
|
nodeUid: string;
|
|
18
28
|
captureTime: Date;
|
|
19
29
|
tags: number[];
|
|
20
30
|
}> {
|
|
21
|
-
const { volumeId } = await this.photoShares.
|
|
31
|
+
const { volumeId } = await this.photoShares.getRootIDs();
|
|
22
32
|
yield* this.apiService.iterateTimeline(volumeId, signal);
|
|
23
33
|
}
|
|
34
|
+
|
|
35
|
+
async isDuplicatePhoto(name: string, generateSha1: () => Promise<string>, signal?: AbortSignal): Promise<boolean> {
|
|
36
|
+
const { volumeId, rootNodeId } = await this.photoShares.getRootIDs();
|
|
37
|
+
const rootNodeUid = makeNodeUid(volumeId, rootNodeId);
|
|
38
|
+
const { hashKey } = await this.nodesService.getNodeKeys(rootNodeUid);
|
|
39
|
+
if (!hashKey) {
|
|
40
|
+
throw new Error('Hash key of photo root node not found');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const nameHash = await this.driveCrypto.generateLookupHash(name, hashKey);
|
|
44
|
+
const duplicates = await this.apiService.checkPhotoDuplicates(volumeId, [nameHash], signal);
|
|
45
|
+
|
|
46
|
+
if (duplicates.length === 0) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate the SHA1 only when there is any matching node hash to avoid
|
|
51
|
+
// computing it for every node as in most cases there is no match.
|
|
52
|
+
const sha1 = await generateSha1();
|
|
53
|
+
const contentHash = await this.driveCrypto.generateLookupHash(sha1, hashKey);
|
|
54
|
+
|
|
55
|
+
const matchingDuplicates = duplicates.filter(
|
|
56
|
+
(duplicate) => duplicate.nameHash === nameHash && duplicate.contentHash === contentHash,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (matchingDuplicates.length === 0) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nodeUids = matchingDuplicates.map((duplicate) => duplicate.nodeUid);
|
|
64
|
+
this.logger.debug(
|
|
65
|
+
`Duplicate photo found: name hash: ${nameHash}, content hash: ${contentHash}, node uids: ${nodeUids}`,
|
|
66
|
+
);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
24
69
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
MetricVolumeType,
|
|
10
10
|
} from '../../interface';
|
|
11
11
|
import { DriveCrypto, PrivateKey, VERIFICATION_STATUS } from '../../crypto';
|
|
12
|
-
import { getVerificationMessage } from '../errors';
|
|
12
|
+
import { getVerificationMessage, isNotApplicationError } from '../errors';
|
|
13
13
|
import {
|
|
14
14
|
EncryptedRootShare,
|
|
15
15
|
DecryptedRootShare,
|
|
@@ -119,6 +119,10 @@ export class SharesCryptoService {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
private reportDecryptionError(share: EncryptedRootShare, error?: unknown) {
|
|
122
|
+
if (isNotApplicationError(error)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
if (this.reportedDecryptionErrors.has(share.shareId)) {
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { PrivateKey, SessionKey } from '../../crypto';
|
|
2
2
|
import { Result, UnverifiedAuthorError } from '../../interface';
|
|
3
3
|
|
|
4
|
+
export enum ShareTargetType {
|
|
5
|
+
Root = 0,
|
|
6
|
+
Folder = 1,
|
|
7
|
+
File = 2,
|
|
8
|
+
Album = 3,
|
|
9
|
+
Photo = 4,
|
|
10
|
+
ProtonVendor = 5,
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
/**
|
|
5
14
|
* Internal interface providing basic identification of volume and its root
|
|
6
15
|
* share and node.
|
|
@@ -50,7 +50,7 @@ describe('SharesManager', () => {
|
|
|
50
50
|
manager = new SharesManager(getMockLogger(), apiService, cache, cryptoCache, cryptoService, account);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
describe('
|
|
53
|
+
describe('getRootIDs', () => {
|
|
54
54
|
const myFilesShare = {
|
|
55
55
|
shareId: 'myFilesShareId',
|
|
56
56
|
volumeId: 'myFilesVolumeId',
|
|
@@ -71,8 +71,8 @@ describe('SharesManager', () => {
|
|
|
71
71
|
cryptoService.decryptRootShare = jest.fn().mockResolvedValue({ share: myFilesShare, key });
|
|
72
72
|
|
|
73
73
|
// Calling twice to check if it loads only once.
|
|
74
|
-
await manager.
|
|
75
|
-
const result = await manager.
|
|
74
|
+
await manager.getRootIDs();
|
|
75
|
+
const result = await manager.getRootIDs();
|
|
76
76
|
|
|
77
77
|
expect(result).toStrictEqual(myFilesShare);
|
|
78
78
|
expect(apiService.getMyFiles).toHaveBeenCalledTimes(1);
|
|
@@ -103,7 +103,7 @@ describe('SharesManager', () => {
|
|
|
103
103
|
});
|
|
104
104
|
apiService.createVolume = jest.fn().mockResolvedValue(myFilesShare);
|
|
105
105
|
|
|
106
|
-
const result = await manager.
|
|
106
|
+
const result = await manager.getRootIDs();
|
|
107
107
|
|
|
108
108
|
expect(result).toStrictEqual(myFilesShare);
|
|
109
109
|
expect(cryptoService.decryptRootShare).not.toHaveBeenCalled();
|
|
@@ -113,7 +113,7 @@ describe('SharesManager', () => {
|
|
|
113
113
|
it('should throw on unknown error', async () => {
|
|
114
114
|
apiService.getMyFiles = jest.fn().mockRejectedValue(new Error('Some error'));
|
|
115
115
|
|
|
116
|
-
await expect(manager.
|
|
116
|
+
await expect(manager.getRootIDs()).rejects.toThrow('Some error');
|
|
117
117
|
expect(cryptoService.decryptRootShare).not.toHaveBeenCalled();
|
|
118
118
|
expect(apiService.createVolume).not.toHaveBeenCalled();
|
|
119
119
|
});
|
|
@@ -142,7 +142,7 @@ describe('SharesManager', () => {
|
|
|
142
142
|
|
|
143
143
|
describe('getMyFilesShareMemberEmailKey', () => {
|
|
144
144
|
it('should return cached volume email key', async () => {
|
|
145
|
-
jest.spyOn(manager, '
|
|
145
|
+
jest.spyOn(manager, 'getRootIDs').mockResolvedValue({ volumeId: 'volumeId' } as VolumeShareNodeIDs);
|
|
146
146
|
cache.getVolume = jest.fn().mockResolvedValue({ addressId: 'addressId' });
|
|
147
147
|
account.getOwnAddress = jest
|
|
148
148
|
.fn()
|
|
@@ -158,7 +158,7 @@ describe('SharesManager', () => {
|
|
|
158
158
|
});
|
|
159
159
|
|
|
160
160
|
it('should load volume email key if not in cache', async () => {
|
|
161
|
-
jest.spyOn(manager, '
|
|
161
|
+
jest.spyOn(manager, 'getRootIDs').mockResolvedValue({ volumeId: 'volumeId' } as VolumeShareNodeIDs);
|
|
162
162
|
const share = {
|
|
163
163
|
volumeId: 'volumeId',
|
|
164
164
|
shareId: 'shareId',
|
|
@@ -46,7 +46,7 @@ export class SharesManager {
|
|
|
46
46
|
*
|
|
47
47
|
* If the default volume or My files section doesn't exist, it creates it.
|
|
48
48
|
*/
|
|
49
|
-
async
|
|
49
|
+
async getRootIDs(): Promise<VolumeShareNodeIDs> {
|
|
50
50
|
if (this.myFilesIds) {
|
|
51
51
|
return this.myFilesIds;
|
|
52
52
|
}
|
|
@@ -140,7 +140,7 @@ export class SharesManager {
|
|
|
140
140
|
addressKey: PrivateKey;
|
|
141
141
|
addressKeyId: string;
|
|
142
142
|
}> {
|
|
143
|
-
const { volumeId } = await this.
|
|
143
|
+
const { volumeId } = await this.getRootIDs();
|
|
144
144
|
|
|
145
145
|
try {
|
|
146
146
|
const { addressId } = await this.cache.getVolume(volumeId);
|
|
@@ -196,11 +196,11 @@ export class SharesManager {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
async isOwnVolume(volumeId: string): Promise<boolean> {
|
|
199
|
-
return (await this.
|
|
199
|
+
return (await this.getRootIDs()).volumeId === volumeId;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
async getVolumeMetricContext(volumeId: string): Promise<MetricVolumeType> {
|
|
203
|
-
const { volumeId: myVolumeId } = await this.
|
|
203
|
+
const { volumeId: myVolumeId } = await this.getRootIDs();
|
|
204
204
|
|
|
205
205
|
// SDK doesn't support public sharing yet, also public sharing
|
|
206
206
|
// doesn't use a volume but shareURL, thus we can simplify and
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
permissionsToMemberRole,
|
|
8
8
|
memberRoleToPermission,
|
|
9
9
|
} from '../apiService';
|
|
10
|
+
import { ShareTargetType } from '../shares';
|
|
10
11
|
import {
|
|
11
12
|
makeNodeUid,
|
|
12
13
|
splitNodeUid,
|
|
@@ -119,14 +120,6 @@ type PutShareUrlRequest = Extract<
|
|
|
119
120
|
type PutShareUrlResponse =
|
|
120
121
|
drivePaths['/drive/shares/{shareID}/urls/{urlID}']['put']['responses']['200']['content']['application/json'];
|
|
121
122
|
|
|
122
|
-
// We do not support photos and albums yet.
|
|
123
|
-
const SUPPORTED_SHARE_TARGET_TYPES = [
|
|
124
|
-
0, // Root
|
|
125
|
-
1, // Folder
|
|
126
|
-
2, // File
|
|
127
|
-
5, // Proton vendor (documents and sheets)
|
|
128
|
-
];
|
|
129
|
-
|
|
130
123
|
/**
|
|
131
124
|
* Provides API communication for fetching and managing sharing.
|
|
132
125
|
*
|
|
@@ -137,9 +130,11 @@ export class SharingAPIService {
|
|
|
137
130
|
constructor(
|
|
138
131
|
private logger: Logger,
|
|
139
132
|
private apiService: DriveAPIService,
|
|
133
|
+
private shareTargetTypes: ShareTargetType[],
|
|
140
134
|
) {
|
|
141
135
|
this.logger = logger;
|
|
142
136
|
this.apiService = apiService;
|
|
137
|
+
this.shareTargetTypes = shareTargetTypes;
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
async *iterateSharedNodeUids(volumeId: string, signal?: AbortSignal): AsyncGenerator<string> {
|
|
@@ -163,6 +158,7 @@ export class SharingAPIService {
|
|
|
163
158
|
async *iterateSharedWithMeNodeUids(signal?: AbortSignal): AsyncGenerator<string> {
|
|
164
159
|
let anchor = '';
|
|
165
160
|
while (true) {
|
|
161
|
+
// TODO: Use ShareTargetTypes filter when it is supported by the API.
|
|
166
162
|
const response = await this.apiService.get<GetSharedWithMeNodesResponse>(
|
|
167
163
|
`drive/v2/sharedwithme?${anchor ? `AnchorID=${anchor}` : ''}`,
|
|
168
164
|
signal,
|
|
@@ -170,8 +166,8 @@ export class SharingAPIService {
|
|
|
170
166
|
for (const link of response.Links) {
|
|
171
167
|
const nodeUid = makeNodeUid(link.VolumeID, link.LinkID);
|
|
172
168
|
|
|
173
|
-
if (!
|
|
174
|
-
this.logger.
|
|
169
|
+
if (!this.shareTargetTypes.includes(link.ShareTargetType)) {
|
|
170
|
+
this.logger.debug(`Unsupported share target type ${link.ShareTargetType} for node ${nodeUid}`);
|
|
175
171
|
continue;
|
|
176
172
|
}
|
|
177
173
|
|
|
@@ -188,14 +184,21 @@ export class SharingAPIService {
|
|
|
188
184
|
async *iterateInvitationUids(signal?: AbortSignal): AsyncGenerator<string> {
|
|
189
185
|
let anchor = '';
|
|
190
186
|
while (true) {
|
|
187
|
+
const params = new URLSearchParams();
|
|
188
|
+
this.shareTargetTypes.forEach((type) => {
|
|
189
|
+
params.append('ShareTargetTypes[]', type.toString());
|
|
190
|
+
});
|
|
191
|
+
if (anchor) {
|
|
192
|
+
params.append('AnchorID', anchor);
|
|
193
|
+
}
|
|
191
194
|
const response = await this.apiService.get<GetInvitationsResponse>(
|
|
192
|
-
`drive/v2/shares/invitations?${
|
|
195
|
+
`drive/v2/shares/invitations?${params.toString()}`,
|
|
193
196
|
signal,
|
|
194
197
|
);
|
|
195
198
|
for (const invitation of response.Invitations) {
|
|
196
199
|
const invitationUid = makeInvitationUid(invitation.ShareID, invitation.InvitationID);
|
|
197
200
|
|
|
198
|
-
if (!
|
|
201
|
+
if (!this.shareTargetTypes.includes(invitation.ShareTargetType)) {
|
|
199
202
|
this.logger.warn(
|
|
200
203
|
`Unsupported share target type ${invitation.ShareTargetType} for invitation ${invitationUid}`,
|
|
201
204
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ProtonDriveAccount, ProtonDriveEntitiesCache, ProtonDriveTelemetry } from '../../interface';
|
|
2
2
|
import { DriveCrypto } from '../../crypto';
|
|
3
3
|
import { DriveAPIService } from '../apiService';
|
|
4
|
+
import { ShareTargetType } from '../shares';
|
|
4
5
|
import { SharingAPIService } from './apiService';
|
|
5
6
|
import { SharingCache } from './cache';
|
|
6
7
|
import { SharingCryptoService } from './cryptoService';
|
|
@@ -9,6 +10,10 @@ import { SharingManagement } from './sharingManagement';
|
|
|
9
10
|
import { SharesService, NodesService } from './interface';
|
|
10
11
|
import { SharingEventHandler } from './events';
|
|
11
12
|
|
|
13
|
+
// Root shares are not allowed to be shared.
|
|
14
|
+
// Photos and Albums are not supported in main volume (core Drive).
|
|
15
|
+
const DEFAULT_SHARE_TARGET_TYPES = [ShareTargetType.Folder, ShareTargetType.File, ShareTargetType.ProtonVendor];
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* Provides facade for the whole sharing module.
|
|
14
19
|
*
|
|
@@ -24,8 +29,9 @@ export function initSharingModule(
|
|
|
24
29
|
crypto: DriveCrypto,
|
|
25
30
|
sharesService: SharesService,
|
|
26
31
|
nodesService: NodesService,
|
|
32
|
+
shareTargetTypes: ShareTargetType[] = DEFAULT_SHARE_TARGET_TYPES,
|
|
27
33
|
) {
|
|
28
|
-
const api = new SharingAPIService(telemetry.getLogger('sharing-api'), apiService);
|
|
34
|
+
const api = new SharingAPIService(telemetry.getLogger('sharing-api'), apiService, shareTargetTypes);
|
|
29
35
|
const cache = new SharingCache(driveEntitiesCache);
|
|
30
36
|
const cryptoService = new SharingCryptoService(telemetry, crypto, account, sharesService);
|
|
31
37
|
const sharingAccess = new SharingAccess(api, cache, cryptoService, sharesService, nodesService);
|
|
@@ -142,7 +142,7 @@ export interface PublicLinkWithCreatorEmail extends PublicLink {
|
|
|
142
142
|
* Interface describing the dependencies to the shares module.
|
|
143
143
|
*/
|
|
144
144
|
export interface SharesService {
|
|
145
|
-
|
|
145
|
+
getRootIDs(): Promise<{ volumeId: string }>;
|
|
146
146
|
loadEncryptedShare(shareId: string): Promise<EncryptedShare>;
|
|
147
147
|
getMyFilesShareMemberEmailKey(): Promise<{
|
|
148
148
|
email: string;
|
|
@@ -93,7 +93,7 @@ describe('SharingAccess', () => {
|
|
|
93
93
|
|
|
94
94
|
// @ts-expect-error No need to implement all methods for mocking
|
|
95
95
|
sharesService = {
|
|
96
|
-
|
|
96
|
+
getRootIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
|
|
97
97
|
loadEncryptedShare: jest.fn().mockResolvedValue({
|
|
98
98
|
id: 'shareId',
|
|
99
99
|
membership: { memberUid: 'memberUid' },
|
|
@@ -40,7 +40,7 @@ export class SharingAccess {
|
|
|
40
40
|
const nodeUids = await this.cache.getSharedByMeNodeUids();
|
|
41
41
|
yield* this.iterateSharedNodesFromCache(nodeUids, signal);
|
|
42
42
|
} catch {
|
|
43
|
-
const { volumeId } = await this.sharesService.
|
|
43
|
+
const { volumeId } = await this.sharesService.getRootIDs();
|
|
44
44
|
const nodeUidsIterator = this.apiService.iterateSharedNodeUids(volumeId, signal);
|
|
45
45
|
yield* this.iterateSharedNodesFromAPI(
|
|
46
46
|
nodeUidsIterator,
|