@protontech/drive-sdk 0.14.0 → 0.14.2

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 (66) hide show
  1. package/dist/internal/batchLoading.d.ts +2 -0
  2. package/dist/internal/batchLoading.js +18 -5
  3. package/dist/internal/batchLoading.js.map +1 -1
  4. package/dist/internal/batchLoading.test.js +92 -0
  5. package/dist/internal/batchLoading.test.js.map +1 -1
  6. package/dist/internal/nodes/nodesAccess.js +1 -1
  7. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  8. package/dist/internal/nodes/nodesAccess.test.js +3 -2
  9. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  10. package/dist/internal/photos/index.js +3 -1
  11. package/dist/internal/photos/index.js.map +1 -1
  12. package/dist/internal/photos/upload.d.ts +1 -1
  13. package/dist/internal/photos/upload.js +2 -2
  14. package/dist/internal/photos/upload.js.map +1 -1
  15. package/dist/internal/sharing/events.d.ts +4 -2
  16. package/dist/internal/sharing/events.js +40 -9
  17. package/dist/internal/sharing/events.js.map +1 -1
  18. package/dist/internal/sharing/events.test.js +30 -2
  19. package/dist/internal/sharing/events.test.js.map +1 -1
  20. package/dist/internal/sharing/index.js +1 -1
  21. package/dist/internal/sharing/index.js.map +1 -1
  22. package/dist/internal/sharingPublic/session/index.d.ts +1 -0
  23. package/dist/internal/sharingPublic/session/index.js +3 -1
  24. package/dist/internal/sharingPublic/session/index.js.map +1 -1
  25. package/dist/internal/sharingPublic/session/manager.d.ts +2 -0
  26. package/dist/internal/sharingPublic/session/manager.js +1 -0
  27. package/dist/internal/sharingPublic/session/manager.js.map +1 -1
  28. package/dist/internal/upload/fileUploader.d.ts +19 -3
  29. package/dist/internal/upload/fileUploader.js +31 -5
  30. package/dist/internal/upload/fileUploader.js.map +1 -1
  31. package/dist/internal/upload/fileUploader.test.js +1 -1
  32. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  33. package/dist/internal/upload/index.js +4 -11
  34. package/dist/internal/upload/index.js.map +1 -1
  35. package/dist/internal/upload/index.test.js +104 -45
  36. package/dist/internal/upload/index.test.js.map +1 -1
  37. package/dist/internal/upload/smallFileUploader.d.ts +14 -14
  38. package/dist/internal/upload/smallFileUploader.js +38 -20
  39. package/dist/internal/upload/smallFileUploader.js.map +1 -1
  40. package/dist/internal/upload/smallFileUploader.test.js +35 -36
  41. package/dist/internal/upload/smallFileUploader.test.js.map +1 -1
  42. package/dist/protonDriveClient.js +2 -1
  43. package/dist/protonDriveClient.js.map +1 -1
  44. package/dist/protonDrivePublicLinkClient.d.ts +15 -1
  45. package/dist/protonDrivePublicLinkClient.js +7 -1
  46. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/internal/batchLoading.test.ts +104 -0
  49. package/src/internal/batchLoading.ts +21 -5
  50. package/src/internal/nodes/nodesAccess.test.ts +4 -3
  51. package/src/internal/nodes/nodesAccess.ts +1 -1
  52. package/src/internal/photos/index.ts +2 -0
  53. package/src/internal/photos/upload.ts +13 -1
  54. package/src/internal/sharing/events.test.ts +35 -2
  55. package/src/internal/sharing/events.ts +47 -10
  56. package/src/internal/sharing/index.ts +1 -0
  57. package/src/internal/sharingPublic/session/index.ts +1 -0
  58. package/src/internal/sharingPublic/session/manager.ts +2 -0
  59. package/src/internal/upload/fileUploader.test.ts +1 -0
  60. package/src/internal/upload/fileUploader.ts +60 -2
  61. package/src/internal/upload/index.test.ts +121 -63
  62. package/src/internal/upload/index.ts +4 -30
  63. package/src/internal/upload/smallFileUploader.test.ts +33 -40
  64. package/src/internal/upload/smallFileUploader.ts +47 -36
  65. package/src/protonDriveClient.ts +2 -1
  66. package/src/protonDrivePublicLinkClient.ts +23 -2
@@ -1,13 +1,14 @@
1
1
  import { Logger } from '../../interface';
