@protontech/drive-sdk 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) 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/events.d.ts +1 -1
  5. package/dist/interface/featureFlags.d.ts +2 -1
  6. package/dist/interface/featureFlags.js +1 -0
  7. package/dist/interface/featureFlags.js.map +1 -1
  8. package/dist/interface/httpClient.d.ts +1 -0
  9. package/dist/interface/nodes.d.ts +7 -0
  10. package/dist/interface/nodes.js.map +1 -1
  11. package/dist/interface/telemetry.d.ts +4 -0
  12. package/dist/interface/telemetry.js.map +1 -1
  13. package/dist/internal/apiService/apiService.d.ts +1 -0
  14. package/dist/internal/apiService/apiService.js +16 -7
  15. package/dist/internal/apiService/apiService.js.map +1 -1
  16. package/dist/internal/apiService/apiService.test.js +24 -0
  17. package/dist/internal/apiService/apiService.test.js.map +1 -1
  18. package/dist/internal/apiService/driveTypes.d.ts +162 -101
  19. package/dist/internal/download/telemetry.js +4 -0
  20. package/dist/internal/download/telemetry.js.map +1 -1
  21. package/dist/internal/download/telemetry.test.js +5 -0
  22. package/dist/internal/download/telemetry.test.js.map +1 -1
  23. package/dist/internal/events/index.js +2 -2
  24. package/dist/internal/events/index.js.map +1 -1
  25. package/dist/internal/events/interface.d.ts +1 -1
  26. package/dist/internal/nodes/apiService.d.ts +4 -0
  27. package/dist/internal/nodes/apiService.js +4 -0
  28. package/dist/internal/nodes/apiService.js.map +1 -1
  29. package/dist/internal/nodes/apiService.test.js +8 -0
  30. package/dist/internal/nodes/apiService.test.js.map +1 -1
  31. package/dist/internal/nodes/cryptoService.js +3 -0
  32. package/dist/internal/nodes/cryptoService.js.map +1 -1
  33. package/dist/internal/nodes/extendedAttributes.d.ts +5 -5
  34. package/dist/internal/nodes/extendedAttributes.js +5 -14
  35. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  36. package/dist/internal/nodes/extendedAttributes.test.js +16 -22
  37. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  38. package/dist/internal/nodes/interface.d.ts +5 -0
  39. package/dist/internal/nodes/nodesManagement.d.ts +3 -3
  40. package/dist/internal/nodes/nodesManagement.js +7 -5
  41. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  42. package/dist/internal/photos/albumsManager.js +1 -0
  43. package/dist/internal/photos/albumsManager.js.map +1 -1
  44. package/dist/internal/photos/nodes.d.ts +1 -1
  45. package/dist/internal/photos/nodes.js +2 -2
  46. package/dist/internal/photos/nodes.js.map +1 -1
  47. package/dist/internal/photos/upload.d.ts +5 -5
  48. package/dist/internal/photos/upload.js +8 -2
  49. package/dist/internal/photos/upload.js.map +1 -1
  50. package/dist/internal/sharing/apiService.js +1 -1
  51. package/dist/internal/sharing/apiService.js.map +1 -1
  52. package/dist/internal/sharingPublic/nodes.d.ts +1 -0
  53. package/dist/internal/upload/apiService.d.ts +45 -1
  54. package/dist/internal/upload/apiService.js +69 -1
  55. package/dist/internal/upload/apiService.js.map +1 -1
  56. package/dist/internal/upload/blockVerifier.d.ts +4 -1
  57. package/dist/internal/upload/blockVerifier.js +5 -0
  58. package/dist/internal/upload/blockVerifier.js.map +1 -1
  59. package/dist/internal/upload/cryptoService.d.ts +2 -2
  60. package/dist/internal/upload/cryptoService.js +1 -3
  61. package/dist/internal/upload/cryptoService.js.map +1 -1
  62. package/dist/internal/upload/fileUploader.d.ts +4 -3
  63. package/dist/internal/upload/fileUploader.js +17 -7
  64. package/dist/internal/upload/fileUploader.js.map +1 -1
  65. package/dist/internal/upload/index.d.ts +3 -3
  66. package/dist/internal/upload/index.js +17 -1
  67. package/dist/internal/upload/index.js.map +1 -1
  68. package/dist/internal/upload/index.test.d.ts +1 -0
  69. package/dist/internal/upload/index.test.js +71 -0
  70. package/dist/internal/upload/index.test.js.map +1 -0
  71. package/dist/internal/upload/interface.d.ts +2 -0
  72. package/dist/internal/upload/manager.d.ts +41 -2
  73. package/dist/internal/upload/manager.js +126 -44
  74. package/dist/internal/upload/manager.js.map +1 -1
  75. package/dist/internal/upload/manager.test.js +268 -1
  76. package/dist/internal/upload/manager.test.js.map +1 -1
  77. package/dist/internal/upload/smallFileUploader.d.ts +83 -0
  78. package/dist/internal/upload/smallFileUploader.js +197 -0
  79. package/dist/internal/upload/smallFileUploader.js.map +1 -0
  80. package/dist/internal/upload/smallFileUploader.test.d.ts +1 -0
  81. package/dist/internal/upload/smallFileUploader.test.js +358 -0
  82. package/dist/internal/upload/smallFileUploader.test.js.map +1 -0
  83. package/dist/internal/upload/streamReader.d.ts +4 -0
  84. package/dist/internal/upload/streamReader.js +37 -0
  85. package/dist/internal/upload/streamReader.js.map +1 -0
  86. package/dist/internal/upload/streamReader.test.d.ts +1 -0
  87. package/dist/internal/upload/streamReader.test.js +90 -0
  88. package/dist/internal/upload/streamReader.test.js.map +1 -0
  89. package/dist/internal/upload/streamUploader.d.ts +6 -0
  90. package/dist/internal/upload/streamUploader.js +3 -3
  91. package/dist/internal/upload/streamUploader.js.map +1 -1
  92. package/dist/internal/upload/telemetry.d.ts +3 -2
  93. package/dist/internal/upload/telemetry.js +5 -0
  94. package/dist/internal/upload/telemetry.js.map +1 -1
  95. package/dist/internal/upload/telemetry.test.js +6 -0
  96. package/dist/internal/upload/telemetry.test.js.map +1 -1
  97. package/dist/protonDrivePhotosClient.d.ts +1 -1
  98. package/dist/protonDrivePublicLinkClient.js +3 -1
  99. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  100. package/dist/telemetry.d.ts +1 -0
  101. package/dist/telemetry.js +21 -0
  102. package/dist/telemetry.js.map +1 -1
  103. package/dist/telemetry.test.d.ts +1 -0
  104. package/dist/telemetry.test.js +37 -0
  105. package/dist/telemetry.test.js.map +1 -0
  106. package/dist/transformers.d.ts +1 -1
  107. package/dist/transformers.js +1 -0
  108. package/dist/transformers.js.map +1 -1
  109. package/package.json +1 -1
  110. package/src/crypto/driveCrypto.ts +2 -0
  111. package/src/interface/events.ts +1 -1
  112. package/src/interface/featureFlags.ts +1 -0
  113. package/src/interface/httpClient.ts +1 -0
  114. package/src/interface/nodes.ts +7 -0
  115. package/src/interface/telemetry.ts +4 -0
  116. package/src/internal/apiService/apiService.test.ts +30 -0
  117. package/src/internal/apiService/apiService.ts +23 -7
  118. package/src/internal/apiService/driveTypes.ts +162 -101
  119. package/src/internal/download/telemetry.test.ts +5 -0
  120. package/src/internal/download/telemetry.ts +5 -1
  121. package/src/internal/events/index.ts +2 -2
  122. package/src/internal/events/interface.ts +1 -1
  123. package/src/internal/nodes/apiService.test.ts +9 -0
  124. package/src/internal/nodes/apiService.ts +4 -0
  125. package/src/internal/nodes/cryptoService.ts +11 -1
  126. package/src/internal/nodes/extendedAttributes.test.ts +25 -25
  127. package/src/internal/nodes/extendedAttributes.ts +10 -19
  128. package/src/internal/nodes/interface.ts +5 -0
  129. package/src/internal/nodes/nodesManagement.ts +8 -6
  130. package/src/internal/photos/albumsManager.ts +1 -0
  131. package/src/internal/photos/nodes.ts +2 -2
  132. package/src/internal/photos/upload.ts +23 -10
  133. package/src/internal/sharing/apiService.ts +5 -5
  134. package/src/internal/upload/apiService.ts +167 -2
  135. package/src/internal/upload/blockVerifier.ts +12 -0
  136. package/src/internal/upload/cryptoService.ts +10 -10
  137. package/src/internal/upload/fileUploader.ts +20 -7
  138. package/src/internal/upload/index.test.ts +99 -0
  139. package/src/internal/upload/index.ts +45 -4
  140. package/src/internal/upload/interface.ts +2 -0
  141. package/src/internal/upload/manager.test.ts +368 -2
  142. package/src/internal/upload/manager.ts +229 -78
  143. package/src/internal/upload/smallFileUploader.test.ts +491 -0
  144. package/src/internal/upload/smallFileUploader.ts +353 -0
  145. package/src/internal/upload/streamReader.test.ts +109 -0
  146. package/src/internal/upload/streamReader.ts +38 -0
  147. package/src/internal/upload/streamUploader.ts +1 -1
  148. package/src/internal/upload/telemetry.test.ts +6 -0
  149. package/src/internal/upload/telemetry.ts +8 -2
  150. package/src/protonDrivePhotosClient.ts +1 -1
  151. package/src/protonDrivePublicLinkClient.ts +2 -0
  152. package/src/telemetry.test.ts +40 -0
  153. package/src/telemetry.ts +22 -0
  154. package/src/transformers.ts +2 -0
