@protontech/drive-sdk 0.12.0 → 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.
Files changed (124) hide show
  1. package/dist/crypto/driveCrypto.d.ts +1 -0
  2. package/dist/crypto/driveCrypto.js +1 -0
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/interface/featureFlags.d.ts +2 -1
  5. package/dist/interface/featureFlags.js +1 -0
  6. package/dist/interface/featureFlags.js.map +1 -1
  7. package/dist/interface/httpClient.d.ts +1 -0
  8. package/dist/interface/nodes.d.ts +7 -0
  9. package/dist/interface/nodes.js.map +1 -1
  10. package/dist/internal/apiService/apiService.d.ts +1 -0
  11. package/dist/internal/apiService/apiService.js +16 -7
  12. package/dist/internal/apiService/apiService.js.map +1 -1
  13. package/dist/internal/apiService/apiService.test.js +24 -0
  14. package/dist/internal/apiService/apiService.test.js.map +1 -1
  15. package/dist/internal/apiService/driveTypes.d.ts +110 -101
  16. package/dist/internal/nodes/apiService.d.ts +4 -0
  17. package/dist/internal/nodes/apiService.js +4 -0
  18. package/dist/internal/nodes/apiService.js.map +1 -1
  19. package/dist/internal/nodes/apiService.test.js +8 -0
  20. package/dist/internal/nodes/apiService.test.js.map +1 -1
  21. package/dist/internal/nodes/cryptoService.js +3 -0
  22. package/dist/internal/nodes/cryptoService.js.map +1 -1
  23. package/dist/internal/nodes/extendedAttributes.d.ts +5 -5
  24. package/dist/internal/nodes/extendedAttributes.js +5 -14
  25. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  26. package/dist/internal/nodes/extendedAttributes.test.js +16 -22
  27. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  28. package/dist/internal/nodes/interface.d.ts +5 -0
  29. package/dist/internal/nodes/nodesManagement.d.ts +3 -3
  30. package/dist/internal/nodes/nodesManagement.js +7 -5
  31. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  32. package/dist/internal/photos/albumsManager.js +1 -0
  33. package/dist/internal/photos/albumsManager.js.map +1 -1
  34. package/dist/internal/photos/nodes.d.ts +1 -1
  35. package/dist/internal/photos/nodes.js +2 -2
  36. package/dist/internal/photos/nodes.js.map +1 -1
  37. package/dist/internal/photos/upload.d.ts +5 -5
  38. package/dist/internal/photos/upload.js +8 -2
  39. package/dist/internal/photos/upload.js.map +1 -1
  40. package/dist/internal/sharingPublic/nodes.d.ts +1 -0
  41. package/dist/internal/upload/apiService.d.ts +45 -1
  42. package/dist/internal/upload/apiService.js +69 -1
  43. package/dist/internal/upload/apiService.js.map +1 -1
  44. package/dist/internal/upload/blockVerifier.d.ts +4 -1
  45. package/dist/internal/upload/blockVerifier.js +5 -0
  46. package/dist/internal/upload/blockVerifier.js.map +1 -1
  47. package/dist/internal/upload/cryptoService.d.ts +2 -2
  48. package/dist/internal/upload/cryptoService.js +1 -3
  49. package/dist/internal/upload/cryptoService.js.map +1 -1
  50. package/dist/internal/upload/fileUploader.d.ts +4 -3
  51. package/dist/internal/upload/fileUploader.js +17 -7
  52. package/dist/internal/upload/fileUploader.js.map +1 -1
  53. package/dist/internal/upload/index.d.ts +3 -3
  54. package/dist/internal/upload/index.js +17 -1
  55. package/dist/internal/upload/index.js.map +1 -1
  56. package/dist/internal/upload/index.test.d.ts +1 -0
  57. package/dist/internal/upload/index.test.js +71 -0
  58. package/dist/internal/upload/index.test.js.map +1 -0
  59. package/dist/internal/upload/interface.d.ts +2 -0
  60. package/dist/internal/upload/manager.d.ts +41 -2
  61. package/dist/internal/upload/manager.js +123 -42
  62. package/dist/internal/upload/manager.js.map +1 -1
  63. package/dist/internal/upload/manager.test.js +267 -0
  64. package/dist/internal/upload/manager.test.js.map +1 -1
  65. package/dist/internal/upload/smallFileUploader.d.ts +83 -0
  66. package/dist/internal/upload/smallFileUploader.js +197 -0
  67. package/dist/internal/upload/smallFileUploader.js.map +1 -0
  68. package/dist/internal/upload/smallFileUploader.test.d.ts +1 -0
  69. package/dist/internal/upload/smallFileUploader.test.js +358 -0
  70. package/dist/internal/upload/smallFileUploader.test.js.map +1 -0
  71. package/dist/internal/upload/streamReader.d.ts +4 -0
  72. package/dist/internal/upload/streamReader.js +37 -0
  73. package/dist/internal/upload/streamReader.js.map +1 -0
  74. package/dist/internal/upload/streamReader.test.d.ts +1 -0
  75. package/dist/internal/upload/streamReader.test.js +90 -0
  76. package/dist/internal/upload/streamReader.test.js.map +1 -0
  77. package/dist/internal/upload/streamUploader.d.ts +6 -0
  78. package/dist/internal/upload/streamUploader.js +3 -3
  79. package/dist/internal/upload/streamUploader.js.map +1 -1
  80. package/dist/internal/upload/telemetry.d.ts +3 -2
  81. package/dist/internal/upload/telemetry.js +3 -0
  82. package/dist/internal/upload/telemetry.js.map +1 -1
  83. package/dist/protonDrivePhotosClient.d.ts +1 -1
  84. package/dist/protonDrivePublicLinkClient.js +3 -1
  85. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  86. package/dist/transformers.d.ts +1 -1
  87. package/dist/transformers.js +1 -0
  88. package/dist/transformers.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/crypto/driveCrypto.ts +2 -0
  91. package/src/interface/featureFlags.ts +1 -0
  92. package/src/interface/httpClient.ts +1 -0
  93. package/src/interface/nodes.ts +7 -0
  94. package/src/internal/apiService/apiService.test.ts +30 -0
  95. package/src/internal/apiService/apiService.ts +23 -7
  96. package/src/internal/apiService/driveTypes.ts +110 -101
  97. package/src/internal/nodes/apiService.test.ts +9 -0
  98. package/src/internal/nodes/apiService.ts +4 -0
  99. package/src/internal/nodes/cryptoService.ts +11 -1
  100. package/src/internal/nodes/extendedAttributes.test.ts +25 -25
  101. package/src/internal/nodes/extendedAttributes.ts +10 -19
  102. package/src/internal/nodes/interface.ts +5 -0
  103. package/src/internal/nodes/nodesManagement.ts +8 -6
  104. package/src/internal/photos/albumsManager.ts +1 -0
  105. package/src/internal/photos/nodes.ts +2 -2
  106. package/src/internal/photos/upload.ts +23 -10
  107. package/src/internal/upload/apiService.ts +167 -2
  108. package/src/internal/upload/blockVerifier.ts +12 -0
  109. package/src/internal/upload/cryptoService.ts +10 -10
  110. package/src/internal/upload/fileUploader.ts +20 -7
  111. package/src/internal/upload/index.test.ts +99 -0
  112. package/src/internal/upload/index.ts +45 -4
  113. package/src/internal/upload/interface.ts +2 -0
  114. package/src/internal/upload/manager.test.ts +367 -1
  115. package/src/internal/upload/manager.ts +226 -76
  116. package/src/internal/upload/smallFileUploader.test.ts +491 -0
  117. package/src/internal/upload/smallFileUploader.ts +353 -0
  118. package/src/internal/upload/streamReader.test.ts +109 -0
  119. package/src/internal/upload/streamReader.ts +38 -0
  120. package/src/internal/upload/streamUploader.ts +1 -1
  121. package/src/internal/upload/telemetry.ts +5 -1
  122. package/src/protonDrivePhotosClient.ts +1 -1
  123. package/src/protonDrivePublicLinkClient.ts +2 -0
  124. package/src/transformers.ts +2 -0
