@protontech/drive-sdk 0.4.1 → 0.5.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 (197) hide show
  1. package/dist/diagnostic/{sdkDiagnosticFull.d.ts → diagnostic.d.ts} +5 -4
  2. package/dist/diagnostic/{sdkDiagnosticFull.js → diagnostic.js} +13 -10
  3. package/dist/diagnostic/diagnostic.js.map +1 -0
  4. package/dist/diagnostic/index.js +2 -4
  5. package/dist/diagnostic/index.js.map +1 -1
  6. package/dist/diagnostic/interface.d.ts +22 -1
  7. package/dist/diagnostic/sdkDiagnostic.d.ts +3 -2
  8. package/dist/diagnostic/sdkDiagnostic.js +80 -8
  9. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  10. package/dist/interface/download.d.ts +4 -4
  11. package/dist/interface/index.d.ts +1 -1
  12. package/dist/interface/index.js.map +1 -1
  13. package/dist/interface/nodes.d.ts +9 -0
  14. package/dist/interface/telemetry.d.ts +4 -1
  15. package/dist/interface/telemetry.js.map +1 -1
  16. package/dist/interface/upload.d.ts +6 -3
  17. package/dist/internal/apiService/apiService.d.ts +3 -0
  18. package/dist/internal/apiService/apiService.js +25 -2
  19. package/dist/internal/apiService/apiService.js.map +1 -1
  20. package/dist/internal/apiService/apiService.test.js +38 -0
  21. package/dist/internal/apiService/apiService.test.js.map +1 -1
  22. package/dist/internal/apiService/driveTypes.d.ts +2595 -2397
  23. package/dist/internal/apiService/errors.js +3 -0
  24. package/dist/internal/apiService/errors.js.map +1 -1
  25. package/dist/internal/apiService/errors.test.js +15 -7
  26. package/dist/internal/apiService/errors.test.js.map +1 -1
  27. package/dist/internal/asyncIteratorMap.d.ts +1 -1
  28. package/dist/internal/asyncIteratorMap.js +6 -1
  29. package/dist/internal/asyncIteratorMap.js.map +1 -1
  30. package/dist/internal/asyncIteratorMap.test.js +9 -0
  31. package/dist/internal/asyncIteratorMap.test.js.map +1 -1
  32. package/dist/internal/download/controller.d.ts +2 -0
  33. package/dist/internal/download/controller.js +15 -1
  34. package/dist/internal/download/controller.js.map +1 -1
  35. package/dist/internal/download/fileDownloader.d.ts +3 -3
  36. package/dist/internal/download/fileDownloader.js +11 -6
  37. package/dist/internal/download/fileDownloader.js.map +1 -1
  38. package/dist/internal/download/fileDownloader.test.js +8 -8
  39. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  40. package/dist/internal/nodes/apiService.d.ts +6 -1
  41. package/dist/internal/nodes/apiService.js +71 -44
  42. package/dist/internal/nodes/apiService.js.map +1 -1
  43. package/dist/internal/nodes/apiService.test.js +204 -15
  44. package/dist/internal/nodes/apiService.test.js.map +1 -1
  45. package/dist/internal/nodes/debouncer.d.ts +24 -0
  46. package/dist/internal/nodes/debouncer.js +92 -0
  47. package/dist/internal/nodes/debouncer.js.map +1 -0
  48. package/dist/internal/nodes/debouncer.test.d.ts +1 -0
  49. package/dist/internal/nodes/debouncer.test.js +108 -0
  50. package/dist/internal/nodes/debouncer.test.js.map +1 -0
  51. package/dist/internal/nodes/extendedAttributes.js +2 -2
  52. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  53. package/dist/internal/nodes/index.js +1 -1
  54. package/dist/internal/nodes/index.js.map +1 -1
  55. package/dist/internal/nodes/nodesAccess.d.ts +6 -4
  56. package/dist/internal/nodes/nodesAccess.js +29 -9
  57. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  58. package/dist/internal/nodes/nodesAccess.test.js +19 -7
  59. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  60. package/dist/internal/nodes/nodesManagement.d.ts +2 -2
  61. package/dist/internal/nodes/nodesManagement.js +5 -3
  62. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  63. package/dist/internal/nodes/nodesManagement.test.js +3 -1
  64. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  65. package/dist/internal/photos/apiService.js +9 -20
  66. package/dist/internal/photos/apiService.js.map +1 -1
  67. package/dist/internal/photos/upload.d.ts +2 -1
  68. package/dist/internal/photos/upload.js +9 -3
  69. package/dist/internal/photos/upload.js.map +1 -1
  70. package/dist/internal/sharing/apiService.d.ts +1 -1
  71. package/dist/internal/sharing/apiService.js +2 -2
  72. package/dist/internal/sharing/apiService.js.map +1 -1
  73. package/dist/internal/sharing/sharingManagement.d.ts +4 -1
  74. package/dist/internal/sharing/sharingManagement.js +7 -4
  75. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  76. package/dist/internal/sharingPublic/apiService.d.ts +8 -10
  77. package/dist/internal/sharingPublic/apiService.js +9 -125
  78. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  79. package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
  80. package/dist/internal/sharingPublic/{cryptoService.js → cryptoReporter.js} +3 -16
  81. package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
  82. package/dist/internal/sharingPublic/index.d.ts +22 -4
  83. package/dist/internal/sharingPublic/index.js +37 -12
  84. package/dist/internal/sharingPublic/index.js.map +1 -1
  85. package/dist/internal/sharingPublic/nodes.d.ts +18 -0
  86. package/dist/internal/sharingPublic/nodes.js +46 -0
  87. package/dist/internal/sharingPublic/nodes.js.map +1 -0
  88. package/dist/internal/sharingPublic/session/apiService.d.ts +7 -5
  89. package/dist/internal/sharingPublic/session/apiService.js +25 -4
  90. package/dist/internal/sharingPublic/session/apiService.js.map +1 -1
  91. package/dist/internal/sharingPublic/session/interface.d.ts +17 -0
  92. package/dist/internal/sharingPublic/session/manager.d.ts +12 -4
  93. package/dist/internal/sharingPublic/session/manager.js +14 -4
  94. package/dist/internal/sharingPublic/session/manager.js.map +1 -1
  95. package/dist/internal/sharingPublic/session/session.d.ts +7 -4
  96. package/dist/internal/sharingPublic/session/session.js +7 -3
  97. package/dist/internal/sharingPublic/session/session.js.map +1 -1
  98. package/dist/internal/sharingPublic/session/url.test.js +3 -3
  99. package/dist/internal/sharingPublic/shares.d.ts +27 -0
  100. package/dist/internal/sharingPublic/shares.js +46 -0
  101. package/dist/internal/sharingPublic/shares.js.map +1 -0
  102. package/dist/internal/upload/apiService.js +10 -1
  103. package/dist/internal/upload/apiService.js.map +1 -1
  104. package/dist/internal/upload/controller.d.ts +11 -3
  105. package/dist/internal/upload/controller.js +16 -2
  106. package/dist/internal/upload/controller.js.map +1 -1
  107. package/dist/internal/upload/fileUploader.d.ts +6 -3
  108. package/dist/internal/upload/fileUploader.js +4 -4
  109. package/dist/internal/upload/fileUploader.js.map +1 -1
  110. package/dist/internal/upload/fileUploader.test.js +23 -11
  111. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  112. package/dist/internal/upload/streamUploader.d.ts +9 -4
  113. package/dist/internal/upload/streamUploader.js +67 -20
  114. package/dist/internal/upload/streamUploader.js.map +1 -1
  115. package/dist/internal/upload/streamUploader.test.js +43 -13
  116. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  117. package/dist/protonDriveClient.d.ts +11 -6
  118. package/dist/protonDriveClient.js +11 -10
  119. package/dist/protonDriveClient.js.map +1 -1
  120. package/dist/protonDrivePublicLinkClient.d.ts +34 -6
  121. package/dist/protonDrivePublicLinkClient.js +52 -9
  122. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  123. package/dist/tests/telemetry.d.ts +4 -2
  124. package/dist/tests/telemetry.js +3 -1
  125. package/dist/tests/telemetry.js.map +1 -1
  126. package/dist/transformers.d.ts +3 -2
  127. package/dist/transformers.js +6 -0
  128. package/dist/transformers.js.map +1 -1
  129. package/package.json +1 -1
  130. package/src/diagnostic/{sdkDiagnosticFull.ts → diagnostic.ts} +10 -6
  131. package/src/diagnostic/index.ts +3 -5
  132. package/src/diagnostic/interface.ts +39 -0
  133. package/src/diagnostic/sdkDiagnostic.ts +111 -10
  134. package/src/interface/download.ts +4 -4
  135. package/src/interface/index.ts +1 -0
  136. package/src/interface/nodes.ts +3 -0
  137. package/src/interface/telemetry.ts +5 -0
  138. package/src/interface/upload.ts +3 -3
  139. package/src/internal/apiService/apiService.test.ts +50 -0
  140. package/src/internal/apiService/apiService.ts +33 -2
  141. package/src/internal/apiService/driveTypes.ts +2713 -2561
  142. package/src/internal/apiService/errors.test.ts +10 -0
  143. package/src/internal/apiService/errors.ts +5 -1
  144. package/src/internal/asyncIteratorMap.test.ts +12 -0
  145. package/src/internal/asyncIteratorMap.ts +8 -0
  146. package/src/internal/download/controller.ts +13 -1
  147. package/src/internal/download/fileDownloader.test.ts +8 -8
  148. package/src/internal/download/fileDownloader.ts +13 -6
  149. package/src/internal/nodes/apiService.test.ts +261 -14
  150. package/src/internal/nodes/apiService.ts +99 -65
  151. package/src/internal/nodes/debouncer.test.ts +141 -0
  152. package/src/internal/nodes/debouncer.ts +109 -0
  153. package/src/internal/nodes/extendedAttributes.ts +2 -2
  154. package/src/internal/nodes/index.ts +1 -8
  155. package/src/internal/nodes/nodesAccess.test.ts +19 -7
  156. package/src/internal/nodes/nodesAccess.ts +44 -9
  157. package/src/internal/nodes/nodesManagement.test.ts +3 -1
  158. package/src/internal/nodes/nodesManagement.ts +11 -5
  159. package/src/internal/photos/apiService.ts +12 -29
  160. package/src/internal/photos/upload.ts +22 -1
  161. package/src/internal/sharing/apiService.ts +2 -2
  162. package/src/internal/sharing/sharingManagement.ts +7 -4
  163. package/src/internal/sharingPublic/apiService.ts +23 -160
  164. package/src/internal/sharingPublic/{cryptoService.ts → cryptoReporter.ts} +2 -27
  165. package/src/internal/sharingPublic/index.ts +76 -13
  166. package/src/internal/sharingPublic/nodes.ts +59 -0
  167. package/src/internal/sharingPublic/session/apiService.ts +32 -10
  168. package/src/internal/sharingPublic/session/interface.ts +20 -0
  169. package/src/internal/sharingPublic/session/manager.ts +31 -8
  170. package/src/internal/sharingPublic/session/session.ts +12 -7
  171. package/src/internal/sharingPublic/session/url.test.ts +3 -3
  172. package/src/internal/sharingPublic/shares.ts +50 -0
  173. package/src/internal/upload/apiService.ts +12 -1
  174. package/src/internal/upload/controller.ts +16 -4
  175. package/src/internal/upload/fileUploader.test.ts +25 -11
  176. package/src/internal/upload/fileUploader.ts +6 -5
  177. package/src/internal/upload/streamUploader.test.ts +56 -12
  178. package/src/internal/upload/streamUploader.ts +78 -20
  179. package/src/protonDriveClient.ts +29 -11
  180. package/src/protonDrivePublicLinkClient.ts +100 -16
  181. package/src/tests/telemetry.ts +6 -3
  182. package/src/transformers.ts +8 -0
  183. package/dist/diagnostic/sdkDiagnosticFull.js.map +0 -1
  184. package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -19
  185. package/dist/internal/sharingPublic/cryptoCache.js +0 -72
  186. package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
  187. package/dist/internal/sharingPublic/cryptoService.d.ts +0 -9
  188. package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
  189. package/dist/internal/sharingPublic/interface.d.ts +0 -6
  190. package/dist/internal/sharingPublic/interface.js +0 -3
  191. package/dist/internal/sharingPublic/interface.js.map +0 -1
  192. package/dist/internal/sharingPublic/manager.d.ts +0 -19
  193. package/dist/internal/sharingPublic/manager.js +0 -81
  194. package/dist/internal/sharingPublic/manager.js.map +0 -1
  195. package/src/internal/sharingPublic/cryptoCache.ts +0 -79
  196. package/src/internal/sharingPublic/interface.ts +0 -14
  197. package/src/internal/sharingPublic/manager.ts +0 -86
