@protontech/drive-sdk 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto/driveCrypto.d.ts +1 -0
- package/dist/crypto/driveCrypto.js +1 -0
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/interface/featureFlags.d.ts +2 -1
- package/dist/interface/featureFlags.js +1 -0
- package/dist/interface/featureFlags.js.map +1 -1
- package/dist/interface/httpClient.d.ts +1 -0
- package/dist/interface/nodes.d.ts +7 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/internal/apiService/apiService.d.ts +1 -0
- package/dist/internal/apiService/apiService.js +16 -7
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +24 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +110 -101
- package/dist/internal/nodes/apiService.d.ts +4 -0
- package/dist/internal/nodes/apiService.js +4 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +8 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.js +3 -0
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.d.ts +5 -5
- package/dist/internal/nodes/extendedAttributes.js +5 -14
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +16 -22
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +5 -0
- package/dist/internal/nodes/nodesManagement.d.ts +3 -3
- package/dist/internal/nodes/nodesManagement.js +7 -5
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/albumsManager.js +1 -0
- package/dist/internal/photos/albumsManager.js.map +1 -1
- package/dist/internal/photos/nodes.d.ts +1 -1
- package/dist/internal/photos/nodes.js +2 -2
- package/dist/internal/photos/nodes.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +5 -5
- package/dist/internal/photos/upload.js +8 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +1 -0
- package/dist/internal/upload/apiService.d.ts +45 -1
- package/dist/internal/upload/apiService.js +69 -1
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/blockVerifier.d.ts +4 -1
- package/dist/internal/upload/blockVerifier.js +5 -0
- package/dist/internal/upload/blockVerifier.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +2 -2
- package/dist/internal/upload/cryptoService.js +1 -3
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +4 -3
- package/dist/internal/upload/fileUploader.js +17 -7
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/index.d.ts +3 -3
- package/dist/internal/upload/index.js +17 -1
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/index.test.d.ts +1 -0
- package/dist/internal/upload/index.test.js +71 -0
- package/dist/internal/upload/index.test.js.map +1 -0
- package/dist/internal/upload/interface.d.ts +2 -0
- package/dist/internal/upload/manager.d.ts +41 -2
- package/dist/internal/upload/manager.js +123 -42
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +267 -0
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/smallFileUploader.d.ts +83 -0
- package/dist/internal/upload/smallFileUploader.js +197 -0
- package/dist/internal/upload/smallFileUploader.js.map +1 -0
- package/dist/internal/upload/smallFileUploader.test.d.ts +1 -0
- package/dist/internal/upload/smallFileUploader.test.js +358 -0
- package/dist/internal/upload/smallFileUploader.test.js.map +1 -0
- package/dist/internal/upload/streamReader.d.ts +4 -0
- package/dist/internal/upload/streamReader.js +37 -0
- package/dist/internal/upload/streamReader.js.map +1 -0
- package/dist/internal/upload/streamReader.test.d.ts +1 -0
- package/dist/internal/upload/streamReader.test.js +90 -0
- package/dist/internal/upload/streamReader.test.js.map +1 -0
- package/dist/internal/upload/streamUploader.d.ts +6 -0
- package/dist/internal/upload/streamUploader.js +3 -3
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/telemetry.d.ts +3 -2
- package/dist/internal/upload/telemetry.js +3 -0
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +1 -1
- package/dist/protonDrivePublicLinkClient.js +3 -1
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +1 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +2 -0
- package/src/interface/featureFlags.ts +1 -0
- package/src/interface/httpClient.ts +1 -0
- package/src/interface/nodes.ts +7 -0
- package/src/internal/apiService/apiService.test.ts +30 -0
- package/src/internal/apiService/apiService.ts +23 -7
- package/src/internal/apiService/driveTypes.ts +110 -101
- package/src/internal/nodes/apiService.test.ts +9 -0
- package/src/internal/nodes/apiService.ts +4 -0
- package/src/internal/nodes/cryptoService.ts +11 -1
- package/src/internal/nodes/extendedAttributes.test.ts +25 -25
- package/src/internal/nodes/extendedAttributes.ts +10 -19
- package/src/internal/nodes/interface.ts +5 -0
- package/src/internal/nodes/nodesManagement.ts +8 -6
- package/src/internal/photos/albumsManager.ts +1 -0
- package/src/internal/photos/nodes.ts +2 -2
- package/src/internal/photos/upload.ts +23 -10
- package/src/internal/upload/apiService.ts +167 -2
- package/src/internal/upload/blockVerifier.ts +12 -0
- package/src/internal/upload/cryptoService.ts +10 -10
- package/src/internal/upload/fileUploader.ts +20 -7
- package/src/internal/upload/index.test.ts +99 -0
- package/src/internal/upload/index.ts +45 -4
- package/src/internal/upload/interface.ts +2 -0
- package/src/internal/upload/manager.test.ts +367 -1
- package/src/internal/upload/manager.ts +226 -76
- package/src/internal/upload/smallFileUploader.test.ts +491 -0
- package/src/internal/upload/smallFileUploader.ts +353 -0
- package/src/internal/upload/streamReader.test.ts +109 -0
- package/src/internal/upload/streamReader.ts +38 -0
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/internal/upload/telemetry.ts +5 -1
- package/src/protonDrivePhotosClient.ts +1 -1
- package/src/protonDrivePublicLinkClient.ts +2 -0
- package/src/transformers.ts +2 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { DriveCrypto } from '../../crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ProtonDriveTelemetry,
|
|
4
|
+
UploadMetadata,
|
|
5
|
+
Thumbnail,
|
|
6
|
+
AnonymousUser,
|
|
7
|
+
FeatureFlagProvider,
|
|
8
|
+
PhotoTag,
|
|
9
|
+
} from '../../interface';
|
|
3
10
|
import { DriveAPIService, drivePaths } from '../apiService';
|
|
4
11
|
import { generateFileExtendedAttributes } from '../nodes';
|
|
5
|
-
import { splitNodeRevisionUid } from '../uids';
|
|
12
|
+
import { splitNodeRevisionUid, splitNodeUid } from '../uids';
|
|
6
13
|
import { UploadAPIService } from '../upload/apiService';
|
|
7
14
|
import { BlockVerifier } from '../upload/blockVerifier';
|
|
8
15
|
import { UploadController } from '../upload/controller';
|
|
@@ -22,9 +29,8 @@ type PostCommitRevisionResponse =
|
|
|
22
29
|
|
|
23
30
|
export type PhotoUploadMetadata = UploadMetadata & {
|
|
24
31
|
captureTime?: Date;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
|
|
32
|
+
mainPhotoNodeUid?: string;
|
|
33
|
+
tags?: PhotoTag[];
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
export class PhotoFileUploader extends FileUploader {
|
|
@@ -180,7 +186,7 @@ export class PhotoUploadManager extends UploadManager {
|
|
|
180
186
|
const photo = {
|
|
181
187
|
contentHash,
|
|
182
188
|
captureTime: uploadMetadata.captureTime || extendedAttributes.modificationTime,
|
|
183
|
-
|
|
189
|
+
mainPhotoNodeUid: uploadMetadata.mainPhotoNodeUid,
|
|
184
190
|
tags: uploadMetadata.tags,
|
|
185
191
|
};
|
|
186
192
|
await this.photoApiService.commitDraftPhoto(nodeRevisionDraft.nodeRevisionUid, nodeCommitCrypto, photo);
|
|
@@ -218,12 +224,19 @@ export class PhotoUploadAPIService extends UploadAPIService {
|
|
|
218
224
|
photo: {
|
|
219
225
|
contentHash: string;
|
|
220
226
|
captureTime?: Date;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
|
|
227
|
+
mainPhotoNodeUid?: string;
|
|
228
|
+
tags?: PhotoTag[];
|
|
224
229
|
},
|
|
225
230
|
): Promise<void> {
|
|
226
231
|
const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
|
|
232
|
+
const { volumeId: mainPhotoVolumeId, nodeId: mainPhotoNodeId } = photo.mainPhotoNodeUid
|
|
233
|
+
? splitNodeUid(photo.mainPhotoNodeUid)
|
|
234
|
+
: { volumeId: null, nodeId: null };
|
|
235
|
+
|
|
236
|
+
if (mainPhotoVolumeId !== null && mainPhotoVolumeId !== volumeId) {
|
|
237
|
+
throw new Error('mainPhotoNodeUid must belong to the same volume as the draft');
|
|
238
|
+
}
|
|
239
|
+
|
|
227
240
|
await this.apiService.put<
|
|
228
241
|
// TODO: Deprected fields but not properly marked in the types.
|
|
229
242
|
Omit<PostCommitRevisionRequest, 'BlockNumber' | 'BlockList' | 'ThumbnailToken' | 'State'>,
|
|
@@ -235,7 +248,7 @@ export class PhotoUploadAPIService extends UploadAPIService {
|
|
|
235
248
|
Photo: {
|
|
236
249
|
ContentHash: photo.contentHash,
|
|
237
250
|
CaptureTime: photo.captureTime ? Math.floor(photo.captureTime?.getTime() / 1000) : 0,
|
|
238
|
-
MainPhotoLinkID:
|
|
251
|
+
MainPhotoLinkID: mainPhotoNodeId,
|
|
239
252
|
Tags: photo.tags || [],
|
|
240
253
|
Exif: null, // Deprecated field, not used.
|
|
241
254
|
},
|
|
@@ -52,6 +52,26 @@ type PostLoadLinksMetadataRequest = Extract<
|
|
|
52
52
|
type PostLoadLinksMetadataResponse =
|
|
53
53
|
drivePaths['/drive/v2/volumes/{volumeID}/links']['post']['responses']['200']['content']['application/json'];
|
|
54
54
|
|
|
55
|
+
type PostSmallFileFormData = Extract<
|
|
56
|
+
Extract<
|
|
57
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/small']['post']['requestBody'],
|
|
58
|
+
{ content: object }
|
|
59
|
+
>['content']['multipart/form-data'],
|
|
60
|
+
{ Metadata: object }
|
|
61
|
+
>;
|
|
62
|
+
type PostSmallFileResponse =
|
|
63
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/small']['post']['responses']['200']['content']['application/json'];
|
|
64
|
+
|
|
65
|
+
type PostSmallRevisionFormData = Extract<
|
|
66
|
+
Extract<
|
|
67
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/small']['post']['requestBody'],
|
|
68
|
+
{ content: object }
|
|
69
|
+
>['content']['multipart/form-data'],
|
|
70
|
+
{ Metadata: object }
|
|
71
|
+
>;
|
|
72
|
+
type PostSmallRevisionResponse =
|
|
73
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/small']['post']['responses']['200']['content']['application/json'];
|
|
74
|
+
|
|
55
75
|
export class UploadAPIService {
|
|
56
76
|
constructor(
|
|
57
77
|
protected apiService: DriveAPIService,
|
|
@@ -218,7 +238,7 @@ export class UploadAPIService {
|
|
|
218
238
|
options: {
|
|
219
239
|
armoredManifestSignature: string;
|
|
220
240
|
signatureEmail: string | AnonymousUser;
|
|
221
|
-
armoredExtendedAttributes
|
|
241
|
+
armoredExtendedAttributes: string;
|
|
222
242
|
},
|
|
223
243
|
): Promise<void> {
|
|
224
244
|
const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
|
|
@@ -229,7 +249,7 @@ export class UploadAPIService {
|
|
|
229
249
|
>(`drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/${revisionId}`, {
|
|
230
250
|
ManifestSignature: options.armoredManifestSignature,
|
|
231
251
|
SignatureAddress: options.signatureEmail,
|
|
232
|
-
XAttr: options.armoredExtendedAttributes
|
|
252
|
+
XAttr: options.armoredExtendedAttributes,
|
|
233
253
|
Photo: null, // Only used for photos in the Photo volume.
|
|
234
254
|
});
|
|
235
255
|
}
|
|
@@ -285,4 +305,149 @@ export class UploadAPIService {
|
|
|
285
305
|
link.File?.ActiveRevision?.RevisionID === revisionId
|
|
286
306
|
);
|
|
287
307
|
}
|
|
308
|
+
|
|
309
|
+
async uploadSmallFile(
|
|
310
|
+
parentFolderUid: string,
|
|
311
|
+
metadata: {
|
|
312
|
+
armoredEncryptedName: string;
|
|
313
|
+
hash: string;
|
|
314
|
+
mediaType: string;
|
|
315
|
+
armoredNodeKey: string;
|
|
316
|
+
armoredNodePassphrase: string;
|
|
317
|
+
armoredNodePassphraseSignature: string;
|
|
318
|
+
base64ContentKeyPacket: string;
|
|
319
|
+
armoredContentKeyPacketSignature: string;
|
|
320
|
+
armoredExtendedAttributes: string;
|
|
321
|
+
signatureEmail: string | AnonymousUser;
|
|
322
|
+
},
|
|
323
|
+
content: {
|
|
324
|
+
armoredManifestSignature: string;
|
|
325
|
+
block:
|
|
326
|
+
| {
|
|
327
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
328
|
+
armoredSignature: string;
|
|
329
|
+
verificationToken: Uint8Array<ArrayBuffer>;
|
|
330
|
+
}
|
|
331
|
+
| undefined;
|
|
332
|
+
thumbnails: {
|
|
333
|
+
type: ThumbnailType;
|
|
334
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
335
|
+
}[];
|
|
336
|
+
},
|
|
337
|
+
signal?: AbortSignal,
|
|
338
|
+
): Promise<{ nodeUid: string; nodeRevisionUid: string }> {
|
|
339
|
+
const { volumeId, nodeId: parentNodeId } = splitNodeUid(parentFolderUid);
|
|
340
|
+
|
|
341
|
+
const metadataPayload: PostSmallFileFormData['Metadata'] = {
|
|
342
|
+
ParentLinkID: parentNodeId,
|
|
343
|
+
Name: metadata.armoredEncryptedName,
|
|
344
|
+
NameHash: metadata.hash,
|
|
345
|
+
NodePassphrase: metadata.armoredNodePassphrase,
|
|
346
|
+
NodePassphraseSignature: metadata.armoredNodePassphraseSignature,
|
|
347
|
+
SignatureEmail: metadata.signatureEmail,
|
|
348
|
+
NodeKey: metadata.armoredNodeKey,
|
|
349
|
+
MIMEType: metadata.mediaType,
|
|
350
|
+
ContentKeyPacket: metadata.base64ContentKeyPacket,
|
|
351
|
+
ContentKeyPacketSignature: metadata.armoredContentKeyPacketSignature,
|
|
352
|
+
ManifestSignature: content.armoredManifestSignature,
|
|
353
|
+
ContentBlockEncSignature: content.block ? content.block.armoredSignature : null,
|
|
354
|
+
ContentBlockVerificationToken: content.block
|
|
355
|
+
? uint8ArrayToBase64String(content.block.verificationToken)
|
|
356
|
+
: null,
|
|
357
|
+
XAttr: metadata.armoredExtendedAttributes,
|
|
358
|
+
Photo: null, // TODO
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const formData = new FormData();
|
|
362
|
+
formData.set('Metadata', new Blob([JSON.stringify(metadataPayload)], { type: 'application/json' }), 'Metadata');
|
|
363
|
+
if (content.block) {
|
|
364
|
+
formData.set('ContentBlock', new Blob([content.block.encryptedData]), 'ContentBlock');
|
|
365
|
+
}
|
|
366
|
+
for (const thumb of content.thumbnails) {
|
|
367
|
+
if (formData.get(`ThumbnailBlockType_${thumb.type}`)) {
|
|
368
|
+
throw new Error('Duplicate thumbnail types');
|
|
369
|
+
}
|
|
370
|
+
formData.set(
|
|
371
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
372
|
+
new Blob([thumb.encryptedData]),
|
|
373
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result = await this.apiService.postFormData<PostSmallFileResponse>(
|
|
378
|
+
`drive/v2/volumes/${volumeId}/files/small`,
|
|
379
|
+
formData,
|
|
380
|
+
signal,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
nodeUid: makeNodeUid(volumeId, result.LinkID),
|
|
385
|
+
nodeRevisionUid: makeNodeRevisionUid(volumeId, result.LinkID, result.RevisionID),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async uploadSmallRevision(
|
|
390
|
+
nodeUid: string,
|
|
391
|
+
currentRevisionUid: string,
|
|
392
|
+
metadata: {
|
|
393
|
+
signatureEmail: string | AnonymousUser | null;
|
|
394
|
+
armoredExtendedAttributes: string;
|
|
395
|
+
},
|
|
396
|
+
content: {
|
|
397
|
+
armoredManifestSignature: string;
|
|
398
|
+
block:
|
|
399
|
+
| {
|
|
400
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
401
|
+
armoredSignature: string;
|
|
402
|
+
verificationToken: Uint8Array<ArrayBuffer>;
|
|
403
|
+
}
|
|
404
|
+
| undefined;
|
|
405
|
+
thumbnails: {
|
|
406
|
+
type: ThumbnailType;
|
|
407
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
408
|
+
}[];
|
|
409
|
+
},
|
|
410
|
+
signal?: AbortSignal,
|
|
411
|
+
): Promise<{ nodeUid: string; nodeRevisionUid: string }> {
|
|
412
|
+
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
413
|
+
const { revisionId: currentRevisionId } = splitNodeRevisionUid(currentRevisionUid);
|
|
414
|
+
|
|
415
|
+
const metadataPayload: PostSmallRevisionFormData['Metadata'] = {
|
|
416
|
+
CurrentRevisionID: currentRevisionId,
|
|
417
|
+
SignatureEmail: metadata.signatureEmail,
|
|
418
|
+
ManifestSignature: content.armoredManifestSignature,
|
|
419
|
+
ContentBlockEncSignature: content.block ? content.block.armoredSignature : null,
|
|
420
|
+
ContentBlockVerificationToken: content.block
|
|
421
|
+
? uint8ArrayToBase64String(content.block.verificationToken)
|
|
422
|
+
: null,
|
|
423
|
+
XAttr: metadata.armoredExtendedAttributes,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const formData = new FormData();
|
|
427
|
+
formData.set('Metadata', new Blob([JSON.stringify(metadataPayload)], { type: 'application/json' }), 'Metadata');
|
|
428
|
+
if (content.block) {
|
|
429
|
+
formData.set('ContentBlock', new Blob([content.block.encryptedData]), 'ContentBlock');
|
|
430
|
+
}
|
|
431
|
+
for (const thumb of content.thumbnails) {
|
|
432
|
+
if (formData.get(`ThumbnailBlockType_${thumb.type}`)) {
|
|
433
|
+
throw new Error('Duplicate thumbnail types');
|
|
434
|
+
}
|
|
435
|
+
formData.set(
|
|
436
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
437
|
+
new Blob([thumb.encryptedData]),
|
|
438
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const result = await this.apiService.postFormData<PostSmallRevisionResponse>(
|
|
443
|
+
`drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/small`,
|
|
444
|
+
formData,
|
|
445
|
+
signal,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
nodeUid: makeNodeUid(volumeId, result.LinkID),
|
|
450
|
+
nodeRevisionUid: makeNodeRevisionUid(volumeId, result.LinkID, result.RevisionID),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
288
453
|
}
|
|
@@ -2,6 +2,18 @@ import { PrivateKey, SessionKey } from '../../crypto';
|
|
|
2
2
|
import { UploadAPIService } from './apiService';
|
|
3
3
|
import { UploadCryptoService } from './cryptoService';
|
|
4
4
|
|
|
5
|
+
export async function verifyBlockWithContentKey(
|
|
6
|
+
cryptoService: UploadCryptoService,
|
|
7
|
+
contentKeyPacket: Uint8Array<ArrayBuffer>,
|
|
8
|
+
contentKeyPacketSessionKey: SessionKey,
|
|
9
|
+
encryptedBlock: Uint8Array<ArrayBuffer>,
|
|
10
|
+
): Promise<{
|
|
11
|
+
verificationToken: Uint8Array<ArrayBuffer>;
|
|
12
|
+
}> {
|
|
13
|
+
const verificationCode = contentKeyPacket.subarray(-32);
|
|
14
|
+
return cryptoService.verifyBlock(contentKeyPacketSessionKey, verificationCode, encryptedBlock);
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export class BlockVerifier {
|
|
6
18
|
private verificationCode?: Uint8Array<ArrayBuffer>;
|
|
7
19
|
private contentKeyPacketSessionKey?: SessionKey;
|
|
@@ -145,7 +145,9 @@ export class UploadCryptoService {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
async encryptBlock(
|
|
148
|
-
verifyBlock: (
|
|
148
|
+
verifyBlock: (
|
|
149
|
+
encryptedBlock: Uint8Array<ArrayBuffer>,
|
|
150
|
+
) => Promise<{ verificationToken: Uint8Array<ArrayBuffer> }>,
|
|
149
151
|
nodeRevisionDraftKeys: NodeRevisionDraftKeys,
|
|
150
152
|
block: Uint8Array<ArrayBuffer>,
|
|
151
153
|
index: number,
|
|
@@ -173,24 +175,22 @@ export class UploadCryptoService {
|
|
|
173
175
|
async commitFile(
|
|
174
176
|
nodeRevisionDraftKeys: NodeRevisionDraftKeys,
|
|
175
177
|
manifest: Uint8Array<ArrayBuffer>,
|
|
176
|
-
extendedAttributes
|
|
178
|
+
extendedAttributes: string,
|
|
177
179
|
): Promise<{
|
|
178
180
|
armoredManifestSignature: string;
|
|
179
181
|
signatureEmail: string | AnonymousUser;
|
|
180
|
-
armoredExtendedAttributes
|
|
182
|
+
armoredExtendedAttributes: string;
|
|
181
183
|
}> {
|
|
182
184
|
const { armoredManifestSignature } = await this.driveCrypto.signManifest(
|
|
183
185
|
manifest,
|
|
184
186
|
nodeRevisionDraftKeys.signingKeys.contentSigningKey,
|
|
185
187
|
);
|
|
186
188
|
|
|
187
|
-
const { armoredExtendedAttributes } =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
: { armoredExtendedAttributes: undefined };
|
|
189
|
+
const { armoredExtendedAttributes } = await this.driveCrypto.encryptExtendedAttributes(
|
|
190
|
+
extendedAttributes,
|
|
191
|
+
nodeRevisionDraftKeys.key,
|
|
192
|
+
nodeRevisionDraftKeys.signingKeys.contentSigningKey,
|
|
193
|
+
);
|
|
194
194
|
|
|
195
195
|
return {
|
|
196
196
|
armoredManifestSignature,
|
|
@@ -13,9 +13,9 @@ import { UploadTelemetry } from './telemetry';
|
|
|
13
13
|
* and initiate the upload process for a file object or a stream.
|
|
14
14
|
*
|
|
15
15
|
* This class is not meant to be used directly, but rather to be extended
|
|
16
|
-
* by `FileUploader`
|
|
16
|
+
* by `FileUploader`, `FileRevisionUploader`, or `SmallFileUploader`.
|
|
17
17
|
*/
|
|
18
|
-
abstract class Uploader {
|
|
18
|
+
export abstract class Uploader {
|
|
19
19
|
protected controller: UploadController;
|
|
20
20
|
protected abortController: AbortController;
|
|
21
21
|
|
|
@@ -51,9 +51,9 @@ abstract class Uploader {
|
|
|
51
51
|
thumbnails: Thumbnail[],
|
|
52
52
|
onProgress?: (uploadedBytes: number) => void,
|
|
53
53
|
): Promise<UploadController> {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
this.assertNotStartedYet();
|
|
55
|
+
this.assertUniqueThumbnailTypes(thumbnails);
|
|
56
|
+
|
|
57
57
|
if (!this.metadata.mediaType) {
|
|
58
58
|
this.metadata.mediaType = fileObject.type;
|
|
59
59
|
}
|
|
@@ -72,11 +72,24 @@ abstract class Uploader {
|
|
|
72
72
|
thumbnails: Thumbnail[],
|
|
73
73
|
onProgress?: (uploadedBytes: number) => void,
|
|
74
74
|
): Promise<UploadController> {
|
|
75
|
+
this.assertNotStartedYet();
|
|
76
|
+
this.assertUniqueThumbnailTypes(thumbnails);
|
|
77
|
+
|
|
78
|
+
this.controller.promise = this.startUpload(stream, thumbnails, onProgress);
|
|
79
|
+
return this.controller;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private assertNotStartedYet(): void {
|
|
75
83
|
if (this.controller.promise) {
|
|
76
84
|
throw new Error(`Upload already started`);
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private assertUniqueThumbnailTypes(thumbnails: Thumbnail[]): void {
|
|
89
|
+
const uniqueThumbnailTypes = new Set(thumbnails.map(({ type }) => type));
|
|
90
|
+
if (uniqueThumbnailTypes.size !== thumbnails.length) {
|
|
91
|
+
throw new Error('Duplicate thumbnail types');
|
|
92
|
+
}
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
protected async startUpload(
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { FeatureFlagProvider, FeatureFlags, UploadMetadata } from '../../interface';
|
|
2
|
+
import { getMockTelemetry } from '../../tests/telemetry';
|
|
3
|
+
import { FileRevisionUploader, FileUploader } from './fileUploader';
|
|
4
|
+
import { initUploadModule } from './index';
|
|
5
|
+
import { SmallFileRevisionUploader, SmallFileUploader } from './smallFileUploader';
|
|
6
|
+
|
|
7
|
+
const SMALL_FILE_SIZE_LIMIT = 128 * 1024; // 128 KiB, must match index.ts
|
|
8
|
+
|
|
9
|
+
describe('initUploadModule - uploader selection', () => {
|
|
10
|
+
const parentFolderUid = 'parent-folder-uid';
|
|
11
|
+
const name = 'test-file.txt';
|
|
12
|
+
const nodeUid = 'node-uid';
|
|
13
|
+
|
|
14
|
+
let featureFlagProvider: jest.Mocked<FeatureFlagProvider>;
|
|
15
|
+
let uploadModule: ReturnType<typeof initUploadModule>;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
const apiService = {};
|
|
19
|
+
const driveCrypto = {};
|
|
20
|
+
const sharesService = {};
|
|
21
|
+
const nodesService = {};
|
|
22
|
+
featureFlagProvider = {
|
|
23
|
+
isEnabled: jest.fn().mockResolvedValue(true),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
uploadModule = initUploadModule(
|
|
27
|
+
getMockTelemetry(),
|
|
28
|
+
apiService as any,
|
|
29
|
+
driveCrypto as any,
|
|
30
|
+
sharesService as any,
|
|
31
|
+
nodesService as any,
|
|
32
|
+
featureFlagProvider as any,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('getFileUploader', () => {
|
|
37
|
+
it('returns SmallFileUploader when feature flag is enabled and file size is below limit', async () => {
|
|
38
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
39
|
+
|
|
40
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
41
|
+
const uploader = await uploadModule.getFileUploader(parentFolderUid, name, metadata);
|
|
42
|
+
|
|
43
|
+
expect(uploader).toBeInstanceOf(SmallFileUploader);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns FileUploader when feature flag is enabled but file size exceeds limit', async () => {
|
|
47
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
48
|
+
|
|
49
|
+
const metadata: UploadMetadata = {
|
|
50
|
+
expectedSize: SMALL_FILE_SIZE_LIMIT,
|
|
51
|
+
mediaType: 'text/plain',
|
|
52
|
+
};
|
|
53
|
+
const uploader = await uploadModule.getFileUploader(parentFolderUid, name, metadata);
|
|
54
|
+
|
|
55
|
+
expect(uploader).toBeInstanceOf(FileUploader);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns FileUploader when feature flag is disabled even for small file', async () => {
|
|
59
|
+
featureFlagProvider.isEnabled.mockResolvedValue(false);
|
|
60
|
+
|
|
61
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
62
|
+
const uploader = await uploadModule.getFileUploader(parentFolderUid, name, metadata);
|
|
63
|
+
|
|
64
|
+
expect(uploader).toBeInstanceOf(FileUploader);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('getFileRevisionUploader', () => {
|
|
69
|
+
it('returns SmallFileRevisionUploader when feature flag is enabled and file size is below limit', async () => {
|
|
70
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
71
|
+
|
|
72
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
73
|
+
const uploader = await uploadModule.getFileRevisionUploader(nodeUid, metadata);
|
|
74
|
+
|
|
75
|
+
expect(uploader).toBeInstanceOf(SmallFileRevisionUploader);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns FileRevisionUploader when feature flag is enabled but file size exceeds limit', async () => {
|
|
79
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
80
|
+
|
|
81
|
+
const metadata: UploadMetadata = {
|
|
82
|
+
expectedSize: SMALL_FILE_SIZE_LIMIT + 1,
|
|
83
|
+
mediaType: 'text/plain',
|
|
84
|
+
};
|
|
85
|
+
const uploader = await uploadModule.getFileRevisionUploader(nodeUid, metadata);
|
|
86
|
+
|
|
87
|
+
expect(uploader).toBeInstanceOf(FileRevisionUploader);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns FileRevisionUploader when feature flag is disabled even for small file', async () => {
|
|
91
|
+
featureFlagProvider.isEnabled.mockResolvedValue(false);
|
|
92
|
+
|
|
93
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
94
|
+
const uploader = await uploadModule.getFileRevisionUploader(nodeUid, metadata);
|
|
95
|
+
|
|
96
|
+
expect(uploader).toBeInstanceOf(FileRevisionUploader);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { FeatureFlagProvider, ProtonDriveTelemetry, UploadMetadata } from '../../interface';
|
|
1
|
+
import { FeatureFlagProvider, FeatureFlags, ProtonDriveTelemetry, UploadMetadata } from '../../interface';
|
|
2
|
+
import type { FileUploader } from '../../interface';
|
|
2
3
|
import { DriveAPIService } from '../apiService';
|
|
3
4
|
import { DriveCrypto } from '../../crypto';
|
|
4
5
|
import { UploadAPIService } from './apiService';
|
|
5
6
|
import { UploadCryptoService } from './cryptoService';
|
|
6
|
-
import { FileUploader, FileRevisionUploader } from './fileUploader';
|
|
7
|
+
import { FileUploader as FileUploaderClass, FileRevisionUploader } from './fileUploader';
|
|
7
8
|
import { NodesService, SharesService } from './interface';
|
|
8
9
|
import { UploadManager } from './manager';
|
|
9
10
|
import { UploadQueue } from './queue';
|
|
11
|
+
import { SmallFileRevisionUploader, SmallFileUploader } from './smallFileUploader';
|
|
10
12
|
import { UploadTelemetry } from './telemetry';
|
|
11
13
|
|
|
14
|
+
const SMALL_FILE_SIZE_LIMIT = 128 * 1024; // 128 KiB
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Provides facade for the upload module.
|
|
14
18
|
*
|
|
@@ -24,6 +28,7 @@ export function initUploadModule(
|
|
|
24
28
|
nodesService: NodesService,
|
|
25
29
|
featureFlagProvider: FeatureFlagProvider,
|
|
26
30
|
clientUid?: string,
|
|
31
|
+
allowSmallFileUpload: boolean = true,
|
|
27
32
|
) {
|
|
28
33
|
const api = new UploadAPIService(apiService, clientUid);
|
|
29
34
|
const cryptoService = new UploadCryptoService(telemetry, driveCrypto, nodesService, featureFlagProvider);
|
|
@@ -33,6 +38,15 @@ export function initUploadModule(
|
|
|
33
38
|
|
|
34
39
|
const queue = new UploadQueue();
|
|
35
40
|
|
|
41
|
+
async function useSmallFileUpload(metadata: UploadMetadata): Promise<boolean> {
|
|
42
|
+
const isEnabled =
|
|
43
|
+
allowSmallFileUpload && (await featureFlagProvider.isEnabled(FeatureFlags.DriveSmallFileUpload));
|
|
44
|
+
if (!isEnabled) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return metadata.expectedSize < SMALL_FILE_SIZE_LIMIT;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
51
|
* Returns a FileUploader instance that can be used to upload a file to
|
|
38
52
|
* a parent folder.
|
|
@@ -52,7 +66,21 @@ export function initUploadModule(
|
|
|
52
66
|
queue.releaseCapacity(metadata.expectedSize);
|
|
53
67
|
};
|
|
54
68
|
|
|
55
|
-
|
|
69
|
+
if (await useSmallFileUpload(metadata)) {
|
|
70
|
+
return new SmallFileUploader(
|
|
71
|
+
uploadTelemetry,
|
|
72
|
+
api,
|
|
73
|
+
cryptoService,
|
|
74
|
+
manager,
|
|
75
|
+
metadata,
|
|
76
|
+
onFinish,
|
|
77
|
+
signal,
|
|
78
|
+
parentFolderUid,
|
|
79
|
+
name,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new FileUploaderClass(
|
|
56
84
|
uploadTelemetry,
|
|
57
85
|
api,
|
|
58
86
|
cryptoService,
|
|
@@ -76,13 +104,26 @@ export function initUploadModule(
|
|
|
76
104
|
nodeUid: string,
|
|
77
105
|
metadata: UploadMetadata,
|
|
78
106
|
signal?: AbortSignal,
|
|
79
|
-
): Promise<
|
|
107
|
+
): Promise<FileUploader> {
|
|
80
108
|
await queue.waitForCapacity(metadata.expectedSize, signal);
|
|
81
109
|
|
|
82
110
|
const onFinish = () => {
|
|
83
111
|
queue.releaseCapacity(metadata.expectedSize);
|
|
84
112
|
};
|
|
85
113
|
|
|
114
|
+
if (await useSmallFileUpload(metadata)) {
|
|
115
|
+
return new SmallFileRevisionUploader(
|
|
116
|
+
uploadTelemetry,
|
|
117
|
+
api,
|
|
118
|
+
cryptoService,
|
|
119
|
+
manager,
|
|
120
|
+
metadata,
|
|
121
|
+
onFinish,
|
|
122
|
+
signal,
|
|
123
|
+
nodeUid,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
86
127
|
return new FileRevisionUploader(
|
|
87
128
|
uploadTelemetry,
|
|
88
129
|
api,
|
|
@@ -40,6 +40,7 @@ export type NodeCrypto = {
|
|
|
40
40
|
};
|
|
41
41
|
contentKey: {
|
|
42
42
|
encrypted: {
|
|
43
|
+
contentKeyPacket: Uint8Array<ArrayBuffer>;
|
|
43
44
|
base64ContentKeyPacket: string;
|
|
44
45
|
armoredContentKeyPacketSignature: string;
|
|
45
46
|
};
|
|
@@ -100,6 +101,7 @@ export interface NodesService {
|
|
|
100
101
|
getNodeKeys(nodeUid: string): Promise<{
|
|
101
102
|
key: PrivateKey;
|
|
102
103
|
passphraseSessionKey: SessionKey;
|
|
104
|
+
contentKeyPacket?: Uint8Array<ArrayBuffer>;
|
|
103
105
|
contentKeyPacketSessionKey?: SessionKey;
|
|
104
106
|
hashKey?: Uint8Array<ArrayBuffer>;
|
|
105
107
|
}>;
|