@protontech/drive-sdk 0.9.9 → 0.11.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 +19 -16
- package/dist/crypto/driveCrypto.js +23 -1
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/driveCrypto.test.js +2 -1
- package/dist/crypto/driveCrypto.test.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/diagnostic/telemetry.js +3 -0
- package/dist/diagnostic/telemetry.js.map +1 -1
- package/dist/interface/index.d.ts +4 -3
- package/dist/interface/index.js +3 -1
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +8 -0
- package/dist/interface/photos.d.ts +31 -2
- package/dist/interface/photos.js +14 -0
- package/dist/interface/photos.js.map +1 -1
- package/dist/interface/sharing.d.ts +2 -0
- package/dist/interface/telemetry.d.ts +13 -1
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/interface/thumbnail.d.ts +2 -2
- package/dist/internal/apiService/apiService.d.ts +1 -1
- package/dist/internal/apiService/apiService.js +27 -14
- 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/apiService.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/nodesAccess.js +1 -1
- package/dist/internal/nodes/nodesAccess.js.map +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 +42 -0
- package/dist/internal/photos/addToAlbum.js +178 -0
- package/dist/internal/photos/addToAlbum.js.map +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/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/{albums.d.ts → albumsManager.d.ts} +6 -4
- package/dist/internal/photos/{albums.js → albumsManager.js} +17 -5
- package/dist/internal/photos/albumsManager.js.map +1 -0
- package/dist/internal/photos/albumsManager.test.d.ts +1 -0
- package/dist/internal/photos/{albums.test.js → albumsManager.test.js} +4 -3
- package/dist/internal/photos/albumsManager.test.js.map +1 -0
- package/dist/internal/photos/apiService.d.ts +22 -2
- package/dist/internal/photos/apiService.js +136 -5
- 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 +5 -3
- package/dist/internal/photos/index.js +5 -2
- package/dist/internal/photos/index.js.map +1 -1
- package/dist/internal/photos/interface.d.ts +6 -15
- package/dist/internal/photos/interface.js +0 -14
- package/dist/internal/photos/interface.js.map +1 -1
- package/dist/internal/photos/nodes.js +33 -3
- 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/photosManager.d.ts +22 -0
- package/dist/internal/photos/photosManager.js +101 -0
- package/dist/internal/photos/photosManager.js.map +1 -0
- package/dist/internal/photos/photosManager.test.d.ts +1 -0
- package/dist/internal/photos/photosManager.test.js +222 -0
- package/dist/internal/photos/photosManager.test.js.map +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.d.ts +57 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.js +113 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.js.map +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.test.d.ts +1 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.test.js +289 -0
- package/dist/internal/photos/photosTransferPayloadBuilder.test.js.map +1 -0
- package/dist/internal/photos/upload.d.ts +2 -2
- package/dist/internal/photos/upload.js +1 -1
- 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 +3 -7
- package/dist/internal/upload/apiService.js +0 -4
- 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 +4 -4
- 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/upload/streamUploader.js +12 -13
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +28 -4
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/internal/utils.d.ts +1 -1
- package/dist/protonDriveClient.d.ts +8 -0
- package/dist/protonDriveClient.js +12 -1
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +35 -3
- package/dist/protonDrivePhotosClient.js +42 -2
- package/dist/protonDrivePhotosClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.js +1 -1
- 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.test.ts +2 -1
- package/src/crypto/driveCrypto.ts +50 -16
- package/src/crypto/hmac.ts +4 -4
- package/src/crypto/interface.ts +58 -27
- package/src/crypto/openPGPCrypto.ts +26 -26
- package/src/diagnostic/telemetry.ts +3 -0
- package/src/interface/index.ts +11 -2
- package/src/interface/nodes.ts +1 -0
- package/src/interface/photos.ts +33 -2
- package/src/interface/sharing.ts +2 -0
- package/src/interface/telemetry.ts +15 -1
- package/src/interface/thumbnail.ts +2 -2
- package/src/internal/apiService/apiService.test.ts +38 -6
- package/src/internal/apiService/apiService.ts +39 -14
- 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/apiService.ts +2 -2
- 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/nodesAccess.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 +234 -0
- package/src/internal/photos/albumsCrypto.ts +54 -3
- package/src/internal/photos/{albums.test.ts → albumsManager.test.ts} +22 -25
- package/src/internal/photos/{albums.ts → albumsManager.ts} +32 -3
- package/src/internal/photos/apiService.test.ts +233 -0
- package/src/internal/photos/apiService.ts +228 -26
- package/src/internal/photos/errors.ts +11 -0
- package/src/internal/photos/index.ts +6 -3
- package/src/internal/photos/interface.ts +8 -18
- package/src/internal/photos/nodes.test.ts +27 -6
- package/src/internal/photos/nodes.ts +35 -3
- package/src/internal/photos/photosManager.test.ts +266 -0
- package/src/internal/photos/photosManager.ts +144 -0
- package/src/internal/photos/photosTransferPayloadBuilder.test.ts +380 -0
- package/src/internal/photos/photosTransferPayloadBuilder.ts +203 -0
- package/src/internal/photos/upload.ts +8 -3
- 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 +10 -12
- 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 +11 -12
- package/src/internal/upload/interface.ts +6 -6
- package/src/internal/upload/manager.ts +2 -2
- package/src/internal/upload/streamUploader.test.ts +33 -4
- package/src/internal/upload/streamUploader.ts +13 -13
- package/src/protonDriveClient.ts +16 -4
- package/src/protonDrivePhotosClient.ts +73 -17
- package/src/protonDrivePublicLinkClient.ts +1 -1
- package/src/transformers.ts +2 -0
- package/dist/internal/photos/albums.js.map +0 -1
- package/dist/internal/photos/albums.test.js.map +0 -1
- /package/dist/internal/photos/{albums.test.d.ts → addToAlbum.test.d.ts} +0 -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,12 +1,14 @@
|
|
|
1
1
|
import { c } from 'ttag';
|
|
2
2
|
|
|
3
3
|
import { ValidationError } from '../../errors';
|
|
4
|
-
import {
|
|
5
|
-
import { APICodeError, DriveAPIService, drivePaths } from '../apiService';
|
|
4
|
+
import { NodeResultWithError, PhotoTag } from '../../interface';
|
|
5
|
+
import { APICodeError, DriveAPIService, drivePaths, InvalidRequirementsAPIError, isCodeOk } from '../apiService';
|
|
6
6
|
import { batch } from '../batch';
|
|
7
7
|
import { EncryptedRootShare, EncryptedShareCrypto, ShareType } from '../shares/interface';
|
|
8
8
|
import { makeNodeUid, splitNodeUid } from '../uids';
|
|
9
|
+
import { MissingRelatedPhotosError } from './errors';
|
|
9
10
|
import { AlbumItem } from './interface';
|
|
11
|
+
import { TransferEncryptedPhotoPayload } from './photosTransferPayloadBuilder';
|
|
10
12
|
|
|
11
13
|
type GetPhotoShareResponse =
|
|
12
14
|
drivePaths['/drive/v2/shares/photos']['get']['responses']['200']['content']['application/json'];
|
|
@@ -46,6 +48,20 @@ type PostPhotoDuplicateRequest = Extract<
|
|
|
46
48
|
type PostPhotoDuplicateResponse =
|
|
47
49
|
drivePaths['/drive/volumes/{volumeID}/photos/duplicates']['post']['responses']['200']['content']['application/json'];
|
|
48
50
|
|
|
51
|
+
type PostAddPhotosToAlbumRequest = Extract<
|
|
52
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/add-multiple']['post']['requestBody'],
|
|
53
|
+
{ content: object }
|
|
54
|
+
>['content']['application/json'];
|
|
55
|
+
type PostAddPhotosToAlbumResponse =
|
|
56
|
+
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/add-multiple']['post']['responses']['200']['content']['application/json'];
|
|
57
|
+
|
|
58
|
+
type PostCopyLinkRequest = Extract<
|
|
59
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['requestBody'],
|
|
60
|
+
{ content: object }
|
|
61
|
+
>['content']['application/json'];
|
|
62
|
+
type PostCopyLinkResponse =
|
|
63
|
+
drivePaths['/drive/volumes/{volumeID}/links/{linkID}/copy']['post']['responses']['200']['content']['application/json'];
|
|
64
|
+
|
|
49
65
|
type PostRemovePhotosFromAlbumRequest = Extract<
|
|
50
66
|
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/remove-multiple']['post']['requestBody'],
|
|
51
67
|
{ content: object }
|
|
@@ -53,6 +69,19 @@ type PostRemovePhotosFromAlbumRequest = Extract<
|
|
|
53
69
|
type PostRemovePhotosFromAlbumResponse =
|
|
54
70
|
drivePaths['/drive/photos/volumes/{volumeID}/albums/{linkID}/remove-multiple']['post']['responses']['200']['content']['application/json'];
|
|
55
71
|
|
|
72
|
+
type PostAddPhotoTagsRequest = Extract<
|
|
73
|
+
drivePaths['/drive/photos/volumes/{volumeID}/links/{linkID}/tags']['post']['requestBody'],
|
|
74
|
+
{ content: object }
|
|
75
|
+
>['content']['application/json'];
|
|
76
|
+
type PostRemovePhotoTagsRequest = Extract<
|
|
77
|
+
drivePaths['/drive/photos/volumes/{volumeID}/links/{linkID}/tags']['delete']['requestBody'],
|
|
78
|
+
{ content: object }
|
|
79
|
+
>['content']['application/json'];
|
|
80
|
+
type PostFavoritePhotoRequest = Extract<
|
|
81
|
+
drivePaths['/drive/photos/volumes/{volumeID}/links/{linkID}/favorite']['post']['requestBody'],
|
|
82
|
+
{ content: object }
|
|
83
|
+
>['content']['application/json'];
|
|
84
|
+
|
|
56
85
|
const ALBUM_CONTAINS_PHOTOS_NOT_IN_TIMELINE_ERROR_CODE = 200302;
|
|
57
86
|
|
|
58
87
|
/**
|
|
@@ -186,10 +215,7 @@ export class PhotosAPIService {
|
|
|
186
215
|
}
|
|
187
216
|
}
|
|
188
217
|
|
|
189
|
-
async *iterateAlbumChildren(
|
|
190
|
-
albumNodeUid: string,
|
|
191
|
-
signal?: AbortSignal,
|
|
192
|
-
): AsyncGenerator<AlbumItem> {
|
|
218
|
+
async *iterateAlbumChildren(albumNodeUid: string, signal?: AbortSignal): AsyncGenerator<AlbumItem> {
|
|
193
219
|
const { volumeId, nodeId: linkId } = splitNodeUid(albumNodeUid);
|
|
194
220
|
let anchor = '';
|
|
195
221
|
while (true) {
|
|
@@ -289,20 +315,17 @@ export class PhotosAPIService {
|
|
|
289
315
|
): Promise<void> {
|
|
290
316
|
const { volumeId, nodeId: linkId } = splitNodeUid(albumNodeUid);
|
|
291
317
|
const coverLinkId = coverPhotoNodeUid ? splitNodeUid(coverPhotoNodeUid).nodeId : undefined;
|
|
292
|
-
await this.apiService.put<PutUpdateAlbumRequest, void>(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
: null,
|
|
304
|
-
},
|
|
305
|
-
);
|
|
318
|
+
await this.apiService.put<PutUpdateAlbumRequest, void>(`drive/photos/volumes/${volumeId}/albums/${linkId}`, {
|
|
319
|
+
CoverLinkID: coverLinkId,
|
|
320
|
+
Link: updatedName
|
|
321
|
+
? {
|
|
322
|
+
Name: updatedName.encryptedName,
|
|
323
|
+
Hash: updatedName.hash,
|
|
324
|
+
OriginalHash: updatedName.originalHash,
|
|
325
|
+
NameSignatureEmail: updatedName.nameSignatureEmail,
|
|
326
|
+
}
|
|
327
|
+
: null,
|
|
328
|
+
});
|
|
306
329
|
}
|
|
307
330
|
|
|
308
331
|
async deleteAlbum(albumNodeUid: string, options: { force?: boolean } = {}): Promise<void> {
|
|
@@ -319,11 +342,143 @@ export class PhotosAPIService {
|
|
|
319
342
|
}
|
|
320
343
|
}
|
|
321
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Add photos from the same volume to an album.
|
|
347
|
+
*
|
|
348
|
+
* To add photos from different volumes, use the {@link copyPhotoToAlbum} method.
|
|
349
|
+
*
|
|
350
|
+
* In the future, these two methods will be merged into a single one.
|
|
351
|
+
*/
|
|
352
|
+
async *addPhotosToAlbum(
|
|
353
|
+
albumNodeUid: string,
|
|
354
|
+
photoPayloads: TransferEncryptedPhotoPayload[],
|
|
355
|
+
signal?: AbortSignal,
|
|
356
|
+
): AsyncGenerator<NodeResultWithError> {
|
|
357
|
+
const { volumeId, nodeId: albumLinkId } = splitNodeUid(albumNodeUid);
|
|
358
|
+
|
|
359
|
+
const allPhotoPayloads = photoPayloads.flatMap((photoPayload) => [photoPayload, ...photoPayload.relatedPhotos]);
|
|
360
|
+
const allPhotoData = allPhotoPayloads.map((photoPayload) => {
|
|
361
|
+
const { nodeId } = splitNodeUid(photoPayload.nodeUid);
|
|
362
|
+
return {
|
|
363
|
+
LinkID: nodeId,
|
|
364
|
+
Hash: photoPayload.nameHash,
|
|
365
|
+
Name: photoPayload.encryptedName,
|
|
366
|
+
NameSignatureEmail: photoPayload.nameSignatureEmail,
|
|
367
|
+
NodePassphrase: photoPayload.nodePassphrase,
|
|
368
|
+
ContentHash: photoPayload.contentHash,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const response = await this.apiService.post<PostAddPhotosToAlbumRequest, PostAddPhotosToAlbumResponse>(
|
|
373
|
+
`drive/photos/volumes/${volumeId}/albums/${albumLinkId}/add-multiple`,
|
|
374
|
+
{
|
|
375
|
+
AlbumData: allPhotoData,
|
|
376
|
+
},
|
|
377
|
+
signal,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const errors = new Map<string, Error>();
|
|
381
|
+
|
|
382
|
+
for (const r of response.Responses || []) {
|
|
383
|
+
// @ts-expect-error - API definition is not correct.
|
|
384
|
+
const details = r as {
|
|
385
|
+
LinkID: string;
|
|
386
|
+
Response: {
|
|
387
|
+
Code: number;
|
|
388
|
+
Error?: string;
|
|
389
|
+
Details: { Missing: string[] };
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (!details.Response.Code || !isCodeOk(details.Response.Code) || details.Response?.Error) {
|
|
394
|
+
const nodeUid = makeNodeUid(volumeId, details.LinkID);
|
|
395
|
+
|
|
396
|
+
if (details.Response.Details?.Missing) {
|
|
397
|
+
const missingNodeUids = details.Response.Details.Missing.map((linkId) =>
|
|
398
|
+
makeNodeUid(volumeId, linkId),
|
|
399
|
+
);
|
|
400
|
+
errors.set(nodeUid, new MissingRelatedPhotosError(missingNodeUids));
|
|
401
|
+
} else {
|
|
402
|
+
errors.set(
|
|
403
|
+
nodeUid,
|
|
404
|
+
new APICodeError(details.Response.Error || c('Error').t`Unknown error`, details.Response.Code),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for (const photoPayload of photoPayloads) {
|
|
411
|
+
const uid = photoPayload.nodeUid;
|
|
412
|
+
const error = errors.get(uid);
|
|
413
|
+
if (error) {
|
|
414
|
+
yield { uid, ok: false, error };
|
|
415
|
+
} else {
|
|
416
|
+
yield { uid, ok: true };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Copy a photo to a shared album on a different volume.
|
|
423
|
+
*
|
|
424
|
+
* To add photos from the same volume to an album, use the {@link addPhotosToAlbum} method.
|
|
425
|
+
*
|
|
426
|
+
* In the future, these two methods will be merged into a single one.
|
|
427
|
+
*/
|
|
428
|
+
async copyPhotoToAlbum(
|
|
429
|
+
albumNodeUid: string,
|
|
430
|
+
payload: TransferEncryptedPhotoPayload,
|
|
431
|
+
signal?: AbortSignal,
|
|
432
|
+
): Promise<string> {
|
|
433
|
+
const { volumeId: sourceVolumeId, nodeId: sourceLinkId } = splitNodeUid(payload.nodeUid);
|
|
434
|
+
const { volumeId: targetVolumeId, nodeId: targetAlbumLinkId } = splitNodeUid(albumNodeUid);
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const response = await this.apiService.post<PostCopyLinkRequest, PostCopyLinkResponse>(
|
|
438
|
+
`drive/volumes/${sourceVolumeId}/links/${sourceLinkId}/copy`,
|
|
439
|
+
{
|
|
440
|
+
TargetVolumeID: targetVolumeId,
|
|
441
|
+
TargetParentLinkID: targetAlbumLinkId,
|
|
442
|
+
Hash: payload.nameHash,
|
|
443
|
+
Name: payload.encryptedName,
|
|
444
|
+
NameSignatureEmail: payload.nameSignatureEmail,
|
|
445
|
+
NodePassphrase: payload.nodePassphrase,
|
|
446
|
+
// @ts-expect-error: API accepts NodePassphraseSignature as optional.
|
|
447
|
+
NodePassphraseSignature: payload.nodePassphraseSignature,
|
|
448
|
+
// @ts-expect-error: API accepts SignatureEmail as optional.
|
|
449
|
+
SignatureEmail: payload.signatureEmail,
|
|
450
|
+
Photos: {
|
|
451
|
+
ContentHash: payload.contentHash,
|
|
452
|
+
RelatedPhotos: payload.relatedPhotos.map((related) => ({
|
|
453
|
+
LinkID: splitNodeUid(related.nodeUid).nodeId,
|
|
454
|
+
Hash: related.nameHash,
|
|
455
|
+
Name: related.encryptedName,
|
|
456
|
+
NodePassphrase: related.nodePassphrase,
|
|
457
|
+
ContentHash: related.contentHash,
|
|
458
|
+
})),
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
signal,
|
|
462
|
+
);
|
|
463
|
+
return makeNodeUid(targetVolumeId, response.LinkID);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error instanceof InvalidRequirementsAPIError) {
|
|
466
|
+
const { Missing: missingLinkIds } = error.details as { Missing: string[] };
|
|
467
|
+
if (missingLinkIds.length > 0) {
|
|
468
|
+
throw new MissingRelatedPhotosError(
|
|
469
|
+
missingLinkIds.map((linkId) => makeNodeUid(sourceVolumeId, linkId)),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
322
477
|
async *removePhotosFromAlbum(
|
|
323
478
|
albumNodeUid: string,
|
|
324
479
|
photoNodeUids: string[],
|
|
325
480
|
signal?: AbortSignal,
|
|
326
|
-
): AsyncGenerator<
|
|
481
|
+
): AsyncGenerator<NodeResultWithError> {
|
|
327
482
|
const { volumeId, nodeId: albumLinkId } = splitNodeUid(albumNodeUid);
|
|
328
483
|
|
|
329
484
|
const batchSize = 50;
|
|
@@ -331,7 +486,7 @@ export class PhotosAPIService {
|
|
|
331
486
|
for (const photoNodeUidsBatch of batch(photoNodeUids, batchSize)) {
|
|
332
487
|
const linkIds = photoNodeUidsBatch.map((nodeUid) => splitNodeUid(nodeUid).nodeId);
|
|
333
488
|
|
|
334
|
-
let
|
|
489
|
+
let error: Error | undefined;
|
|
335
490
|
try {
|
|
336
491
|
await this.apiService.post<PostRemovePhotosFromAlbumRequest, PostRemovePhotosFromAlbumResponse>(
|
|
337
492
|
`drive/photos/volumes/${volumeId}/albums/${albumLinkId}/remove-multiple`,
|
|
@@ -340,18 +495,65 @@ export class PhotosAPIService {
|
|
|
340
495
|
},
|
|
341
496
|
signal,
|
|
342
497
|
);
|
|
343
|
-
} catch (
|
|
344
|
-
|
|
498
|
+
} catch (e) {
|
|
499
|
+
error = e instanceof Error ? e : new Error(c('Error').t`Unknown error`);
|
|
345
500
|
}
|
|
346
501
|
|
|
347
502
|
// The API does not return individual results for each photo.
|
|
348
503
|
for (const uid of photoNodeUidsBatch) {
|
|
349
|
-
if (
|
|
350
|
-
yield { uid, ok: false, error
|
|
504
|
+
if (error) {
|
|
505
|
+
yield { uid, ok: false, error };
|
|
351
506
|
} else {
|
|
352
507
|
yield { uid, ok: true };
|
|
353
508
|
}
|
|
354
509
|
}
|
|
355
510
|
}
|
|
356
511
|
}
|
|
512
|
+
|
|
513
|
+
async addPhotoTags(nodeUid: string, tags: PhotoTag[]): Promise<void> {
|
|
514
|
+
const { volumeId, nodeId: linkId } = splitNodeUid(nodeUid);
|
|
515
|
+
await this.apiService.post<PostAddPhotoTagsRequest, { Code: number }>(
|
|
516
|
+
`drive/photos/volumes/${volumeId}/links/${linkId}/tags`,
|
|
517
|
+
{ Tags: tags },
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async removePhotoTags(nodeUid: string, tags: PhotoTag[]): Promise<void> {
|
|
522
|
+
const { volumeId, nodeId: linkId } = splitNodeUid(nodeUid);
|
|
523
|
+
await this.apiService.delete<PostRemovePhotoTagsRequest, { Code: number }>(
|
|
524
|
+
`drive/photos/volumes/${volumeId}/links/${linkId}/tags`,
|
|
525
|
+
{ Tags: tags },
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async setPhotoFavorite(nodeUid: string, payload?: TransferEncryptedPhotoPayload): Promise<void> {
|
|
530
|
+
const { volumeId, nodeId: linkId } = splitNodeUid(nodeUid);
|
|
531
|
+
const requestBody = payload
|
|
532
|
+
? {
|
|
533
|
+
PhotoData: {
|
|
534
|
+
Hash: payload.nameHash,
|
|
535
|
+
Name: payload.encryptedName,
|
|
536
|
+
NameSignatureEmail: payload.nameSignatureEmail,
|
|
537
|
+
NodePassphrase: payload.nodePassphrase,
|
|
538
|
+
ContentHash: payload.contentHash,
|
|
539
|
+
NodePassphraseSignature: payload.nodePassphraseSignature ?? null,
|
|
540
|
+
SignatureEmail: payload.signatureEmail ?? null,
|
|
541
|
+
RelatedPhotos: payload.relatedPhotos.map((related) => ({
|
|
542
|
+
LinkID: splitNodeUid(related.nodeUid).nodeId,
|
|
543
|
+
Hash: related.nameHash,
|
|
544
|
+
Name: related.encryptedName,
|
|
545
|
+
NameSignatureEmail: related.nameSignatureEmail,
|
|
546
|
+
NodePassphrase: related.nodePassphrase,
|
|
547
|
+
ContentHash: related.contentHash,
|
|
548
|
+
NodePassphraseSignature: related.nodePassphraseSignature ?? null,
|
|
549
|
+
SignatureEmail: related.signatureEmail ?? null,
|
|
550
|
+
})),
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
: undefined;
|
|
554
|
+
await this.apiService.post<PostFavoritePhotoRequest, { Code: number }>(
|
|
555
|
+
`drive/photos/volumes/${volumeId}/links/${linkId}/favorite`,
|
|
556
|
+
requestBody,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
357
559
|
}
|
|
@@ -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
|
+
}
|
|
@@ -18,13 +18,14 @@ import { SharesCryptoService } from '../shares/cryptoService';
|
|
|
18
18
|
import { NodesService as UploadNodesService } from '../upload/interface';
|
|
19
19
|
import { UploadTelemetry } from '../upload/telemetry';
|
|
20
20
|
import { UploadQueue } from '../upload/queue';
|
|
21
|
-
import {
|
|
21
|
+
import { AlbumsManager } from './albumsManager';
|
|
22
22
|
import { AlbumsCryptoService } from './albumsCrypto';
|
|
23
23
|
import { PhotosAPIService } from './apiService';
|
|
24
24
|
import { SharesService } from './interface';
|
|
25
25
|
import { PhotosNodesAPIService, PhotosNodesAccess, PhotosNodesCache, PhotosNodesManagement } from './nodes';
|
|
26
26
|
import { PhotoSharesManager } from './shares';
|
|
27
27
|
import { PhotosTimeline } from './timeline';
|
|
28
|
+
import { PhotosManager } from './photosManager';
|
|
28
29
|
import {
|
|
29
30
|
PhotoFileUploader,
|
|
30
31
|
PhotoUploadAPIService,
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
PhotoUploadMetadata,
|
|
34
35
|
} from './upload';
|
|
35
36
|
|
|
36
|
-
export type { DecryptedPhotoNode, TimelineItem, AlbumItem
|
|
37
|
+
export type { DecryptedPhotoNode, TimelineItem, AlbumItem } from './interface';
|
|
37
38
|
|
|
38
39
|
// Only photos and albums can be shared in photos volume.
|
|
39
40
|
export const PHOTOS_SHARE_TARGET_TYPES = [ShareTargetType.Photo, ShareTargetType.Album];
|
|
@@ -60,11 +61,13 @@ export function initPhotosModule(
|
|
|
60
61
|
photoShares,
|
|
61
62
|
nodesService,
|
|
62
63
|
);
|
|
63
|
-
const albums = new
|
|
64
|
+
const albums = new AlbumsManager(telemetry, api, albumsCryptoService, photoShares, nodesService);
|
|
65
|
+
const photos = new PhotosManager(telemetry.getLogger('photos-update'), api, albumsCryptoService, nodesService);
|
|
64
66
|
|
|
65
67
|
return {
|
|
66
68
|
timeline,
|
|
67
69
|
albums,
|
|
70
|
+
photos,
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PrivateKey } from '../../crypto';
|
|
2
|
-
import { MetricVolumeType, PhotoAttributes } from '../../interface';
|
|
2
|
+
import { MetricVolumeType, PhotoAttributes, AlbumAttributes, PhotoTag } from '../../interface';
|
|
3
3
|
import { DecryptedNode, EncryptedNode, DecryptedUnparsedNode } from '../nodes/interface';
|
|
4
4
|
import { EncryptedShare } from '../shares';
|
|
5
5
|
|
|
@@ -24,18 +24,21 @@ export interface SharesService {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export type EncryptedPhotoNode = EncryptedNode & {
|
|
27
|
-
photo?:
|
|
27
|
+
photo?: EncryptedPhotoAttributes;
|
|
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
|
-
export type
|
|
41
|
+
export type EncryptedPhotoAttributes = Omit<PhotoAttributes, 'albums'> & {
|
|
39
42
|
contentHash?: string;
|
|
40
43
|
albums: (PhotoAttributes['albums'][0] & {
|
|
41
44
|
nameHash?: string;
|
|
@@ -47,22 +50,9 @@ export type TimelineItem = {
|
|
|
47
50
|
nodeUid: string;
|
|
48
51
|
captureTime: Date;
|
|
49
52
|
tags: PhotoTag[];
|
|
50
|
-
}
|
|
53
|
+
};
|
|
51
54
|
|
|
52
55
|
export type AlbumItem = {
|
|
53
56
|
nodeUid: string;
|
|
54
57
|
captureTime: Date;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export enum PhotoTag {
|
|
58
|
-
Favorites = 0,
|
|
59
|
-
Screenshots = 1,
|
|
60
|
-
Videos = 2,
|
|
61
|
-
LivePhotos = 3,
|
|
62
|
-
MotionPhotos = 4,
|
|
63
|
-
Selfies = 5,
|
|
64
|
-
Portraits = 6,
|
|
65
|
-
Bursts = 7,
|
|
66
|
-
Panoramas = 8,
|
|
67
|
-
Raw = 9,
|
|
68
|
-
}
|
|
58
|
+
};
|