@@ -1,3 +1,4 @@
1
+ import { AbortError } from '../../errors';
1
2
  import { apiErrorFactory } from './errors';
2
3
  import * as errors from './errors';
3
4
  import { ErrorCode } from './errorCodes';
@@ -17,6 +18,15 @@ function mockAPIResponseAndResult(options: {
17
18
  }
18
19
 
19
20
  describe('apiErrorFactory should return', () => {
21
+ it('AbortError on aborted error', () => {
22
+ const abortError = new Error('AbortError');
23
+ abortError.name = 'AbortError';
24
+
25
+ const error = apiErrorFactory({ response: new Response(), error: abortError });
26
+ expect(error).toBeInstanceOf(AbortError);
27
+ expect(error.message).toBe('Request aborted');
28
+ });
29
+
20
30
  it('generic APIHTTPError when there is no specifc body', () => {
21
31
  const response = new Response('', { status: 404, statusText: 'Not found' });
22
32
  const error = apiErrorFactory({ response });
@@ -1,6 +1,6 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { ServerError, ValidationError } from '../../errors';
3
+ import { AbortError, ServerError, ValidationError } from '../../errors';
4
4
  import { ErrorCode, HTTPErrorCode } from './errorCodes';
5
5
 
6
6
  export function apiErrorFactory({
@@ -12,6 +12,10 @@ export function apiErrorFactory({
12
12
  result?: unknown;
13
13
  error?: unknown;
14
14
  }): ServerError {
15
+ if (error && error instanceof Error && error.name === 'AbortError') {
16
+ return new AbortError(c('Error').t`Request aborted`);
17
+ }
18
+
15
19
  // Backend responses with 404 both in the response and body code.
16
20
  // In such a case we want to stick to APIHTTPError to be very clear
17
21
  // it is not NotFoundAPIError.
@@ -1,3 +1,4 @@
1
+ import { AbortError } from '../errors';
1
2
  import { asyncIteratorMap } from './asyncIteratorMap';
2
3
 
3
4
  // Helper function to create an async generator from array
@@ -147,4 +148,15 @@ describe('asyncIteratorMap', () => {
147
148
  expect(maxConcurrentExecutions).toBe(concurrencyLimit);
148
149
  expect(results).toEqual([2, 4, 6, 8, 10, 12, 14, 16]);
149
150
  });
151
+
152
+ test('throws AbortError if signal is aborted', async () => {
153
+ const inputGen = createAsyncGenerator([1, 2, 3, 4, 5]);
154
+ const mapper = async (x: number) => x * 2;
155
+
156
+ const ac = new AbortController();
157
+ ac.abort();
158
+
159
+ const mappedGen = asyncIteratorMap(inputGen, mapper, 1, ac.signal);
160
+ await expect(collectResults(mappedGen)).rejects.toThrow(AbortError);
161
+ });
150
162
  });
@@ -1,3 +1,7 @@
1
+ import { c } from 'ttag';
2
+
3
+ import { AbortError } from '../errors';
4
+
1
5
  const DEFAULT_CONCURRENCY = 10;
2
6
 
3
7
  /**
@@ -18,6 +22,7 @@ export async function* asyncIteratorMap<I, O>(
18
22
  inputIterator: AsyncGenerator<I>,
19
23
  mapper: (item: I) => Promise<O>,
20
24
  concurrency: number = DEFAULT_CONCURRENCY,
25
+ signal?: AbortSignal,
21
26
  ): AsyncGenerator<O> {
22
27
  let done = false;
23
28
 
@@ -50,6 +55,9 @@ export async function* asyncIteratorMap<I, O>(
50
55
  };
51
56
 
52
57
  while (!done || executing.size > 0 || results.length > 0) {
58
+ if (signal?.aborted) {
59
+ throw new AbortError(c('Error').t`Operation aborted`);
60
+ }
53
61
  while (!done && executing.size < concurrency) {
54
62
  await pump();
55
63
  }
@@ -1,11 +1,23 @@
1
+ import { AbortError } from '../../errors';
1
2
  import { waitForCondition } from '../wait';
2
3
 
3
4
  export class DownloadController {
4
5
  private paused = false;
5
6
  public promise?: Promise<void>;
6
7
 
8
+ constructor(private signal?: AbortSignal) {
9
+ this.signal = signal;
10
+ }
11
+
7
12
  async waitWhilePaused(): Promise<void> {
8
- await waitForCondition(() => !this.paused);
13
+ try {
14
+ await waitForCondition(() => !this.paused, this.signal);
15
+ } catch (error) {
16
+ if (error instanceof AbortError) {
17
+ return;
18
+ }
19
+ throw error;
20
+ }
9
21
  }
10
22
 
11
23
  pause(): void {
@@ -78,7 +78,7 @@ describe('FileDownloader', () => {
78
78
  } as DecryptedRevision;
79
79
  });
80
80
 
81
- describe('writeToStream', () => {
81
+ describe('downloadToStream', () => {
82
82
  let onProgress: (downloadedBytes: number) => void;
83
83
  let onFinish: () => void;
84
84
 
@@ -89,7 +89,7 @@ describe('FileDownloader', () => {
89
89
  const verifySuccess = async (
90
90
  fileProgress: number = 6, // 3 blocks of length 1, 2, 3
91
91
  ) => {
92
- const controller = downloader.writeToStream(stream, onProgress);
92
+ const controller = downloader.downloadToStream(stream, onProgress);
93
93
  await controller.completion();
94
94
 
95
95
  expect(apiService.iterateRevisionBlocks).toHaveBeenCalledWith('revisionUid', undefined);
@@ -103,7 +103,7 @@ describe('FileDownloader', () => {
103
103
  };
104
104
 
105
105
  const verifyFailure = async (error: string, downloadedBytes: number | undefined) => {
106
- const controller = downloader.writeToStream(stream, onProgress);
106
+ const controller = downloader.downloadToStream(stream, onProgress);
107
107
 
108
108
  await expect(controller.completion()).rejects.toThrow(error);
109
109
 
@@ -156,9 +156,9 @@ describe('FileDownloader', () => {
156
156
  });
157
157
 
158
158
  it('should reject two download starts', async () => {
159
- downloader.writeToStream(stream, onProgress);
160
- expect(() => downloader.writeToStream(stream, onProgress)).toThrow('Download already started');
161
- expect(() => downloader.unsafeWriteToStream(stream, onProgress)).toThrow('Download already started');
159
+ downloader.downloadToStream(stream, onProgress);
160
+ expect(() => downloader.downloadToStream(stream, onProgress)).toThrow('Download already started');
161
+ expect(() => downloader.unsafeDownloadToStream(stream, onProgress)).toThrow('Download already started');
162
162
  });
163
163
 
164
164
  it('should start a download and write to the stream', async () => {
@@ -347,7 +347,7 @@ describe('FileDownloader', () => {
347
347
  });
348
348
  });
349
349
 
350
- describe('unsafeWriteToStream', () => {
350
+ describe('unsafeDownloadToStream', () => {
351
351
  let onProgress: (downloadedBytes: number) => void;
352
352
  let onFinish: () => void;
353
353
 
@@ -381,7 +381,7 @@ describe('FileDownloader', () => {
381
381
  });
382
382
 
383
383
  it('should skip verification steps', async () => {
384
- const controller = downloader.unsafeWriteToStream(stream, onProgress);
384
+ const controller = downloader.unsafeDownloadToStream(stream, onProgress);
385
385
  await controller.completion();
386
386
 
387
387
  expect(apiService.iterateRevisionBlocks).toHaveBeenCalledWith('revisionUid', undefined);
@@ -1,4 +1,7 @@
1
+ import { c } from 'ttag';
2
+
1
3
  import { PrivateKey, SessionKey, base64StringToUint8Array } from '../../crypto';
4
+ import { AbortError } from '../../errors';
2
5
  import { Logger } from '../../interface';
3
6
  import { LoggerWithPrefix } from '../../telemetry';
4
7
  import { APIHTTPError, HTTPErrorCode } from '../apiService';
@@ -48,7 +51,7 @@ export class FileDownloader {
48
51
  this.revision = revision;
49
52
  this.signal = signal;
50
53
  this.onFinish = onFinish;
51
- this.controller = new DownloadController();
54
+ this.controller = new DownloadController(this.signal);
52
55
  }
53
56
 
54
57
  getClaimedSizeInBytes(): number | undefined {
@@ -138,24 +141,24 @@ export class FileDownloader {
138
141
  }
139
142
  }
140
143
 
141
- writeToStream(stream: WritableStream, onProgress?: (downloadedBytes: number) => void): DownloadController {
144
+ downloadToStream(stream: WritableStream, onProgress?: (downloadedBytes: number) => void): DownloadController {
142
145
  if (this.controller.promise) {
143
146
  throw new Error(`Download already started`);
144
147
  }
145
- this.controller.promise = this.downloadToStream(stream, onProgress);
148
+ this.controller.promise = this.internalDownloadToStream(stream, onProgress);
146
149
  return this.controller;
147
150
  }
148
151
 
149
- unsafeWriteToStream(stream: WritableStream, onProgress?: (downloadedBytes: number) => void): DownloadController {
152
+ unsafeDownloadToStream(stream: WritableStream, onProgress?: (downloadedBytes: number) => void): DownloadController {
150
153
  if (this.controller.promise) {
151
154
  throw new Error(`Download already started`);
152
155
  }
153
156
  const ignoreIntegrityErrors = true;
154
- this.controller.promise = this.downloadToStream(stream, onProgress, ignoreIntegrityErrors);
157
+ this.controller.promise = this.internalDownloadToStream(stream, onProgress, ignoreIntegrityErrors);
155
158
  return this.controller;
156
159
  }
157
160
 
158
- private async downloadToStream(
161
+ private async internalDownloadToStream(
159
162
  stream: WritableStream,
160
163
  onProgress?: (downloadedBytes: number) => void,
161
164
  ignoreIntegrityErrors = false,
@@ -289,6 +292,10 @@ export class FileDownloader {
289
292
  logger.debug(`Decrypting`);
290
293
  decryptedBlock = await this.cryptoService.decryptBlock(encryptedBlock, cryptoKeys);
291
294
  } catch (error) {
295
+ if (this.signal?.aborted) {
296
+ throw new AbortError(c('Error').t`Operation aborted`);
297
+ }
298
+
292
299
  if (blockProgress !== 0) {
293
300
  onProgress?.(-blockProgress);
294
301
  blockProgress = 0;
@@ -1,7 +1,8 @@
1
+ import { NodeWithSameNameExistsValidationError, ValidationError } from '../../errors';
1
2
  import { MemberRole, NodeType } from '../../interface';
2
3
  import { getMockLogger } from '../../tests/logger';
3
4
  import { DriveAPIService, ErrorCode, InvalidRequirementsAPIError } from '../apiService';
4
- import { NodeAPIService } from './apiService';
5
+ import { NodeAPIService, groupNodeUidsByVolumeAndIteratePerBatch } from './apiService';
5
6
  import { NodeOutOfSyncError } from './errors';
6
7
 
7
8
  function generateAPIFileNode(linkOverrides = {}, overrides = {}) {
@@ -476,6 +477,44 @@ describe('nodeAPIService', () => {
476
477
  { uid: 'volumeId~nodeId2', ok: false, error: 'INSUFFICIENT_SCOPE' },
477
478
  ]);
478
479
  });
480
+
481
+ it('should trash nodes in batches', async () => {
482
+ // @ts-expect-error Mocking for testing purposes
483
+ apiMock.post = jest.fn(async (_, { LinkIDs }) =>
484
+ Promise.resolve({
485
+ Responses: LinkIDs.map((linkId: string) => ({
486
+ LinkID: linkId,
487
+ Response: {
488
+ Code: ErrorCode.OK,
489
+ },
490
+ })),
491
+ }),
492
+ );
493
+
494
+ const nodeUids = Array.from({ length: 250 }, (_, i) => `volumeId1~nodeId${i}`);
495
+ const nodeIds = nodeUids.map((uid) => uid.split('~')[1]);
496
+
497
+ const results = await Array.fromAsync(api.trashNodes(nodeUids));
498
+ expect(results).toHaveLength(nodeUids.length);
499
+ expect(results.every((result) => result.ok)).toBe(true);
500
+
501
+ expect(apiMock.post).toHaveBeenCalledTimes(3);
502
+ expect(apiMock.post).toHaveBeenCalledWith(
503
+ 'drive/v2/volumes/volumeId1/trash_multiple',
504
+ { LinkIDs: nodeIds.slice(0, 100) },
505
+ undefined,
506
+ );
507
+ expect(apiMock.post).toHaveBeenCalledWith(
508
+ 'drive/v2/volumes/volumeId1/trash_multiple',
509
+ { LinkIDs: nodeIds.slice(100, 200) },
510
+ undefined,
511
+ );
512
+ expect(apiMock.post).toHaveBeenCalledWith(
513
+ 'drive/v2/volumes/volumeId1/trash_multiple',
514
+ { LinkIDs: nodeIds.slice(200, 250) },
515
+ undefined,
516
+ );
517
+ });
479
518
  });
480
519
 
481
520
  describe('restoreNodes', () => {
@@ -517,17 +556,28 @@ describe('nodeAPIService', () => {
517
556
  ]);
518
557
  });
519
558
 
520
- it('should fail restoring from multiple volumes', async () => {
521
- try {
522
- await Array.fromAsync(api.restoreNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2']));
523
- throw new Error('Should have thrown');
524
- } catch (error: any) {
525
- expect(error.message).toEqual('Restoring items from multiple sections is not allowed');
526
- }
559
+ it('should restore nodes from multiple volumes', async () => {
560
+ // @ts-expect-error Mocking for testing purposes
561
+ apiMock.put = jest.fn(async (_, { LinkIDs }) =>
562
+ Promise.resolve({
563
+ Responses: LinkIDs.map((linkId: string) => ({
564
+ LinkID: linkId,
565
+ Response: {
566
+ Code: ErrorCode.OK,
567
+ },
568
+ })),
569
+ }),
570
+ );
571
+
572
+ const result = await Array.fromAsync(api.restoreNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2']));
573
+ expect(result).toEqual([
574
+ { uid: 'volumeId1~nodeId1', ok: true },
575
+ { uid: 'volumeId2~nodeId2', ok: true },
576
+ ]);
527
577
  });
528
578
  });
529
579
 
530
- describe('deleteNOdes', () => {
580
+ describe('deleteNodes', () => {
531
581
  it('should delete nodes', async () => {
532
582
  // @ts-expect-error Mocking for testing purposes
533
583
  apiMock.post = jest.fn(async () =>
@@ -557,12 +607,86 @@ describe('nodeAPIService', () => {
557
607
  ]);
558
608
  });
559
609
 
560
- it('should fail deleting nodes from multiple volumes', async () => {
610
+ it('should delete nodes from multiple volumes', async () => {
611
+ // @ts-expect-error Mocking for testing purposes
612
+ apiMock.post = jest.fn(async (_, { LinkIDs }) =>
613
+ Promise.resolve({
614
+ Responses: LinkIDs.map((linkId: string) => ({
615
+ LinkID: linkId,
616
+ Response: {
617
+ Code: ErrorCode.OK,
618
+ },
619
+ })),
620
+ }),
621
+ );
622
+
623
+ const result = await Array.fromAsync(api.deleteNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2']));
624
+ expect(result).toEqual([
625
+ { uid: 'volumeId1~nodeId1', ok: true },
626
+ { uid: 'volumeId2~nodeId2', ok: true },
627
+ ]);
628
+ });
629
+ });
630
+
631
+ describe('createFolder', () => {
632
+ it('should create folder', async () => {
633
+ apiMock.post = jest.fn().mockResolvedValue({
634
+ Code: ErrorCode.OK,
635
+ Folder: {
636
+ ID: 'newNodeId',
637
+ },
638
+ });
639
+
640
+ const result = await api.createFolder('volumeId~parentNodeId', {
641
+ armoredKey: 'armoredKey',
642
+ armoredHashKey: 'armoredHashKey',
643
+ armoredNodePassphrase: 'armoredNodePassphrase',
644
+ armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
645
+ signatureEmail: 'signatureEmail',
646
+ encryptedName: 'encryptedName',
647
+ hash: 'hash',
648
+ armoredExtendedAttributes: 'armoredExtendedAttributes',
649
+ });
650
+
651
+ expect(result).toEqual('volumeId~newNodeId');
652
+ expect(apiMock.post).toHaveBeenCalledWith('drive/v2/volumes/volumeId/folders', {
653
+ ParentLinkID: 'parentNodeId',
654
+ NodeKey: 'armoredKey',
655
+ NodeHashKey: 'armoredHashKey',
656
+ NodePassphrase: 'armoredNodePassphrase',
657
+ NodePassphraseSignature: 'armoredNodePassphraseSignature',
658
+ SignatureEmail: 'signatureEmail',
659
+ Name: 'encryptedName',
660
+ Hash: 'hash',
661
+ XAttr: 'armoredExtendedAttributes',
662
+ });
663
+ });
664
+
665
+ it('should throw NodeWithSameNameExistsValidationError if node already exists', async () => {
666
+ apiMock.post = jest.fn().mockRejectedValue(
667
+ new ValidationError('Node already exists', ErrorCode.ALREADY_EXISTS, {
668
+ ConflictLinkID: 'existingNodeId',
669
+ }),
670
+ );
671
+
561
672
  try {
562
- await Array.fromAsync(api.deleteNodes(['volumeId1~nodeId1', 'volumeId2~nodeId2']));
563
- throw new Error('Should have thrown');
564
- } catch (error: any) {
565
- expect(error.message).toEqual('Deleting items from multiple sections is not allowed');
673
+ await api.createFolder('volumeId~parentNodeId', {
674
+ armoredKey: 'armoredKey',
675
+ armoredHashKey: 'armoredHashKey',
676
+ armoredNodePassphrase: 'armoredNodePassphrase',
677
+ armoredNodePassphraseSignature: 'armoredNodePassphraseSignature',
678
+ signatureEmail: 'signatureEmail',
679
+ encryptedName: 'encryptedName',
680
+ hash: 'hash',
681
+ armoredExtendedAttributes: 'armoredExtendedAttributes',
682
+ });
683
+ expect(false).toBeTruthy();
684
+ } catch (error: unknown) {
685
+ expect(error).toBeInstanceOf(NodeWithSameNameExistsValidationError);
686
+ if (error instanceof NodeWithSameNameExistsValidationError) {
687
+ expect(error.code).toEqual(ErrorCode.ALREADY_EXISTS);
688
+ expect(error.existingNodeUid).toEqual('volumeId~existingNodeId');
689
+ }
566
690
  }
567
691
  });
568
692
  });
@@ -600,3 +724,126 @@ describe('nodeAPIService', () => {
600
724
  });
601
725
  });
602
726
  });
727
+
728
+ describe('groupNodeUidsByVolumeAndIteratePerBatch', () => {
729
+ it('should handle empty array', () => {
730
+ const result = Array.from(groupNodeUidsByVolumeAndIteratePerBatch([]));
731
+ expect(result).toEqual([]);
732
+ });
733
+
734
+ it('should handle single volume with nodes that fit in one batch', () => {
735
+ const nodeUids = ['volumeId1~nodeId1', 'volumeId1~nodeId2', 'volumeId1~nodeId3'];
736
+
737
+ const result = Array.from(groupNodeUidsByVolumeAndIteratePerBatch(nodeUids));
738
+
739
+ expect(result).toEqual([
740
+ {
741
+ volumeId: 'volumeId1',
742
+ batchNodeIds: ['nodeId1', 'nodeId2', 'nodeId3'],
743
+ batchNodeUids: ['volumeId1~nodeId1', 'volumeId1~nodeId2', 'volumeId1~nodeId3'],
744
+ },
745
+ ]);
746
+ });
747
+
748
+ it('should handle single volume with nodes that require multiple batches', () => {
749
+ // Create 250 node UIDs to test batching (API_NODES_BATCH_SIZE = 100)
750
+ const nodeUids = Array.from({ length: 250 }, (_, i) => `volumeId1~nodeId${i}`);
751
+
752
+ const result = Array.from(groupNodeUidsByVolumeAndIteratePerBatch(nodeUids));
753
+
754
+ expect(result).toHaveLength(3); // 100 + 100 + 50
755
+
756
+ // First batch
757
+ expect(result[0]).toEqual({
758
+ volumeId: 'volumeId1',
759
+ batchNodeIds: Array.from({ length: 100 }, (_, i) => `nodeId${i}`),
760
+ batchNodeUids: Array.from({ length: 100 }, (_, i) => `volumeId1~nodeId${i}`),
761
+ });
762
+
763
+ // Second batch
764
+ expect(result[1]).toEqual({
765
+ volumeId: 'volumeId1',
766
+ batchNodeIds: Array.from({ length: 100 }, (_, i) => `nodeId${i + 100}`),
767
+ batchNodeUids: Array.from({ length: 100 }, (_, i) => `volumeId1~nodeId${i + 100}`),
768
+ });
769
+
770
+ // Third batch
771
+ expect(result[2]).toEqual({
772
+ volumeId: 'volumeId1',
773
+ batchNodeIds: Array.from({ length: 50 }, (_, i) => `nodeId${i + 200}`),
774
+ batchNodeUids: Array.from({ length: 50 }, (_, i) => `volumeId1~nodeId${i + 200}`),
775
+ });
776
+ });
777
+
778
+ it('should handle multiple volumes with nodes distributed across them', () => {
779
+ const nodeUids = [
780
+ 'volumeId1~nodeId1',
781
+ 'volumeId2~nodeId2',
782
+ 'volumeId1~nodeId3',
783
+ 'volumeId3~nodeId4',
784
+ 'volumeId2~nodeId5',
785
+ ];
786
+
787
+ const result = Array.from(groupNodeUidsByVolumeAndIteratePerBatch(nodeUids));
788
+
789
+ expect(result).toHaveLength(3); // One batch per volume
790
+
791
+ // Results should be grouped by volume
792
+ const volumeId1Batch = result.find((batch) => batch.volumeId === 'volumeId1');
793
+ const volumeId2Batch = result.find((batch) => batch.volumeId === 'volumeId2');
794
+ const volumeId3Batch = result.find((batch) => batch.volumeId === 'volumeId3');
795
+
796
+ expect(volumeId1Batch).toEqual({
797
+ volumeId: 'volumeId1',
798
+ batchNodeIds: ['nodeId1', 'nodeId3'],
799
+ batchNodeUids: ['volumeId1~nodeId1', 'volumeId1~nodeId3'],
800
+ });
801
+
802
+ expect(volumeId2Batch).toEqual({
803
+ volumeId: 'volumeId2',
804
+ batchNodeIds: ['nodeId2', 'nodeId5'],
805
+ batchNodeUids: ['volumeId2~nodeId2', 'volumeId2~nodeId5'],
806
+ });
807
+
808
+ expect(volumeId3Batch).toEqual({
809
+ volumeId: 'volumeId3',
810
+ batchNodeIds: ['nodeId4'],
811
+ batchNodeUids: ['volumeId3~nodeId4'],
812
+ });
813
+ });
814
+
815
+ it('should handle multiple volumes where some require multiple batches', () => {
816
+ // Volume 1: 150 nodes (2 batches)
817
+ // Volume 2: 50 nodes (1 batch)
818
+ // Volume 3: 200 nodes (2 batches)
819
+ const volume1Nodes = Array.from({ length: 150 }, (_, i) => `volumeId1~nodeId${i}`);
820
+ const volume2Nodes = Array.from({ length: 50 }, (_, i) => `volumeId2~nodeId${i}`);
821
+ const volume3Nodes = Array.from({ length: 200 }, (_, i) => `volumeId3~nodeId${i}`);
822
+
823
+ const nodeUids = [...volume1Nodes, ...volume2Nodes, ...volume3Nodes];
824
+
825
+ const result = Array.from(groupNodeUidsByVolumeAndIteratePerBatch(nodeUids));
826
+
827
+ expect(result).toHaveLength(5); // 2 + 1 + 2 batches
828
+
829
+ // Group results by volume
830
+ const volume1Batches = result.filter((batch) => batch.volumeId === 'volumeId1');
831
+ const volume2Batches = result.filter((batch) => batch.volumeId === 'volumeId2');
832
+ const volume3Batches = result.filter((batch) => batch.volumeId === 'volumeId3');
833
+
834
+ expect(volume1Batches).toHaveLength(2);
835
+ expect(volume2Batches).toHaveLength(1);
836
+ expect(volume3Batches).toHaveLength(2);
837
+
838
+ // Verify volume 1 batches
839
+ expect(volume1Batches[0].batchNodeIds).toHaveLength(100);
840
+ expect(volume1Batches[1].batchNodeIds).toHaveLength(50);
841
+
842
+ // Verify volume 2 batch
843
+ expect(volume2Batches[0].batchNodeIds).toHaveLength(50);
844
+
845
+ // Verify volume 3 batches
846
+ expect(volume3Batches[0].batchNodeIds).toHaveLength(100);
847
+ expect(volume3Batches[1].batchNodeIds).toHaveLength(100);
848
+ });
849
+ });