@protontech/drive-sdk 0.9.8 → 0.10.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 +15 -15
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/hmac.d.ts +3 -3
- package/dist/crypto/hmac.js.map +1 -1
- package/dist/crypto/interface.d.ts +45 -25
- package/dist/crypto/interface.js.map +1 -1
- package/dist/crypto/openPGPCrypto.d.ts +37 -37
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/crypto/utils.d.ts +1 -1
- package/dist/interface/index.d.ts +3 -3
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +8 -0
- package/dist/interface/photos.d.ts +18 -1
- package/dist/interface/sharing.d.ts +2 -0
- package/dist/interface/telemetry.d.ts +1 -0
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/interface/thumbnail.d.ts +2 -2
- package/dist/internal/apiService/apiService.js +25 -12
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +33 -5
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +2942 -3187
- package/dist/internal/apiService/errors.test.js +17 -7
- package/dist/internal/apiService/errors.test.js.map +1 -1
- package/dist/internal/devices/manager.d.ts +1 -0
- package/dist/internal/devices/manager.js +11 -0
- package/dist/internal/devices/manager.js.map +1 -1
- package/dist/internal/download/apiService.d.ts +1 -1
- package/dist/internal/download/cryptoService.d.ts +4 -4
- package/dist/internal/download/cryptoService.js.map +1 -1
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/download/thumbnailDownloader.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +4 -4
- package/dist/internal/nodes/cryptoService.js +5 -3
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +1 -1
- package/dist/internal/nodes/nodesManagement.js +0 -1
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/addToAlbum.d.ts +46 -0
- package/dist/internal/photos/addToAlbum.js +257 -0
- package/dist/internal/photos/addToAlbum.js.map +1 -0
- package/dist/internal/photos/addToAlbum.test.d.ts +1 -0
- package/dist/internal/photos/addToAlbum.test.js +409 -0
- package/dist/internal/photos/addToAlbum.test.js.map +1 -0
- package/dist/internal/photos/albums.d.ts +7 -2
- package/dist/internal/photos/albums.js +24 -1
- package/dist/internal/photos/albums.js.map +1 -1
- package/dist/internal/photos/albums.test.js +26 -1
- package/dist/internal/photos/albums.test.js.map +1 -1
- package/dist/internal/photos/albumsCrypto.d.ts +20 -3
- package/dist/internal/photos/albumsCrypto.js +27 -0
- package/dist/internal/photos/albumsCrypto.js.map +1 -1
- package/dist/internal/photos/apiService.d.ts +20 -0
- package/dist/internal/photos/apiService.js +142 -0
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/apiService.test.d.ts +1 -0
- package/dist/internal/photos/apiService.test.js +199 -0
- package/dist/internal/photos/apiService.test.js.map +1 -0
- package/dist/internal/photos/errors.d.ts +4 -0
- package/dist/internal/photos/errors.js +17 -0
- package/dist/internal/photos/errors.js.map +1 -0
- package/dist/internal/photos/index.d.ts +1 -1
- package/dist/internal/photos/index.js +1 -1
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +36 -1
- package/dist/internal/photos/interface.js +14 -0
- package/dist/internal/photos/interface.js.map +1 -1
- package/dist/internal/photos/nodes.js +32 -2
- package/dist/internal/photos/nodes.js.map +1 -1
- package/dist/internal/photos/nodes.test.js +25 -5
- package/dist/internal/photos/nodes.test.js.map +1 -1
- package/dist/internal/photos/timeline.d.ts +2 -5
- package/dist/internal/photos/timeline.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +2 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/shares/apiService.js +1 -0
- package/dist/internal/shares/apiService.js.map +1 -1
- package/dist/internal/shares/interface.d.ts +1 -0
- package/dist/internal/sharing/apiService.d.ts +8 -1
- package/dist/internal/sharing/apiService.js +23 -1
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/cryptoService.js +8 -4
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.d.ts +1 -0
- package/dist/internal/sharing/sharingManagement.js +15 -2
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.test.js +30 -5
- package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +2 -2
- package/dist/internal/sharingPublic/nodes.js.map +1 -1
- package/dist/internal/upload/apiService.d.ts +5 -5
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/blockVerifier.d.ts +2 -2
- package/dist/internal/upload/blockVerifier.js.map +1 -1
- package/dist/internal/upload/chunkStreamReader.d.ts +2 -2
- package/dist/internal/upload/chunkStreamReader.js.map +1 -1
- package/dist/internal/upload/chunkStreamReader.test.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +7 -7
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +6 -6
- package/dist/internal/upload/manager.d.ts +1 -1
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +1 -1
- package/dist/internal/utils.d.ts +1 -1
- package/dist/protonDriveClient.d.ts +8 -0
- package/dist/protonDriveClient.js +11 -0
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +42 -7
- package/dist/protonDrivePhotosClient.js +50 -2
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +9 -0
- package/dist/protonDrivePublicLinkClient.js +12 -0
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.js +2 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +4 -4
- package/src/crypto/driveCrypto.ts +15 -15
- package/src/crypto/hmac.ts +4 -4
- package/src/crypto/interface.ts +58 -27
- package/src/crypto/openPGPCrypto.ts +26 -26
- package/src/interface/index.ts +10 -2
- package/src/interface/nodes.ts +1 -0
- package/src/interface/photos.ts +19 -1
- package/src/interface/sharing.ts +2 -0
- package/src/interface/telemetry.ts +1 -0
- package/src/interface/thumbnail.ts +2 -2
- package/src/internal/apiService/apiService.test.ts +38 -6
- package/src/internal/apiService/apiService.ts +33 -12
- package/src/internal/apiService/driveTypes.ts +2942 -3187
- package/src/internal/devices/manager.ts +14 -0
- package/src/internal/download/apiService.ts +1 -1
- package/src/internal/download/cryptoService.ts +4 -4
- package/src/internal/download/fileDownloader.test.ts +4 -4
- package/src/internal/download/fileDownloader.ts +6 -6
- package/src/internal/download/thumbnailDownloader.ts +4 -4
- package/src/internal/nodes/cryptoService.test.ts +2 -2
- package/src/internal/nodes/cryptoService.ts +11 -8
- package/src/internal/nodes/interface.ts +1 -1
- package/src/internal/nodes/nodesManagement.ts +0 -1
- package/src/internal/photos/addToAlbum.test.ts +515 -0
- package/src/internal/photos/addToAlbum.ts +341 -0
- package/src/internal/photos/albums.test.ts +46 -22
- package/src/internal/photos/albums.ts +48 -2
- package/src/internal/photos/albumsCrypto.ts +54 -3
- package/src/internal/photos/apiService.test.ts +233 -0
- package/src/internal/photos/apiService.ts +234 -15
- package/src/internal/photos/errors.ts +11 -0
- package/src/internal/photos/index.ts +2 -2
- package/src/internal/photos/interface.ts +40 -1
- package/src/internal/photos/nodes.test.ts +27 -6
- package/src/internal/photos/nodes.ts +34 -2
- package/src/internal/photos/timeline.ts +2 -5
- package/src/internal/photos/upload.ts +2 -2
- package/src/internal/shares/apiService.ts +1 -0
- package/src/internal/shares/interface.ts +1 -0
- package/src/internal/sharing/apiService.ts +49 -5
- package/src/internal/sharing/cryptoService.ts +10 -4
- package/src/internal/sharing/sharingManagement.test.ts +33 -5
- package/src/internal/sharing/sharingManagement.ts +28 -6
- package/src/internal/sharingPublic/nodes.ts +1 -1
- package/src/internal/upload/apiService.ts +5 -5
- package/src/internal/upload/blockVerifier.ts +3 -3
- package/src/internal/upload/chunkStreamReader.test.ts +7 -7
- package/src/internal/upload/chunkStreamReader.ts +3 -3
- package/src/internal/upload/cryptoService.ts +9 -9
- package/src/internal/upload/interface.ts +6 -6
- package/src/internal/upload/manager.ts +2 -2
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/protonDriveClient.ts +15 -3
- package/src/protonDrivePhotosClient.ts +78 -22
- package/src/protonDrivePublicLinkClient.ts +13 -0
- package/src/transformers.ts +2 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { DriveAPIService } from '../apiService/apiService';
|
|
2
|
+
import { APICodeError, InvalidRequirementsAPIError } from '../apiService/errors';
|
|
3
|
+
import { PhotosAPIService } from './apiService';
|
|
4
|
+
import { MissingRelatedPhotosError } from './errors';
|
|
5
|
+
|
|
6
|
+
describe('photosAPIService', () => {
|
|
7
|
+
let apiMock: DriveAPIService;
|
|
8
|
+
let api: PhotosAPIService;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
|
|
13
|
+
// @ts-expect-error Mocking for testing purposes
|
|
14
|
+
apiMock = {
|
|
15
|
+
get: jest.fn(),
|
|
16
|
+
post: jest.fn(),
|
|
17
|
+
put: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
api = new PhotosAPIService(apiMock);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const albumNodeUid = 'volumeId1~albumNodeId';
|
|
24
|
+
|
|
25
|
+
describe('addPhotosToAlbum', () => {
|
|
26
|
+
const photoPayloads = [
|
|
27
|
+
{
|
|
28
|
+
nodeUid: 'volumeId1~photoNodeId1',
|
|
29
|
+
contentHash: 'contentHash1',
|
|
30
|
+
nameHash: 'nameHash1',
|
|
31
|
+
encryptedName: 'encryptedName1',
|
|
32
|
+
nameSignatureEmail: 'nameSignatureEmail1',
|
|
33
|
+
nodePassphrase: 'nodePassphrase1',
|
|
34
|
+
nodePassphraseSignature: 'nodePassphraseSignature1',
|
|
35
|
+
signatureEmail: 'signatureEmail1',
|
|
36
|
+
relatedPhotos: [
|
|
37
|
+
{
|
|
38
|
+
nodeUid: 'volumeId1~photoNodeId2',
|
|
39
|
+
contentHash: 'contentHash2',
|
|
40
|
+
nameHash: 'nameHash2',
|
|
41
|
+
encryptedName: 'encryptedName2',
|
|
42
|
+
nameSignatureEmail: 'nameSignatureEmail2',
|
|
43
|
+
nodePassphrase: 'nodePassphrase2',
|
|
44
|
+
nodePassphraseSignature: 'nodePassphraseSignature2',
|
|
45
|
+
signatureEmail: 'signatureEmail2',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
it('should add photos to album', async () => {
|
|
52
|
+
apiMock.post = jest.fn().mockResolvedValue({
|
|
53
|
+
Code: 1000,
|
|
54
|
+
Responses: [
|
|
55
|
+
{
|
|
56
|
+
LinkID: 'photoNodeId1',
|
|
57
|
+
Response: {
|
|
58
|
+
Code: 1000,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = await Array.fromAsync(api.addPhotosToAlbum(albumNodeUid, photoPayloads));
|
|
65
|
+
|
|
66
|
+
expect(result).toEqual([
|
|
67
|
+
{
|
|
68
|
+
uid: 'volumeId1~photoNodeId1',
|
|
69
|
+
ok: true,
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
expect(apiMock.post).toHaveBeenCalledWith(
|
|
73
|
+
`drive/photos/volumes/volumeId1/albums/albumNodeId/add-multiple`,
|
|
74
|
+
{
|
|
75
|
+
AlbumData: [
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
LinkID: 'photoNodeId1',
|
|
78
|
+
Hash: 'nameHash1',
|
|
79
|
+
Name: 'encryptedName1',
|
|
80
|
+
NameSignatureEmail: 'nameSignatureEmail1',
|
|
81
|
+
}),
|
|
82
|
+
expect.objectContaining({
|
|
83
|
+
LinkID: 'photoNodeId2',
|
|
84
|
+
Hash: 'nameHash2',
|
|
85
|
+
Name: 'encryptedName2',
|
|
86
|
+
NameSignatureEmail: 'nameSignatureEmail2',
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
undefined,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return MissingRelatedPhotosError if related photos are missing', async () => {
|
|
95
|
+
apiMock.post = jest.fn().mockResolvedValue({
|
|
96
|
+
Code: 1000,
|
|
97
|
+
Responses: [
|
|
98
|
+
{
|
|
99
|
+
LinkID: 'photoNodeId1',
|
|
100
|
+
Response: {
|
|
101
|
+
Code: 2000,
|
|
102
|
+
Details: {
|
|
103
|
+
Missing: ['photoNodeId3'],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await Array.fromAsync(api.addPhotosToAlbum(albumNodeUid, photoPayloads));
|
|
111
|
+
|
|
112
|
+
expect(result).toEqual([
|
|
113
|
+
{
|
|
114
|
+
uid: 'volumeId1~photoNodeId1',
|
|
115
|
+
ok: false,
|
|
116
|
+
error: new MissingRelatedPhotosError([]),
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
expect((result[0] as any).error.missingNodeUids).toEqual(['volumeId1~photoNodeId3']);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return error for unknown error', async () => {
|
|
123
|
+
apiMock.post = jest.fn().mockResolvedValue({
|
|
124
|
+
Code: 1000,
|
|
125
|
+
Responses: [
|
|
126
|
+
{
|
|
127
|
+
LinkID: 'photoNodeId1',
|
|
128
|
+
Response: {
|
|
129
|
+
Code: 3000,
|
|
130
|
+
Error: 'Some error',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await Array.fromAsync(api.addPhotosToAlbum(albumNodeUid, photoPayloads));
|
|
137
|
+
|
|
138
|
+
expect(result).toEqual([
|
|
139
|
+
{
|
|
140
|
+
uid: 'volumeId1~photoNodeId1',
|
|
141
|
+
ok: false,
|
|
142
|
+
error: new APICodeError('Some error', 3000),
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('copyPhotoToAlbum', () => {
|
|
149
|
+
const photoPayloads = [
|
|
150
|
+
{
|
|
151
|
+
nodeUid: 'volumeId2~photoNodeId1',
|
|
152
|
+
contentHash: 'contentHash1',
|
|
153
|
+
nameHash: 'nameHash1',
|
|
154
|
+
encryptedName: 'encryptedName1',
|
|
155
|
+
nameSignatureEmail: 'nameSignatureEmail1',
|
|
156
|
+
nodePassphrase: 'nodePassphrase1',
|
|
157
|
+
nodePassphraseSignature: 'nodePassphraseSignature1',
|
|
158
|
+
signatureEmail: 'signatureEmail1',
|
|
159
|
+
relatedPhotos: [
|
|
160
|
+
{
|
|
161
|
+
nodeUid: 'volumeId2~photoNodeId2',
|
|
162
|
+
contentHash: 'contentHash2',
|
|
163
|
+
nameHash: 'nameHash2',
|
|
164
|
+
encryptedName: 'encryptedName2',
|
|
165
|
+
nameSignatureEmail: 'nameSignatureEmail2',
|
|
166
|
+
nodePassphrase: 'nodePassphrase2',
|
|
167
|
+
nodePassphraseSignature: 'nodePassphraseSignature2',
|
|
168
|
+
signatureEmail: 'signatureEmail2',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
it('should copy photo to album', async () => {
|
|
175
|
+
apiMock.post = jest.fn().mockResolvedValue({
|
|
176
|
+
Code: 1000,
|
|
177
|
+
LinkID: 'photoNodeId1',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await api.copyPhotoToAlbum(albumNodeUid, photoPayloads[0]);
|
|
181
|
+
|
|
182
|
+
expect(result).toEqual('volumeId1~photoNodeId1');
|
|
183
|
+
expect(apiMock.post).toHaveBeenCalledWith(
|
|
184
|
+
`drive/volumes/volumeId2/links/photoNodeId1/copy`,
|
|
185
|
+
expect.objectContaining({
|
|
186
|
+
TargetVolumeID: 'volumeId1',
|
|
187
|
+
TargetParentLinkID: 'albumNodeId',
|
|
188
|
+
Hash: 'nameHash1',
|
|
189
|
+
Name: 'encryptedName1',
|
|
190
|
+
Photos: {
|
|
191
|
+
ContentHash: 'contentHash1',
|
|
192
|
+
RelatedPhotos: expect.arrayContaining([
|
|
193
|
+
expect.objectContaining({
|
|
194
|
+
LinkID: 'photoNodeId2',
|
|
195
|
+
Hash: 'nameHash2',
|
|
196
|
+
Name: 'encryptedName2',
|
|
197
|
+
}),
|
|
198
|
+
]),
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
undefined,
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return MissingRelatedPhotosError if related photos are missing', async () => {
|
|
206
|
+
apiMock.post = jest.fn().mockRejectedValue(new InvalidRequirementsAPIError(
|
|
207
|
+
'Missing related photos',
|
|
208
|
+
2000,
|
|
209
|
+
{
|
|
210
|
+
Missing: ['photoNodeId3'],
|
|
211
|
+
},
|
|
212
|
+
));
|
|
213
|
+
|
|
214
|
+
const promise = api.copyPhotoToAlbum(albumNodeUid, photoPayloads[0]);
|
|
215
|
+
|
|
216
|
+
await expect(promise).rejects.toThrow(MissingRelatedPhotosError);
|
|
217
|
+
try {
|
|
218
|
+
await promise;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
expect((error as MissingRelatedPhotosError).missingNodeUids).toEqual(['volumeId2~photoNodeId3']);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should return error for unknown error', async () => {
|
|
225
|
+
const error = new APICodeError('Some error', 3000);
|
|
226
|
+
apiMock.post = jest.fn().mockRejectedValue(error);
|
|
227
|
+
|
|
228
|
+
const promise = api.copyPhotoToAlbum(albumNodeUid, photoPayloads[0]);
|
|
229
|
+
|
|
230
|
+
await expect(promise).rejects.toThrow(error);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { ValidationError } from '../../errors';
|
|
4
|
-
import {
|
|
4
|
+
import { NodeResultWithError } from '../../interface';
|
|
5
|
+
import { APICodeError, DriveAPIService, drivePaths, InvalidRequirementsAPIError, isCodeOk } from '../apiService';
|
|
6
|
+
import { batch } from '../batch';
|
|
5
7
|
import { EncryptedRootShare, EncryptedShareCrypto, ShareType } from '../shares/interface';
|
|
6
8
|
import { makeNodeUid, splitNodeUid } from '../uids';
|
|
9
|
+
import { MissingRelatedPhotosError } from './errors';
|
|
10
|
+
import { AddToAlbumEncryptedPhotoPayload, AlbumItem } from './interface';
|
|
7
11
|
|
|
8
12
|
type GetPhotoShareResponse =
|
|
9
13
|
drivePaths['/drive/v2/shares/photos']['get']['responses']['200']['content']['application/json'];
|
|
@@ -21,6 +25,9 @@ type GetTimelineResponse =
|
|
|
21
25
|
type GetAlbumsResponse =
|
|
22
26
|
drivePaths['/drive/photos/volumes/{volumeID}/albums']['get']['responses']['200']['content']['application/json'];
|
|
23
27
|
|
|
28
|
+
type GetAlbumChildrenResponse =
|
|
29
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/children']['get']['responses']['200']['content']['application/json'];
|
|
30
|
+
|
|
24
31
|
type PostCreateAlbumRequest = Extract<
|
|
25
32
|
drivePaths['/drive/photos/volumes/{volumeID}/albums']['post']['requestBody'],
|
|
26
33
|
{ content: object }
|
|
@@ -40,6 +47,27 @@ type PostPhotoDuplicateRequest = Extract<
|
|
|
40
47
|
type PostPhotoDuplicateResponse =
|
|
41
48
|
drivePaths['/drive/volumes/{volumeID}/photos/duplicates']['post']['responses']['200']['content']['application/json'];
|
|
42
49
|
|
|
50
|
+
type PostAddPhotosToAlbumRequest = Extract<
|
|
51
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/add-multiple']['post']['requestBody'],
|
|
52
|
+
{ content: object }
|
|
53
|
+
>['content']['application/json'];
|
|
54
|
+
type PostAddPhotosToAlbumResponse =
|
|
55
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/add-multiple']['post']['responses']['200']['content']['application/json'];
|
|
56
|
+
|
|
57
|
+
type PostCopyLinkRequest = Extract<
|
|
58
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['requestBody'],
|
|
59
|
+
{ content: object }
|
|
60
|
+
>['content']['application/json'];
|
|
61
|
+
type PostCopyLinkResponse =
|
|
62
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['responses']['200']['content']['application/json'];
|
|
63
|
+
|
|
64
|
+
type PostRemovePhotosFromAlbumRequest = Extract<
|
|
65
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/remove-multiple']['post']['requestBody'],
|
|
66
|
+
{ content: object }
|
|
67
|
+
>['content']['application/json'];
|
|
68
|
+
type PostRemovePhotosFromAlbumResponse =
|
|
69
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/remove-multiple']['post']['responses']['200']['content']['application/json'];
|
|
70
|
+
|
|
43
71
|
const ALBUM_CONTAINS_PHOTOS_NOT_IN_TIMELINE_ERROR_CODE = 200302;
|
|
44
72
|
|
|
45
73
|
/**
|
|
@@ -173,6 +201,28 @@ export class PhotosAPIService {
|
|
|
173
201
|
}
|
|
174
202
|
}
|
|
175
203
|
|
|
204
|
+
async *iterateAlbumChildren(albumNodeUid: string, signal?: AbortSignal): AsyncGenerator<AlbumItem> {
|
|
205
|
+
const { volumeId, nodeId: linkId } = splitNodeUid(albumNodeUid);
|
|
206
|
+
let anchor = '';
|
|
207
|
+
while (true) {
|
|
208
|
+
const response = await this.apiService.get<GetAlbumChildrenResponse>(
|
|
209
|
+
`drive/photos/volumes/${volumeId}/albums/${linkId}/children?Sort=Captured&Desc=1${anchor ? `&AnchorID=${anchor}` : ''}`,
|
|
210
|
+
signal,
|
|
211
|
+
);
|
|
212
|
+
for (const photo of response.Photos) {
|
|
213
|
+
yield {
|
|
214
|
+
nodeUid: makeNodeUid(volumeId, photo.LinkID),
|
|
215
|
+
captureTime: new Date(photo.CaptureTime * 1000),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!response.More || !response.AnchorID) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
anchor = response.AnchorID;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
176
226
|
async checkPhotoDuplicates(
|
|
177
227
|
volumeId: string,
|
|
178
228
|
nameHashes: string[],
|
|
@@ -251,20 +301,17 @@ export class PhotosAPIService {
|
|
|
251
301
|
): Promise<void> {
|
|
252
302
|
const { volumeId, nodeId: linkId } = splitNodeUid(albumNodeUid);
|
|
253
303
|
const coverLinkId = coverPhotoNodeUid ? splitNodeUid(coverPhotoNodeUid).nodeId : undefined;
|
|
254
|
-
await this.apiService.put<PutUpdateAlbumRequest, void>(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
: null,
|
|
266
|
-
},
|
|
267
|
-
);
|
|
304
|
+
await this.apiService.put<PutUpdateAlbumRequest, void>(`drive/photos/volumes/${volumeId}/albums/${linkId}`, {
|
|
305
|
+
CoverLinkID: coverLinkId,
|
|
306
|
+
Link: updatedName
|
|
307
|
+
? {
|
|
308
|
+
Name: updatedName.encryptedName,
|
|
309
|
+
Hash: updatedName.hash,
|
|
310
|
+
OriginalHash: updatedName.originalHash,
|
|
311
|
+
NameSignatureEmail: updatedName.nameSignatureEmail,
|
|
312
|
+
}
|
|
313
|
+
: null,
|
|
314
|
+
});
|
|
268
315
|
}
|
|
269
316
|
|
|
270
317
|
async deleteAlbum(albumNodeUid: string, options: { force?: boolean } = {}): Promise<void> {
|
|
@@ -280,4 +327,176 @@ export class PhotosAPIService {
|
|
|
280
327
|
throw error;
|
|
281
328
|
}
|
|
282
329
|
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Add photos from the same volume to an album.
|
|
333
|
+
*
|
|
334
|
+
* To add photos from different volumes, use the {@link copyPhotoToAlbum} method.
|
|
335
|
+
*
|
|
336
|
+
* In the future, these two methods will be merged into a single one.
|
|
337
|
+
*/
|
|
338
|
+
async *addPhotosToAlbum(
|
|
339
|
+
albumNodeUid: string,
|
|
340
|
+
photoPayloads: AddToAlbumEncryptedPhotoPayload[],
|
|
341
|
+
signal?: AbortSignal,
|
|
342
|
+
): AsyncGenerator<NodeResultWithError> {
|
|
343
|
+
const { volumeId, nodeId: albumLinkId } = splitNodeUid(albumNodeUid);
|
|
344
|
+
|
|
345
|
+
const allPhotoPayloads = photoPayloads.flatMap((photoPayload) => [
|
|
346
|
+
photoPayload,
|
|
347
|
+
...(photoPayload.relatedPhotos || []),
|
|
348
|
+
]);
|
|
349
|
+
const allPhotoData = allPhotoPayloads.map((photoPayload) => {
|
|
350
|
+
const { nodeId } = splitNodeUid(photoPayload.nodeUid);
|
|
351
|
+
return {
|
|
352
|
+
LinkID: nodeId,
|
|
353
|
+
Hash: photoPayload.nameHash,
|
|
354
|
+
Name: photoPayload.encryptedName,
|
|
355
|
+
NameSignatureEmail: photoPayload.nameSignatureEmail,
|
|
356
|
+
NodePassphrase: photoPayload.nodePassphrase,
|
|
357
|
+
ContentHash: photoPayload.contentHash,
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const response = await this.apiService.post<PostAddPhotosToAlbumRequest, PostAddPhotosToAlbumResponse>(
|
|
362
|
+
`drive/photos/volumes/${volumeId}/albums/${albumLinkId}/add-multiple`,
|
|
363
|
+
{
|
|
364
|
+
AlbumData: allPhotoData,
|
|
365
|
+
},
|
|
366
|
+
signal,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const errors = new Map<string, Error>();
|
|
370
|
+
|
|
371
|
+
for (const r of response.Responses || []) {
|
|
372
|
+
// @ts-expect-error - API definition is not correct.
|
|
373
|
+
const details = r as {
|
|
374
|
+
LinkID: string;
|
|
375
|
+
Response: {
|
|
376
|
+
Code: number;
|
|
377
|
+
Error?: string;
|
|
378
|
+
Details: { Missing: string[] };
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
if (!details.Response.Code || !isCodeOk(details.Response.Code) || details.Response?.Error) {
|
|
383
|
+
const nodeUid = makeNodeUid(volumeId, details.LinkID);
|
|
384
|
+
|
|
385
|
+
if (details.Response.Details?.Missing) {
|
|
386
|
+
const missingNodeUids = details.Response.Details.Missing.map((linkId) =>
|
|
387
|
+
makeNodeUid(volumeId, linkId),
|
|
388
|
+
);
|
|
389
|
+
errors.set(nodeUid, new MissingRelatedPhotosError(missingNodeUids));
|
|
390
|
+
} else {
|
|
391
|
+
errors.set(
|
|
392
|
+
nodeUid,
|
|
393
|
+
new APICodeError(details.Response.Error || c('Error').t`Unknown error`, details.Response.Code),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const photoPayload of photoPayloads) {
|
|
400
|
+
const uid = photoPayload.nodeUid;
|
|
401
|
+
const error = errors.get(uid);
|
|
402
|
+
if (error) {
|
|
403
|
+
yield { uid, ok: false, error };
|
|
404
|
+
} else {
|
|
405
|
+
yield { uid, ok: true };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Copy a photo to a shared album on a different volume.
|
|
412
|
+
*
|
|
413
|
+
* To add photos from the same volume to an album, use the {@link addPhotosToAlbum} method.
|
|
414
|
+
*
|
|
415
|
+
* In the future, these two methods will be merged into a single one.
|
|
416
|
+
*/
|
|
417
|
+
async copyPhotoToAlbum(
|
|
418
|
+
albumNodeUid: string,
|
|
419
|
+
payload: AddToAlbumEncryptedPhotoPayload,
|
|
420
|
+
signal?: AbortSignal,
|
|
421
|
+
): Promise<string> {
|
|
422
|
+
const { volumeId: sourceVolumeId, nodeId: sourceLinkId } = splitNodeUid(payload.nodeUid);
|
|
423
|
+
const { volumeId: targetVolumeId, nodeId: targetAlbumLinkId } = splitNodeUid(albumNodeUid);
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const response = await this.apiService.post<PostCopyLinkRequest, PostCopyLinkResponse>(
|
|
427
|
+
`drive/volumes/${sourceVolumeId}/links/${sourceLinkId}/copy`,
|
|
428
|
+
{
|
|
429
|
+
TargetVolumeID: targetVolumeId,
|
|
430
|
+
TargetParentLinkID: targetAlbumLinkId,
|
|
431
|
+
Hash: payload.nameHash,
|
|
432
|
+
Name: payload.encryptedName,
|
|
433
|
+
NameSignatureEmail: payload.nameSignatureEmail,
|
|
434
|
+
NodePassphrase: payload.nodePassphrase,
|
|
435
|
+
// @ts-expect-error: API accepts NodePassphraseSignature as optional.
|
|
436
|
+
NodePassphraseSignature: payload.nodePassphraseSignature,
|
|
437
|
+
// @ts-expect-error: API accepts SignatureEmail as optional.
|
|
438
|
+
SignatureEmail: payload.signatureEmail,
|
|
439
|
+
Photos: {
|
|
440
|
+
ContentHash: payload.contentHash,
|
|
441
|
+
RelatedPhotos:
|
|
442
|
+
payload.relatedPhotos?.map((related) => ({
|
|
443
|
+
LinkID: splitNodeUid(related.nodeUid).nodeId,
|
|
444
|
+
Hash: related.nameHash,
|
|
445
|
+
Name: related.encryptedName,
|
|
446
|
+
NodePassphrase: related.nodePassphrase,
|
|
447
|
+
ContentHash: related.contentHash,
|
|
448
|
+
})) || [],
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
signal,
|
|
452
|
+
);
|
|
453
|
+
return makeNodeUid(targetVolumeId, response.LinkID);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (error instanceof InvalidRequirementsAPIError) {
|
|
456
|
+
const { Missing: missingLinkIds } = error.details as { Missing: string[] };
|
|
457
|
+
if (missingLinkIds.length > 0) {
|
|
458
|
+
throw new MissingRelatedPhotosError(
|
|
459
|
+
missingLinkIds.map((linkId) => makeNodeUid(sourceVolumeId, linkId)),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async *removePhotosFromAlbum(
|
|
468
|
+
albumNodeUid: string,
|
|
469
|
+
photoNodeUids: string[],
|
|
470
|
+
signal?: AbortSignal,
|
|
471
|
+
): AsyncGenerator<NodeResultWithError> {
|
|
472
|
+
const { volumeId, nodeId: albumLinkId } = splitNodeUid(albumNodeUid);
|
|
473
|
+
|
|
474
|
+
const batchSize = 50;
|
|
475
|
+
|
|
476
|
+
for (const photoNodeUidsBatch of batch(photoNodeUids, batchSize)) {
|
|
477
|
+
const linkIds = photoNodeUidsBatch.map((nodeUid) => splitNodeUid(nodeUid).nodeId);
|
|
478
|
+
|
|
479
|
+
let error: Error | undefined;
|
|
480
|
+
try {
|
|
481
|
+
await this.apiService.post<PostRemovePhotosFromAlbumRequest, PostRemovePhotosFromAlbumResponse>(
|
|
482
|
+
`drive/photos/volumes/${volumeId}/albums/${albumLinkId}/remove-multiple`,
|
|
483
|
+
{
|
|
484
|
+
LinkIDs: linkIds,
|
|
485
|
+
},
|
|
486
|
+
signal,
|
|
487
|
+
);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
error = e instanceof Error ? e : new Error(c('Error').t`Unknown error`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// The API does not return individual results for each photo.
|
|
493
|
+
for (const uid of photoNodeUidsBatch) {
|
|
494
|
+
if (error) {
|
|
495
|
+
yield { uid, ok: false, error };
|
|
496
|
+
} else {
|
|
497
|
+
yield { uid, ok: true };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
283
502
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { c } from 'ttag';
|
|
2
|
+
|
|
3
|
+
export class MissingRelatedPhotosError extends Error {
|
|
4
|
+
constructor(public missingNodeUids: string[]) {
|
|
5
|
+
// We do not want to leak the technical details of the error to the user.
|
|
6
|
+
// When this error happens, it is retried by the SDK, so very likely the
|
|
7
|
+
// user will not see this error unless the operation fails twice in a row.
|
|
8
|
+
super(c('Error').t`Operation failed, try again later`);
|
|
9
|
+
this.name = 'MissingRelatedPhotosError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
PhotoUploadMetadata,
|
|
34
34
|
} from './upload';
|
|
35
35
|
|
|
36
|
-
export type { DecryptedPhotoNode } from './interface';
|
|
36
|
+
export type { DecryptedPhotoNode, TimelineItem, AlbumItem, PhotoTag } from './interface';
|
|
37
37
|
|
|
38
38
|
// Only photos and albums can be shared in photos volume.
|
|
39
39
|
export const PHOTOS_SHARE_TARGET_TYPES = [ShareTargetType.Photo, ShareTargetType.Album];
|
|
@@ -60,7 +60,7 @@ export function initPhotosModule(
|
|
|
60
60
|
photoShares,
|
|
61
61
|
nodesService,
|
|
62
62
|
);
|
|
63
|
-
const albums = new Albums(api, albumsCryptoService, photoShares, nodesService);
|
|
63
|
+
const albums = new Albums(telemetry, api, albumsCryptoService, photoShares, nodesService);
|
|
64
64
|
|
|
65
65
|
return {
|
|
66
66
|
timeline,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PrivateKey } from '../../crypto';
|
|
2
|
-
import { MetricVolumeType, PhotoAttributes } from '../../interface';
|
|
2
|
+
import { MetricVolumeType, PhotoAttributes, AlbumAttributes } from '../../interface';
|
|
3
3
|
import { DecryptedNode, EncryptedNode, DecryptedUnparsedNode } from '../nodes/interface';
|
|
4
4
|
import { EncryptedShare } from '../shares';
|
|
5
5
|
|
|
@@ -25,14 +25,17 @@ export interface SharesService {
|
|
|
25
25
|
|
|
26
26
|
export type EncryptedPhotoNode = EncryptedNode & {
|
|
27
27
|
photo?: EcnryptedPhotoAttributes;
|
|
28
|
+
album?: AlbumAttributes;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export type DecryptedUnparsedPhotoNode = DecryptedUnparsedNode & {
|
|
31
32
|
photo?: PhotoAttributes;
|
|
33
|
+
album?: AlbumAttributes;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
export type DecryptedPhotoNode = DecryptedNode & {
|
|
35
37
|
photo?: PhotoAttributes;
|
|
38
|
+
album?: AlbumAttributes;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
export type EcnryptedPhotoAttributes = Omit<PhotoAttributes, 'albums'> & {
|
|
@@ -42,3 +45,39 @@ export type EcnryptedPhotoAttributes = Omit<PhotoAttributes, 'albums'> & {
|
|
|
42
45
|
contentHash?: string;
|
|
43
46
|
})[];
|
|
44
47
|
};
|
|
48
|
+
|
|
49
|
+
export type TimelineItem = {
|
|
50
|
+
nodeUid: string;
|
|
51
|
+
captureTime: Date;
|
|
52
|
+
tags: PhotoTag[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type AlbumItem = {
|
|
56
|
+
nodeUid: string;
|
|
57
|
+
captureTime: Date;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export enum PhotoTag {
|
|
61
|
+
Favorites = 0,
|
|
62
|
+
Screenshots = 1,
|
|
63
|
+
Videos = 2,
|
|
64
|
+
LivePhotos = 3,
|
|
65
|
+
MotionPhotos = 4,
|
|
66
|
+
Selfies = 5,
|
|
67
|
+
Portraits = 6,
|
|
68
|
+
Bursts = 7,
|
|
69
|
+
Panoramas = 8,
|
|
70
|
+
Raw = 9,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type AddToAlbumEncryptedPhotoPayload = {
|
|
74
|
+
nodeUid: string;
|
|
75
|
+
contentHash: string;
|
|
76
|
+
nameHash: string;
|
|
77
|
+
encryptedName: string;
|
|
78
|
+
nameSignatureEmail: string;
|
|
79
|
+
nodePassphrase: string;
|
|
80
|
+
nodePassphraseSignature?: string;
|
|
81
|
+
signatureEmail?: string;
|
|
82
|
+
relatedPhotos?: Omit<AddToAlbumEncryptedPhotoPayload, 'relatedPhotos'>[];
|
|
83
|
+
};
|