@@ -1,8 +1,15 @@
1
1
  import { DriveCrypto } from '../../crypto';
2
- import { ProtonDriveTelemetry, UploadMetadata, Thumbnail, AnonymousUser, FeatureFlagProvider } from '../../interface';
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
- mainPhotoLinkID?: string;
26
- // TODO: handle tags enum in the SDK
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
- mainPhotoLinkID: uploadMetadata.mainPhotoLinkID,
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
- mainPhotoLinkID?: string;
222
- // TODO: handle tags enum in the SDK
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: photo.mainPhotoLinkID || null,
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?: string;
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 || null,
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: (encryptedBlock: Uint8Array<ArrayBuffer>) => Promise<{ verificationToken: Uint8Array<ArrayBuffer> }>,
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?: string,
178
+ extendedAttributes: string,
177
179
  ): Promise<{
178
180
  armoredManifestSignature: string;
179
181
  signatureEmail: string | AnonymousUser;
180
- armoredExtendedAttributes?: string;
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 } = extendedAttributes
188
- ? await this.driveCrypto.encryptExtendedAttributes(
189
- extendedAttributes,
190
- nodeRevisionDraftKeys.key,
191
- nodeRevisionDraftKeys.signingKeys.contentSigningKey,
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` and `FileRevisionUploader`.
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
- if (this.controller.promise) {
55
- throw new Error(`Upload already started`);
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
- this.controller.promise = this.startUpload(stream, thumbnails, onProgress);
79
- return this.controller;
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
- return new FileUploader(
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<FileRevisionUploader> {
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
  }>;