2
2
  import { DriveEvent, DriveEventType } from '../events';
3
3
  import { SharingCache } from './cache';
4
- import { SharesService } from './interface';
4
+ import { NodesService, SharesService } from './interface';
5
5
 
6
6
  export class SharingEventHandler {
7
7
  constructor(
8
8
  private logger: Logger,
9
9
  private cache: SharingCache,
10
10
  private shares: SharesService,
11
+ private nodesService: NodesService,
11
12
  ) {}
12
13
 
13
14
  /**
@@ -26,23 +27,59 @@ export class SharingEventHandler {
26
27
  */
27
28
  async handleDriveEvent(event: DriveEvent) {
28
29
  try {
29
- if (event.type === DriveEventType.SharedWithMeUpdated) {
30
- await this.cache.setSharedWithMeNodeUids(undefined);
31
- return;
32
- }
30
+ await this.handleSharedWithMeNodeUidsLoaded(event);
33
31
  await this.handleSharedByMeNodeUidsLoaded(event);
34
32
  } catch (error: unknown) {
35
- this.logger.error(`Skipping shared by me node cache update`, error);
33
+ this.logger.error(`Skipping sharing cache update`, error);
36
34
  }
37
35
  }
38
36
 
39
- private async handleSharedByMeNodeUidsLoaded(event: DriveEvent) {
40
- if (event.type === DriveEventType.TreeRefresh || event.type === DriveEventType.TreeRemove) {
41
- await this.cache.setSharedWithMeNodeUids(undefined);
37
+ private async handleSharedWithMeNodeUidsLoaded(event: DriveEvent) {
38
+ if (
39
+ ![DriveEventType.SharedWithMeUpdated, DriveEventType.TreeRefresh, DriveEventType.TreeRemove].includes(
40
+ event.type,
41
+ )
42
+ ) {
42
43
  return;
43
44
  }
44
45
 
45
- if (![DriveEventType.NodeCreated, DriveEventType.NodeUpdated, DriveEventType.NodeDeleted].includes(event.type)) {
46
+ // When user changes the membership (permissions) for a user, the
47
+ // backend emits both NodeUpdated and SharedWithMeUpdated events.
48
+ // Ideally, the SDK doesn't have to refresh all the shared nodes,
49
+ // only those that were changed via the NodeUpdated event. However,
50
+ // the client very likely will not be subscribed to all shared volumes.
51
+ // When the client only lists the list itself and not the trees, it
52
+ // is still required to refresh all the nodes to be sure to have the
53
+ // latest state.
54
+ // The sharing module doesn't have access to the nodes cache, thus
55
+ // it notifies the nodes via the service. If this fails, we need to
56
+ // log it, but it should not block the event handling. The node might
57
+ // be wrong at the "shared with me" listing, but it will be eventually
58
+ // updated once the user opens the volume tree and client processes
59
+ // the events for that volume.
60
+ // Ideally, in the future, the Drive API provides a custom event with
61
+ // indication of what node was added or removed or updated, instead
62
+ // of emitting destructive SharedWithMeUpdated event.
63
+ const hasSharedWithMeLoaded = await this.cache.hasSharedWithMeNodeUidsLoaded();
64
+ if (event.type === DriveEventType.SharedWithMeUpdated && hasSharedWithMeLoaded) {
65
+ try {
66
+ const sharedWithMeNodeUids = await this.cache.getSharedWithMeNodeUids();
67
+ this.logger.debug(`Shared with me updated, notifying ${sharedWithMeNodeUids.length} nodes`);
68
+ for (const nodeUid of sharedWithMeNodeUids) {
69
+ await this.nodesService.notifyNodeChanged(nodeUid);
70
+ }
71
+ } catch (error: unknown) {
72
+ this.logger.error(`Skipping shared with me node cache update`, error);
73
+ }
74
+ }
75
+
76
+ await this.cache.setSharedWithMeNodeUids(undefined);
77
+ }
78
+
79
+ private async handleSharedByMeNodeUidsLoaded(event: DriveEvent) {
80
+ if (
81
+ ![DriveEventType.NodeCreated, DriveEventType.NodeUpdated, DriveEventType.NodeDeleted].includes(event.type)
82
+ ) {
46
83
  return;
47
84
  }
48
85
 
@@ -48,6 +48,7 @@ export function initSharingModule(
48
48
  telemetry.getLogger('sharing-event-handler'),
49
49
  cache,
50
50
  sharesService,
51
+ nodesService,
51
52
  );
52
53
 
53
54
  return {
@@ -1 +1,2 @@
1
1
  export { SharingPublicSessionManager } from './manager';
2
+ export { SharingPublicLinkSession } from './session';
@@ -87,6 +87,7 @@ export class SharingPublicSessionManager {
87
87
  shareKey: PrivateKey;
88
88
  rootUid: string;
89
89
  publicRole: MemberRole;
90
+ session: SharingPublicLinkSession;
90
91
  }> {
91
92
  let info = this.infosPerToken.get(token);
92
93
  if (!info) {
@@ -105,6 +106,7 @@ export class SharingPublicSessionManager {
105
106
  shareKey,
106
107
  rootUid,
107
108
  publicRole: permissionsToMemberRole(this.logger, encryptedShare.publicPermissions),
109
+ session,
108
110
  };
109
111
  }
110
112
 
@@ -129,6 +129,7 @@ describe('FileUploader', () => {
129
129
  'name',
130
130
  metadata,
131
131
  onFinish,
132
+ () => Promise.resolve(false),
132
133
  abortController.signal,
133
134
  );
134
135
 
@@ -5,6 +5,7 @@ import { UploadController } from './controller';
5
5
  import { UploadCryptoService } from './cryptoService';
6
6
  import { NodeRevisionDraft } from './interface';
7
7
  import { UploadManager } from './manager';
8
+ import { SmallFileRevisionUploader, SmallFileUploader } from './smallFileUploader';
8
9
  import { StreamUploader } from './streamUploader';
9
10
  import { UploadTelemetry } from './telemetry';
10
11
 
@@ -26,6 +27,7 @@ export abstract class Uploader {
26
27
  protected manager: UploadManager,
27
28
  protected metadata: UploadMetadata,
28
29
  protected onFinish: () => void,
30
+ protected shouldUseSmallFileUpload: (expectedSize: number) => Promise<boolean>,
29
31
  protected signal?: AbortSignal,
30
32
  ) {
31
33
  this.telemetry = telemetry;
@@ -34,6 +36,7 @@ export abstract class Uploader {
34
36
  this.manager = manager;
35
37
  this.metadata = metadata;
36
38
  this.onFinish = onFinish;
39
+ this.shouldUseSmallFileUpload = shouldUseSmallFileUpload;
37
40
 
38
41
  this.signal = signal;
39
42
  this.abortController = new AbortController();
@@ -97,10 +100,28 @@ export abstract class Uploader {
97
100
  thumbnails: Thumbnail[],
98
101
  onProgress?: (uploadedBytes: number) => void,
99
102
  ): Promise<{ nodeRevisionUid: string; nodeUid: string }> {
103
+ const expectedEncryptedTotalSize = this.getExpectedEncryptedTotalSize(thumbnails);
104
+ if (await this.shouldUseSmallFileUpload(expectedEncryptedTotalSize)) {
105
+ return this.initSmallFileUploader(stream, thumbnails, onProgress);
106
+ }
107
+
100
108
  const uploader = await this.initStreamUploader();
101
109
  return uploader.start(stream, thumbnails, onProgress);
102
110
  }
103
111
 
112
+ private getExpectedEncryptedTotalSize(thumbnails: Thumbnail[]): number {
113
+ const thumbnailSize = thumbnails.reduce((acc, thumbnail) => acc + thumbnail.thumbnail.length, 0);
114
+ const totalSize = this.metadata.expectedSize + thumbnailSize;
115
+ const expectedEncryptedTotalSize = totalSize * 1.1; // 10% margin for encryption overhead
116
+ return expectedEncryptedTotalSize;
117
+ }
118
+
119
+ protected abstract initSmallFileUploader(
120
+ stream: ReadableStream,
121
+ thumbnails: Thumbnail[],
122
+ onProgress?: (uploadedBytes: number) => void,
123
+ ): Promise<{ nodeRevisionUid: string; nodeUid: string }>;
124
+
104
125
  protected async initStreamUploader(): Promise<StreamUploader> {
105
126
  const { revisionDraft, blockVerifier } = await this.createRevisionDraft();
106
127
 
@@ -154,9 +175,10 @@ export class FileUploader extends Uploader {
154
175
  private name: string,
155
176
  metadata: UploadMetadata,
156
177
  onFinish: () => void,
178
+ protected shouldUseSmallFileUpload: (expectedSize: number) => Promise<boolean>,
157
179
  signal?: AbortSignal,
158
180
  ) {
159
- super(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal);
181
+ super(telemetry, apiService, cryptoService, manager, metadata, onFinish, shouldUseSmallFileUpload, signal);
160
182
 
161
183
  this.parentFolderUid = parentFolderUid;
162
184
  this.name = name;
@@ -192,6 +214,24 @@ export class FileUploader extends Uploader {
192
214
  protected async deleteRevisionDraft(revisionDraft: NodeRevisionDraft): Promise<void> {
193
215
  await this.manager.deleteDraftNode(revisionDraft.nodeUid);
194
216
  }
217
+
218
+ protected async initSmallFileUploader(
219
+ stream: ReadableStream,
220
+ thumbnails: Thumbnail[],
221
+ onProgress?: (uploadedBytes: number) => void,
222
+ ): Promise<{ nodeRevisionUid: string; nodeUid: string }> {
223
+ const uploader = new SmallFileUploader(
224
+ this.telemetry,
225
+ this.cryptoService,
226
+ this.manager,
227
+ this.metadata,
228
+ this.onFinish,
229
+ this.signal,
230
+ this.parentFolderUid,
231
+ this.name,
232
+ );
233
+ return uploader.upload(stream, thumbnails, onProgress);
234
+ }
195
235
  }
196
236
 
197
237
  /**
@@ -206,9 +246,10 @@ export class FileRevisionUploader extends Uploader {
206
246
  private nodeUid: string,
207
247
  metadata: UploadMetadata,
208
248
  onFinish: () => void,
249
+ protected shouldUseSmallFileUpload: (expectedSize: number) => Promise<boolean>,
209
250
  signal?: AbortSignal,
210
251
  ) {
211
- super(telemetry, apiService, cryptoService, manager, metadata, onFinish, signal);
252
+ super(telemetry, apiService, cryptoService, manager, metadata, onFinish, shouldUseSmallFileUpload, signal);
212
253
 
213
254
  this.nodeUid = nodeUid;
214
255
  }
@@ -243,4 +284,21 @@ export class FileRevisionUploader extends Uploader {
243
284
  protected async deleteRevisionDraft(revisionDraft: NodeRevisionDraft): Promise<void> {
244
285
  await this.manager.deleteDraftRevision(revisionDraft.nodeRevisionUid);
245
286
  }
287
+
288
+ protected async initSmallFileUploader(
289
+ stream: ReadableStream,
290
+ thumbnails: Thumbnail[],
291
+ onProgress?: (uploadedBytes: number) => void,
292
+ ): Promise<{ nodeRevisionUid: string; nodeUid: string }> {
293
+ const uploader = new SmallFileRevisionUploader(
294
+ this.telemetry,
295
+ this.cryptoService,
296
+ this.manager,
297
+ this.metadata,
298
+ this.onFinish,
299
+ this.signal,
300
+ this.nodeUid,
301
+ );
302
+ return uploader.upload(stream, thumbnails, onProgress);
303
+ }
246
304
  }
@@ -1,18 +1,23 @@
1
- import { FeatureFlagProvider, FeatureFlags, UploadMetadata } from '../../interface';
1
+ import { FeatureFlagProvider, ThumbnailType, UploadMetadata } from '../../interface';
2
2
  import { getMockTelemetry } from '../../tests/telemetry';
3
- import { FileRevisionUploader, FileUploader } from './fileUploader';
3
+ import { FileRevisionUploader, FileUploader, Uploader } from './fileUploader';
4
4
  import { initUploadModule } from './index';
5
- import { SmallFileRevisionUploader, SmallFileUploader } from './smallFileUploader';
6
5
 
7
- const SMALL_FILE_SIZE_LIMIT = 128 * 1024; // 128 KiB, must match index.ts
6
+ const RAW_SMALL_FILE_SIZE_LIMIT = (128 * 1024) / 1.1; // 128 KiB, must match index.ts
8
7
 
9
- describe('initUploadModule - uploader selection', () => {
8
+ describe('initUploadModule', () => {
10
9
  const parentFolderUid = 'parent-folder-uid';
11
10
  const name = 'test-file.txt';
12
11
  const nodeUid = 'node-uid';
13
12
 
14
13
  let featureFlagProvider: jest.Mocked<FeatureFlagProvider>;
15
14
  let uploadModule: ReturnType<typeof initUploadModule>;
15
+ let initSmallFileSpy: jest.SpyInstance;
16
+ let initSmallRevisionSpy: jest.SpyInstance;
17
+ let initStreamSpy: jest.SpyInstance;
18
+
19
+ let stream: ReadableStream;
20
+ const thumbnail100k = { type: ThumbnailType.Type1, thumbnail: new Uint8Array(100_000) };
16
21
 
17
22
  beforeEach(() => {
18
23
  const apiService = {};
@@ -31,69 +36,122 @@ describe('initUploadModule - uploader selection', () => {
31
36
  nodesService as any,
32
37
  featureFlagProvider as any,
33
38
  );
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
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);
40
+ initSmallFileSpy = jest.spyOn(FileUploader.prototype as any, 'initSmallFileUploader').mockResolvedValue({
41
+ nodeRevisionUid: 'revision-uid',
42
+ nodeUid: 'node-uid',
44
43
  });
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);
44
+ initSmallRevisionSpy = jest
45
+ .spyOn(FileRevisionUploader.prototype as any, 'initSmallFileUploader')
46
+ .mockResolvedValue({
47
+ nodeRevisionUid: 'revision-uid',
48
+ nodeUid: 'node-uid',
49
+ });
50
+ initStreamSpy = jest.spyOn(Uploader.prototype as any, 'initStreamUploader').mockResolvedValue({
51
+ start: jest.fn().mockResolvedValue({
52
+ nodeRevisionUid: 'revision-uid',
53
+ nodeUid: 'node-uid',
54
+ }),
55
+ } as any);
56
+
57
+ stream = new ReadableStream({
58
+ start(controller) {
59
+ controller.close();
60
+ },
65
61
  });
66
62
  });
67
63
 
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);
64
+ afterEach(() => {
65
+ jest.restoreAllMocks();
66
+ });
95
67
 
96
- expect(uploader).toBeInstanceOf(FileRevisionUploader);
68
+ async function drainUpload(controller: { completion(): Promise<unknown> }) {
69
+ await controller.completion();
70
+ }
71
+
72
+ const suites = [
73
+ {
74
+ method: 'getFileUploader',
75
+ getUploader: (metadata: UploadMetadata) => uploadModule.getFileUploader(parentFolderUid, name, metadata),
76
+ expect: (option: 'small' | 'stream') => {
77
+ if (option === 'stream') {
78
+ expect(initStreamSpy).toHaveBeenCalled();
79
+ expect(initSmallFileSpy).not.toHaveBeenCalled();
80
+ expect(initSmallRevisionSpy).not.toHaveBeenCalled();
81
+ } else {
82
+ expect(initSmallFileSpy).toHaveBeenCalled();
83
+ expect(initStreamSpy).not.toHaveBeenCalled();
84
+ expect(initSmallRevisionSpy).not.toHaveBeenCalled();
85
+ }
86
+ },
87
+ },
88
+ {
89
+ method: 'getFileRevisionUploader',
90
+ getUploader: (metadata: UploadMetadata) => uploadModule.getFileRevisionUploader(nodeUid, metadata),
91
+ expect: (option: 'small' | 'stream') => {
92
+ if (option === 'stream') {
93
+ expect(initStreamSpy).toHaveBeenCalled();
94
+ expect(initSmallFileSpy).not.toHaveBeenCalled();
95
+ expect(initSmallRevisionSpy).not.toHaveBeenCalled();
96
+ } else {
97
+ expect(initSmallRevisionSpy).toHaveBeenCalled();
98
+ expect(initSmallFileSpy).not.toHaveBeenCalled();
99
+ expect(initStreamSpy).not.toHaveBeenCalled();
100
+ }
101
+ },
102
+ },
103
+ ];
104
+ for (const suite of suites) {
105
+ describe(suite.method, () => {
106
+ it('uses stream path when feature flag is disabled even for small file', async () => {
107
+ featureFlagProvider.isEnabled.mockResolvedValue(false);
108
+
109
+ const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
110
+ const uploader = await suite.getUploader(metadata);
111
+ await drainUpload(await uploader.uploadFromStream(stream, []));
112
+
113
+ suite.expect('stream');
114
+ });
115
+
116
+ it('uses small-file path when flag is on and encrypted total size is below cap', async () => {
117
+ featureFlagProvider.isEnabled.mockResolvedValue(true);
118
+
119
+ const metadata: UploadMetadata = { expectedSize: 100, mediaType: 'text/plain' };
120
+ const uploader = await suite.getUploader(metadata);
121
+ await drainUpload(await uploader.uploadFromStream(stream, []));
122
+
123
+ suite.expect('small');
124
+ });
125
+
126
+ it('uses small-file path when flag is on and encrypted total size with thumbnails is below cap', async () => {
127
+ featureFlagProvider.isEnabled.mockResolvedValue(true);
128
+
129
+ const metadata: UploadMetadata = { expectedSize: 100, mediaType: 'image/jpeg' };
130
+ const uploader = await suite.getUploader(metadata);
131
+ await drainUpload(await uploader.uploadFromStream(stream, [thumbnail100k]));
132
+
133
+ suite.expect('small');
134
+ });
135
+
136
+ it('uses stream path when feature flag is enabled but raw file size exceeds limit', async () => {
137
+ featureFlagProvider.isEnabled.mockResolvedValue(true);
138
+
139
+ const metadata: UploadMetadata = { expectedSize: RAW_SMALL_FILE_SIZE_LIMIT, mediaType: 'text/plain' };
140
+ const uploader = await suite.getUploader(metadata);
141
+ await drainUpload(await uploader.uploadFromStream(stream, []));
142
+
143
+ suite.expect('stream');
144
+ });
145
+
146
+ it('uses stream path when thumbnail bytes push encrypted total size with thumbnail exceeds limit', async () => {
147
+ featureFlagProvider.isEnabled.mockResolvedValue(true);
148
+
149
+ const metadata: UploadMetadata = { expectedSize: 100_000, mediaType: 'image/jpeg' };
150
+ const uploader = await suite.getUploader(metadata);
151
+ await drainUpload(await uploader.uploadFromStream(stream, [thumbnail100k]));
152
+
153
+ suite.expect('stream');
154
+ });
97
155
  });
98
- });
156
+ }
99
157
  });
@@ -8,7 +8,6 @@ import { FileUploader as FileUploaderClass, FileRevisionUploader } from './fileU
8
8
  import { NodesService, SharesService } from './interface';
9
9
  import { UploadManager } from './manager';
10
10
  import { UploadQueue } from './queue';
11
- import { SmallFileRevisionUploader, SmallFileUploader } from './smallFileUploader';
12
11
  import { UploadTelemetry } from './telemetry';
13
12
 
14
13
  const SMALL_FILE_SIZE_LIMIT = 128 * 1024; // 128 KiB
@@ -38,13 +37,13 @@ export function initUploadModule(
38
37
 
39
38
  const queue = new UploadQueue();
40
39
 
41
- async function useSmallFileUpload(metadata: UploadMetadata): Promise<boolean> {
40
+ async function shouldUseSmallFileUpload(expectedSize: number): Promise<boolean> {
42
41
  const isEnabled =
43
42
  allowSmallFileUpload && (await featureFlagProvider.isEnabled(FeatureFlags.DriveSmallFileUpload));
44
43
  if (!isEnabled) {
45
44
  return false;
46
45
  }
47
- return metadata.expectedSize < SMALL_FILE_SIZE_LIMIT;
46
+ return expectedSize < SMALL_FILE_SIZE_LIMIT;
48
47
  }
49
48
 
50
49
  /**
@@ -66,20 +65,6 @@ export function initUploadModule(
66
65
  queue.releaseCapacity(metadata.expectedSize);
67
66
  };
68
67
 
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
68
  return new FileUploaderClass(
84
69
  uploadTelemetry,
85
70
  api,
@@ -89,6 +74,7 @@ export function initUploadModule(
89
74
  name,
90
75
  metadata,
91
76
  onFinish,
77
+ shouldUseSmallFileUpload,
92
78
  signal,
93
79
  );
94
80
  }
@@ -111,19 +97,6 @@ export function initUploadModule(
111
97
  queue.releaseCapacity(metadata.expectedSize);
112
98
  };
113
99
 
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
-
127
100
  return new FileRevisionUploader(
128
101
  uploadTelemetry,
129
102
  api,
@@ -132,6 +105,7 @@ export function initUploadModule(
132
105
  nodeUid,
133
106
  metadata,
134
107
  onFinish,
108
+ shouldUseSmallFileUpload,
135
109
  signal,
136
110
  );
137
111
  }