@@ -312,6 +312,7 @@ export abstract class NodesManagementBase<
312
312
  async createFolder(parentNodeUid: string, folderName: string, modificationTime?: Date): Promise<TDecryptedNode> {
313
313
  validateNodeName(folderName);
314
314
 
315
+ const parentNode = await this.nodesAccess.getNode(parentNodeUid);
315
316
  const parentKeys = await this.nodesAccess.getNodeKeys(parentNodeUid);
316
317
  if (!parentKeys.hashKey) {
317
318
  throw new ValidationError(c('Error').t`Creating folders in non-folders is not allowed`);
@@ -338,14 +339,14 @@ export abstract class NodesManagementBase<
338
339
  });
339
340
 
340
341
  await this.nodesAccess.notifyChildCreated(parentNodeUid);
341
- const node = this.generateNodeFolder(nodeUid, parentNodeUid, folderName, encryptedCrypto);
342
+ const node = this.generateNodeFolder(parentNode, nodeUid, folderName, encryptedCrypto);
342
343
  await this.cryptoCache.setNodeKeys(nodeUid, keys);
343
344
  return node;
344
345
  }
345
346
 
346
347
  protected abstract generateNodeFolder(
348
+ parentNode: TDecryptedNode,
347
349
  nodeUid: string,
348
- parentUid: string,
349
350
  name: string,
350
351
  encryptedCrypto: {
351
352
  hash: string;
@@ -355,8 +356,8 @@ export abstract class NodesManagementBase<
355
356
  ): TDecryptedNode;
356
357
 
357
358
  protected generateNodeFolderBase(
359
+ parentNode: TDecryptedNode,
358
360
  nodeUid: string,
359
- parentNodeUid: string,
360
361
  name: string,
361
362
  encryptedCrypto: {
362
363
  hash: string;
@@ -371,7 +372,7 @@ export abstract class NodesManagementBase<
371
372
 
372
373
  // Basic node metadata
373
374
  uid: nodeUid,
374
- parentUid: parentNodeUid,
375
+ parentUid: parentNode.uid,
375
376
  type: NodeType.Folder,
376
377
  mediaType: FOLDER_MEDIA_TYPE,
377
378
  creationTime: new Date(),
@@ -381,6 +382,7 @@ export abstract class NodesManagementBase<
381
382
  isShared: false,
382
383
  isSharedPublicly: false,
383
384
  directRole: MemberRole.Inherited,
385
+ ownedBy: parentNode.ownedBy,
384
386
 
385
387
  // Decrypted metadata
386
388
  isStale: false,
@@ -432,8 +434,8 @@ export abstract class NodesManagementBase<
432
434
 
433
435
  export class NodesManagement extends NodesManagementBase {
434
436
  protected generateNodeFolder(
437
+ parentNode: DecryptedNode,
435
438
  nodeUid: string,
436
- parentNodeUid: string,
437
439
  name: string,
438
440
  encryptedCrypto: {
439
441
  hash: string;
@@ -441,6 +443,6 @@ export class NodesManagement extends NodesManagementBase {
441
443
  signatureEmail: string | null;
442
444
  },
443
445
  ): DecryptedNode {
444
- return this.generateNodeFolderBase(nodeUid, parentNodeUid, name, encryptedCrypto);
446
+ return this.generateNodeFolderBase(parentNode, nodeUid, name, encryptedCrypto);
445
447
  }
446
448
  }
@@ -95,6 +95,7 @@ export class AlbumsManager {
95
95
  isShared: false,
96
96
  isSharedPublicly: false,
97
97
  directRole: MemberRole.Inherited,
98
+ ownedBy: rootNode.ownedBy,
98
99
 
99
100
  // Decrypted metadata
100
101
  isStale: false,
@@ -252,8 +252,8 @@ export class PhotosNodesManagement extends NodesManagementBase<
252
252
  PhotosNodesCryptoService
253
253
  > {
254
254
  protected generateNodeFolder(
255
+ parentNode: DecryptedPhotoNode,
255
256
  nodeUid: string,
256
- parentNodeUid: string,
257
257
  name: string,
258
258
  encryptedCrypto: {
259
259
  hash: string;
@@ -261,6 +261,6 @@ export class PhotosNodesManagement extends NodesManagementBase<
261
261
  signatureEmail: string | null;
262
262
  },
263
263
  ): DecryptedPhotoNode {
264
- return this.generateNodeFolderBase(nodeUid, parentNodeUid, name, encryptedCrypto);
264
+ return this.generateNodeFolderBase(parentNode, nodeUid, name, encryptedCrypto);
265
265
  }
266
266
  }
@@ -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
  },
@@ -79,12 +79,12 @@ type PostCreateShareResponse =
79
79
  drivePaths['/drive/volumes/{volumeID}/shares']['post']['responses']['200']['content']['application/json'];
80
80
 
81
81
  type PostChangeSharePropertiesRequest = Extract<
82
- drivePaths['/drive/shares/{shareID}/property']['post']['requestBody'],
82
+ drivePaths['/drive/shares/{shareID}/editors-can-share']['put']['requestBody'],
83
83
  { content: object }
84
84
  >['content']['application/json'];
85
85
 
86
86
  type PostChangeSharePropertiesResponse =
87
- drivePaths['/drive/shares/{shareID}/property']['post']['responses']['200']['content']['application/json'];
87
+ drivePaths['/drive/shares/{shareID}/editors-can-share']['put']['responses']['200']['content']['application/json'];
88
88
 
89
89
  type PostInviteProtonUserRequest = Extract<
90
90
  drivePaths['/drive/v2/shares/{shareID}/invitations']['post']['requestBody'],
@@ -423,9 +423,9 @@ export class SharingAPIService {
423
423
  }
424
424
 
425
425
  async changeShareProperties(shareId: string, { editorsCanShare }: { editorsCanShare: boolean }) {
426
- await this.apiService.post<PostChangeSharePropertiesRequest, PostChangeSharePropertiesResponse>(
427
- `drive/shares/${shareId}/property`,
428
- { EditorsCanShare: editorsCanShare },
426
+ await this.apiService.put<PostChangeSharePropertiesRequest, PostChangeSharePropertiesResponse>(
427
+ `drive/shares/${shareId}/editors-can-share`,
428
+ { Value: editorsCanShare },
429
429
  );
430
430
  }
431
431
 
@@ -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
+ });