@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.
- package/dist/diagnostic/{sdkDiagnosticFull.d.ts → diagnostic.d.ts} +5 -4
- package/dist/diagnostic/{sdkDiagnosticFull.js → diagnostic.js} +13 -10
- package/dist/diagnostic/diagnostic.js.map +1 -0
- package/dist/diagnostic/index.js +2 -4
- package/dist/diagnostic/index.js.map +1 -1
- package/dist/diagnostic/interface.d.ts +22 -1
- package/dist/diagnostic/sdkDiagnostic.d.ts +3 -2
- package/dist/diagnostic/sdkDiagnostic.js +80 -8
- package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
- package/dist/interface/download.d.ts +4 -4
- package/dist/interface/index.d.ts +1 -1
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +9 -0
- package/dist/interface/telemetry.d.ts +4 -1
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/interface/upload.d.ts +6 -3
- package/dist/internal/apiService/apiService.d.ts +3 -0
- package/dist/internal/apiService/apiService.js +25 -2
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +38 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +2595 -2397
- package/dist/internal/apiService/errors.js +3 -0
- package/dist/internal/apiService/errors.js.map +1 -1
- package/dist/internal/apiService/errors.test.js +15 -7
- package/dist/internal/apiService/errors.test.js.map +1 -1
- package/dist/internal/asyncIteratorMap.d.ts +1 -1
- package/dist/internal/asyncIteratorMap.js +6 -1
- package/dist/internal/asyncIteratorMap.js.map +1 -1
- package/dist/internal/asyncIteratorMap.test.js +9 -0
- package/dist/internal/asyncIteratorMap.test.js.map +1 -1
- package/dist/internal/download/controller.d.ts +2 -0
- package/dist/internal/download/controller.js +15 -1
- package/dist/internal/download/controller.js.map +1 -1
- package/dist/internal/download/fileDownloader.d.ts +3 -3
- package/dist/internal/download/fileDownloader.js +11 -6
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +8 -8
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/nodes/apiService.d.ts +6 -1
- package/dist/internal/nodes/apiService.js +71 -44
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +204 -15
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/debouncer.d.ts +24 -0
- package/dist/internal/nodes/debouncer.js +92 -0
- package/dist/internal/nodes/debouncer.js.map +1 -0
- package/dist/internal/nodes/debouncer.test.d.ts +1 -0
- package/dist/internal/nodes/debouncer.test.js +108 -0
- package/dist/internal/nodes/debouncer.test.js.map +1 -0
- package/dist/internal/nodes/extendedAttributes.js +2 -2
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/index.js +1 -1
- package/dist/internal/nodes/index.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.d.ts +6 -4
- package/dist/internal/nodes/nodesAccess.js +29 -9
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesAccess.test.js +19 -7
- package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.d.ts +2 -2
- package/dist/internal/nodes/nodesManagement.js +5 -3
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.test.js +3 -1
- package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
- package/dist/internal/photos/apiService.js +9 -20
- package/dist/internal/photos/apiService.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +2 -1
- package/dist/internal/photos/upload.js +9 -3
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharing/apiService.d.ts +1 -1
- package/dist/internal/sharing/apiService.js +2 -2
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/sharingManagement.d.ts +4 -1
- package/dist/internal/sharing/sharingManagement.js +7 -4
- package/dist/internal/sharing/sharingManagement.js.map +1 -1
- package/dist/internal/sharingPublic/apiService.d.ts +8 -10
- package/dist/internal/sharingPublic/apiService.js +9 -125
- package/dist/internal/sharingPublic/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
- package/dist/internal/sharingPublic/{cryptoService.js → cryptoReporter.js} +3 -16
- package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
- package/dist/internal/sharingPublic/index.d.ts +22 -4
- package/dist/internal/sharingPublic/index.js +37 -12
- package/dist/internal/sharingPublic/index.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +18 -0
- package/dist/internal/sharingPublic/nodes.js +46 -0
- package/dist/internal/sharingPublic/nodes.js.map +1 -0
- package/dist/internal/sharingPublic/session/apiService.d.ts +7 -5
- package/dist/internal/sharingPublic/session/apiService.js +25 -4
- package/dist/internal/sharingPublic/session/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/session/interface.d.ts +17 -0
- package/dist/internal/sharingPublic/session/manager.d.ts +12 -4
- package/dist/internal/sharingPublic/session/manager.js +14 -4
- package/dist/internal/sharingPublic/session/manager.js.map +1 -1
- package/dist/internal/sharingPublic/session/session.d.ts +7 -4
- package/dist/internal/sharingPublic/session/session.js +7 -3
- package/dist/internal/sharingPublic/session/session.js.map +1 -1
- package/dist/internal/sharingPublic/session/url.test.js +3 -3
- package/dist/internal/sharingPublic/shares.d.ts +27 -0
- package/dist/internal/sharingPublic/shares.js +46 -0
- package/dist/internal/sharingPublic/shares.js.map +1 -0
- package/dist/internal/upload/apiService.js +10 -1
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/controller.d.ts +11 -3
- package/dist/internal/upload/controller.js +16 -2
- package/dist/internal/upload/controller.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +6 -3
- package/dist/internal/upload/fileUploader.js +4 -4
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/fileUploader.test.js +23 -11
- package/dist/internal/upload/fileUploader.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.d.ts +9 -4
- package/dist/internal/upload/streamUploader.js +67 -20
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +43 -13
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/protonDriveClient.d.ts +11 -6
- package/dist/protonDriveClient.js +11 -10
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/protonDrivePublicLinkClient.d.ts +34 -6
- package/dist/protonDrivePublicLinkClient.js +52 -9
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/tests/telemetry.d.ts +4 -2
- package/dist/tests/telemetry.js +3 -1
- package/dist/tests/telemetry.js.map +1 -1
- package/dist/transformers.d.ts +3 -2
- package/dist/transformers.js +6 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/diagnostic/{sdkDiagnosticFull.ts → diagnostic.ts} +10 -6
- package/src/diagnostic/index.ts +3 -5
- package/src/diagnostic/interface.ts +39 -0
- package/src/diagnostic/sdkDiagnostic.ts +111 -10
- package/src/interface/download.ts +4 -4
- package/src/interface/index.ts +1 -0
- package/src/interface/nodes.ts +3 -0
- package/src/interface/telemetry.ts +5 -0
- package/src/interface/upload.ts +3 -3
- package/src/internal/apiService/apiService.test.ts +50 -0
- package/src/internal/apiService/apiService.ts +33 -2
- package/src/internal/apiService/driveTypes.ts +2713 -2561
- package/src/internal/apiService/errors.test.ts +10 -0
- package/src/internal/apiService/errors.ts +5 -1
- package/src/internal/asyncIteratorMap.test.ts +12 -0
- package/src/internal/asyncIteratorMap.ts +8 -0
- package/src/internal/download/controller.ts +13 -1
- package/src/internal/download/fileDownloader.test.ts +8 -8
- package/src/internal/download/fileDownloader.ts +13 -6
- package/src/internal/nodes/apiService.test.ts +261 -14
- package/src/internal/nodes/apiService.ts +99 -65
- package/src/internal/nodes/debouncer.test.ts +141 -0
- package/src/internal/nodes/debouncer.ts +109 -0
- package/src/internal/nodes/extendedAttributes.ts +2 -2
- package/src/internal/nodes/index.ts +1 -8
- package/src/internal/nodes/nodesAccess.test.ts +19 -7
- package/src/internal/nodes/nodesAccess.ts +44 -9
- package/src/internal/nodes/nodesManagement.test.ts +3 -1
- package/src/internal/nodes/nodesManagement.ts +11 -5
- package/src/internal/photos/apiService.ts +12 -29
- package/src/internal/photos/upload.ts +22 -1
- package/src/internal/sharing/apiService.ts +2 -2
- package/src/internal/sharing/sharingManagement.ts +7 -4
- package/src/internal/sharingPublic/apiService.ts +23 -160
- package/src/internal/sharingPublic/{cryptoService.ts → cryptoReporter.ts} +2 -27
- package/src/internal/sharingPublic/index.ts +76 -13
- package/src/internal/sharingPublic/nodes.ts +59 -0
- package/src/internal/sharingPublic/session/apiService.ts +32 -10
- package/src/internal/sharingPublic/session/interface.ts +20 -0
- package/src/internal/sharingPublic/session/manager.ts +31 -8
- package/src/internal/sharingPublic/session/session.ts +12 -7
- package/src/internal/sharingPublic/session/url.test.ts +3 -3
- package/src/internal/sharingPublic/shares.ts +50 -0
- package/src/internal/upload/apiService.ts +12 -1
- package/src/internal/upload/controller.ts +16 -4
- package/src/internal/upload/fileUploader.test.ts +25 -11
- package/src/internal/upload/fileUploader.ts +6 -5
- package/src/internal/upload/streamUploader.test.ts +56 -12
- package/src/internal/upload/streamUploader.ts +78 -20
- package/src/protonDriveClient.ts +29 -11
- package/src/protonDrivePublicLinkClient.ts +100 -16
- package/src/tests/telemetry.ts +6 -3
- package/src/transformers.ts +8 -0
- package/dist/diagnostic/sdkDiagnosticFull.js.map +0 -1
- package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -19
- package/dist/internal/sharingPublic/cryptoCache.js +0 -72
- package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
- package/dist/internal/sharingPublic/cryptoService.d.ts +0 -9
- package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
- package/dist/internal/sharingPublic/interface.d.ts +0 -6
- package/dist/internal/sharingPublic/interface.js +0 -3
- package/dist/internal/sharingPublic/interface.js.map +0 -1
- package/dist/internal/sharingPublic/manager.d.ts +0 -19
- package/dist/internal/sharingPublic/manager.js +0 -81
- package/dist/internal/sharingPublic/manager.js.map +0 -1
- package/src/internal/sharingPublic/cryptoCache.ts +0 -79
- package/src/internal/sharingPublic/interface.ts +0 -14
- 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
|
-
|
|
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('
|
|
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.
|
|
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.
|
|
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.
|
|
160
|
-
expect(() => downloader.
|
|
161
|
-
expect(() => downloader.
|
|
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('
|
|
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.
|
|
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
|
-
|
|
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.
|
|
148
|
+
this.controller.promise = this.internalDownloadToStream(stream, onProgress);
|
|
146
149
|
return this.controller;
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
|
|
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.
|
|
157
|
+
this.controller.promise = this.internalDownloadToStream(stream, onProgress, ignoreIntegrityErrors);
|
|
155
158
|
return this.controller;
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
private async
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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('
|
|
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
|
|
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
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
});
|