diff --git a/package.json b/package.json index 00bfee3..2d8bad9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@internxt/sdk", "author": "Internxt ", - "version": "1.15.1", + "version": "1.15.2", "description": "An sdk for interacting with Internxt's services", "repository": { "type": "git", diff --git a/src/network/index.ts b/src/network/index.ts index b8ae811..1a39d93 100644 --- a/src/network/index.ts +++ b/src/network/index.ts @@ -19,14 +19,6 @@ const uuidValidate = (str: string): boolean => UUID_REGEX.test(str); export * from './types'; -export class DuplicatedIndexesError extends Error { - constructor() { - super('Duplicated indexes found'); - - Object.setPrototypeOf(this, DuplicatedIndexesError.prototype); - } -} - export class InvalidFileIndexError extends Error { constructor() { super('Invalid file index'); @@ -35,14 +27,6 @@ export class InvalidFileIndexError extends Error { } } -export class InvalidUploadIndexError extends Error { - constructor() { - super('Invalid upload index'); - - Object.setPrototypeOf(this, InvalidUploadIndexError.prototype); - } -} - export class InvalidUploadSizeError extends Error { constructor() { super('Invalid size'); @@ -89,21 +73,13 @@ export class Network { return this.auth; } - startUpload(bucketId: string, payload: StartUploadPayload, parts = 1): Promise { - let totalSize = 0; - - for (const { index, size } of payload.uploads) { - if (index < 0) { - throw new InvalidUploadIndexError(); - } - if (size < 0) { - throw new InvalidUploadSizeError(); - } - totalSize += size; + async startUpload(bucketId: string, fileSize: number, signal?: AbortSignal, parts = 1): Promise { + if (fileSize <= 0) { + throw new InvalidUploadSizeError(); } const MB100 = 100 * 1024 * 1024; - if (totalSize < MB100 && parts > 1) { + if (fileSize < MB100 && parts > 1) { throw new FileTooSmallForMultipartError(); } @@ -111,25 +87,14 @@ export class Network { throw new InvalidMultipartValueError(); } - const uploadIndexesWithoutDuplicates = new Set(payload.uploads.map((upload) => upload.index)); - - if (uploadIndexesWithoutDuplicates.size < payload.uploads.length) { - throw new DuplicatedIndexesError(); - } - - return Network.startUpload( - bucketId, - payload, - { - client: this.client, - appDetails: this.appDetails, - auth: this.auth, - }, - parts, - ); + return await this.startUploadRequest(bucketId, { uploads: [{ index: 0, size: fileSize }] }, signal, parts); } - finishUpload(bucketId: string, payload: FinishUploadPayload): Promise { + async finishUpload( + bucketId: string, + payload: FinishUploadPayload, + signal?: AbortSignal, + ): Promise { const { index, shards } = payload; if (!isHexString(index) || index.length !== 64) { throw new InvalidFileIndexError(); @@ -141,14 +106,14 @@ export class Network { } } - return Network.finishUpload(bucketId, payload, { - client: this.client, - appDetails: this.appDetails, - auth: this.auth, - }); + return await this.finishUploadRequest(bucketId, payload, signal); } - finishMultipartUpload(bucketId: string, payload: FinishMultipartUploadPayload): Promise { + async finishMultipartUpload( + bucketId: string, + payload: FinishMultipartUploadPayload, + signal?: AbortSignal, + ): Promise { const { index, shards } = payload; if (!isHexString(index) || index.length !== 64) { throw new InvalidFileIndexError(); @@ -167,15 +132,11 @@ export class Network { } } - return Network.finishUpload(bucketId, payload, { - client: this.client, - appDetails: this.appDetails, - auth: this.auth, - }); + return await this.finishUploadRequest(bucketId, payload, signal); } - getDownloadLinks(bucketId: string, fileId: string, token?: string): Promise { - return Network.getDownloadLinks( + async getDownloadLinks(bucketId: string, fileId: string, token?: string): Promise { + return await Network.getDownloadLinks( bucketId, fileId, { @@ -200,17 +161,13 @@ export class Network { * @param bucketId * @param uploads */ - static startUpload( - bucketId: string, - payload: StartUploadPayload, - { client, appDetails, auth }: NetworkRequestConfig, - parts = 1, - ) { - const headers = Network.headersWithBasicAuth(appDetails, auth); - return client.post( + async startUploadRequest(bucketId: string, payload: StartUploadPayload, signal?: AbortSignal, parts = 1) { + const headers = Network.headersWithBasicAuth(this.appDetails, this.auth); + return await this.client.post( `/v2/buckets/${bucketId}/files/start?multiparts=${parts}`, payload, headers, + signal, ); } @@ -220,13 +177,18 @@ export class Network { * @param index * @param shards */ - private static finishUpload( + private async finishUploadRequest( bucketId: string, payload: FinishUploadPayload | FinishMultipartUploadPayload, - { client, appDetails, auth }: NetworkRequestConfig, + signal?: AbortSignal, ) { - const headers = Network.headersWithBasicAuth(appDetails, auth); - return client.post(`/v2/buckets/${bucketId}/files/finish`, payload, headers); + const headers = Network.headersWithBasicAuth(this.appDetails, this.auth); + return await this.client.post( + `/v2/buckets/${bucketId}/files/finish`, + payload, + headers, + signal, + ); } /** @@ -234,7 +196,7 @@ export class Network { * @param bucketId * @param file */ - private static getDownloadLinks( + private static async getDownloadLinks( bucketId: string, fileId: string, { client, appDetails, auth }: NetworkRequestConfig, @@ -245,7 +207,7 @@ export class Network { ? Network.headersWithAuthToken(appDetails, token) : Network.headersWithBasicAuth(appDetails, auth); - return client.get(`/buckets/${bucketId}/files/${fileId}/info`, { + return await client.get(`/buckets/${bucketId}/files/${fileId}/info`, { ...headers, 'x-api-version': '2', }); @@ -256,9 +218,13 @@ export class Network { * @param bucketId * @param file */ - private static deleteFile(bucketId: string, fileId: string, { client, appDetails, auth }: NetworkRequestConfig) { + private static async deleteFile( + bucketId: string, + fileId: string, + { client, appDetails, auth }: NetworkRequestConfig, + ) { const headers = Network.headersWithBasicAuth(appDetails, auth); - return client.delete(`/v2/buckets/${bucketId}/files/${fileId}`, headers); + return await client.delete(`/v2/buckets/${bucketId}/files/${fileId}`, headers); } /** diff --git a/src/network/upload.ts b/src/network/upload.ts index 41d8a9f..69348e1 100644 --- a/src/network/upload.ts +++ b/src/network/upload.ts @@ -17,6 +17,7 @@ export async function uploadFile( fileSize: number, encryptFile: EncryptFileFunction, uploadFile: UploadFileFunction, + signal?: AbortSignal, ): Promise { let index: BinaryData; let iv: BinaryData; @@ -33,14 +34,7 @@ export async function uploadFile( iv = index.slice(0, 16); key = await crypto.generateFileKey(mnemonic, bucketId, index); - const { uploads } = await network.startUpload(bucketId, { - uploads: [ - { - index: 0, - size: fileSize, - }, - ], - }); + const { uploads } = await network.startUpload(bucketId, fileSize, signal); const [{ url, uuid }] = uploads; @@ -56,7 +50,7 @@ export async function uploadFile( shards: [{ hash, uuid }], }; - const finishUploadResponse = await network.finishUpload(bucketId, finishUploadPayload); + const finishUploadResponse = await network.finishUpload(bucketId, finishUploadPayload, signal); return finishUploadResponse.id; } catch (err) { @@ -86,6 +80,7 @@ export async function uploadMultipartFile( fileSize: number, encryptFile: EncryptFileFunction, uploadMultiparts: UploadFileMultipartFunction, + signal?: AbortSignal, parts = 1, ): Promise { const mnemonicIsValid = crypto.validateMnemonic(mnemonic); @@ -98,18 +93,7 @@ export async function uploadMultipartFile( const iv = index.slice(0, 16); const key = await crypto.generateFileKey(mnemonic, bucketId, index); - const { uploads } = await network.startUpload( - bucketId, - { - uploads: [ - { - index: 0, - size: fileSize, - }, - ], - }, - parts, - ); + const { uploads } = await network.startUpload(bucketId, fileSize, signal, parts); const [{ urls, uuid, UploadId }] = uploads; @@ -128,7 +112,7 @@ export async function uploadMultipartFile( shards: [{ hash, uuid, UploadId, parts: uploadedPartsReference }], }; - const finishUploadResponse = await network.finishUpload(bucketId, finishUploadPayload); + const finishUploadResponse = await network.finishUpload(bucketId, finishUploadPayload, signal); return finishUploadResponse.id; } diff --git a/src/shared/http/client.ts b/src/shared/http/client.ts index 1d6605a..de070ee 100644 --- a/src/shared/http/client.ts +++ b/src/shared/http/client.ts @@ -85,8 +85,8 @@ export class HttpClient { * @param url * @param headers */ - public get(url: URL, headers: Headers): Promise { - return this.execute(() => this.axios.get(url, { headers })); + public async get(url: URL, headers: Headers): Promise { + return await this.execute(() => this.axios.get(url, { headers })); } /** @@ -95,8 +95,8 @@ export class HttpClient { * @param params * @param headers */ - public getWithParams(url: URL, params: Parameters, headers: Headers): Promise { - return this.execute(() => this.axios.get(url, { params, headers })); + public async getWithParams(url: URL, params: Parameters, headers: Headers): Promise { + return await this.execute(() => this.axios.get(url, { params, headers })); } /** @@ -129,8 +129,8 @@ export class HttpClient { * @param params * @param headers */ - public post(url: URL, params: Parameters, headers: Headers): Promise { - return this.execute(() => this.axios.post(url, params, { headers })); + public async post(url: URL, params: Parameters, headers: Headers, signal?: AbortSignal): Promise { + return await this.execute(() => this.axios.post(url, params, { headers, signal })); } /** @@ -139,8 +139,8 @@ export class HttpClient { * @param params * @param headers */ - public postForm(url: URL, params: Parameters, headers: Headers): Promise { - return this.execute(() => this.axios.postForm(url, params, { headers })); + public async postForm(url: URL, params: Parameters, headers: Headers): Promise { + return await this.execute(() => this.axios.postForm(url, params, { headers })); } /** @@ -175,8 +175,8 @@ export class HttpClient { * @param params * @param headers */ - public patch(url: URL, params: Parameters, headers: Headers): Promise { - return this.execute(() => this.axios.patch(url, params, { headers })); + public async patch(url: URL, params: Parameters, headers: Headers): Promise { + return await this.execute(() => this.axios.patch(url, params, { headers })); } /** @@ -185,8 +185,8 @@ export class HttpClient { * @param params * @param headers */ - public put(url: URL, params: Parameters, headers: Headers): Promise { - return this.execute(() => this.axios.put(url, params, { headers })); + public async put(url: URL, params: Parameters, headers: Headers): Promise { + return await this.execute(() => this.axios.put(url, params, { headers })); } /** @@ -195,8 +195,8 @@ export class HttpClient { * @param params * @param headers */ - public putForm(url: URL, params: Parameters, headers: Headers): Promise { - return this.execute(() => this.axios.putForm(url, params, { headers })); + public async putForm(url: URL, params: Parameters, headers: Headers): Promise { + return await this.execute(() => this.axios.putForm(url, params, { headers })); } /** @@ -205,8 +205,8 @@ export class HttpClient { * @param headers * @param params */ - public delete(url: URL, headers: Headers, params?: Parameters): Promise { - return this.execute(() => this.axios.delete(url, { headers, data: params })); + public async delete(url: URL, headers: Headers, params?: Parameters): Promise { + return await this.execute(() => this.axios.delete(url, { headers, data: params })); } /** diff --git a/test/network/network.test.ts b/test/network/network.test.ts index 535c9c4..f0f811e 100644 --- a/test/network/network.test.ts +++ b/test/network/network.test.ts @@ -2,13 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { HttpClient } from '../../src/shared/http/client'; import { AppDetails } from '../../src/shared'; -import { - DuplicatedIndexesError, - InvalidFileIndexError, - InvalidUploadIndexError, - InvalidUploadSizeError, - Network, -} from '../../src/network/index'; +import { InvalidFileIndexError, Network, InvalidUploadSizeError } from '../../src/network/index'; import { headersWithBasicAuth } from '../../src/shared/headers/index'; import { StartUploadPayload, @@ -23,12 +17,11 @@ const validHex = '2e1884c34f174110ca6e324e7b745754b3d6356b53ef9f594b960fd5340500 const url = 'http://internxt.com'; -const invalidIndex = -1; -const validIndex = 0; - const invalidSize = -33; const validSize = 1; +const abortSignal = new AbortController().signal; + describe('network ', () => { beforeEach(() => { vi.restoreAllMocks(); @@ -40,66 +33,24 @@ describe('network ', () => { const idBucket = 'id-bucket'; try { - await client.startUpload(idBucket, { - uploads: [{ index: validIndex, size: invalidSize }], - }); + await client.startUpload(idBucket, invalidSize, abortSignal); fail('Expected function to throw an error, but it did not.'); } catch (err) { expect(err).toBeInstanceOf(InvalidUploadSizeError); } }); - it('Should throw if an invalid index is provided', async () => { - const { client } = clientAndHeadersWithBasicAuth(); - const idBucket = 'id-bucket'; - - try { - await client.startUpload(idBucket, { - uploads: [{ index: invalidIndex, size: validSize }], - }); - fail('Expected function to throw an error, but it did not.'); - } catch (err) { - expect(err).toBeInstanceOf(InvalidUploadIndexError); - } - }); - - it('Should throw if an index is duplicated', async () => { - const { client } = clientAndHeadersWithBasicAuth(); - const idBucket = 'id-bucket'; - - try { - await client.startUpload(idBucket, { - uploads: [ - { index: validIndex, size: validSize }, - { index: validIndex, size: validSize }, - ], - }); - fail('Expected function to throw an error, but it did not.'); - } catch (err) { - expect(err).toBeInstanceOf(DuplicatedIndexesError); - } - }); - it('Should work properly if the input is valid', async () => { const { client } = clientAndHeadersWithBasicAuth(); const idBucket = 'id-bucket'; - const uploads = [ - { index: validIndex, size: validSize }, - { index: validIndex + 1, size: validSize }, - ]; const expected = { - uploads: uploads.map((u) => ({ - index: u.index, - uuid: validUUID, - url, - urls: null, - })), + uploads: [{ index: 0, uuid: validUUID, url, urls: null }], }; - vi.spyOn(Network, 'startUpload').mockResolvedValue(expected); + vi.spyOn(client, 'startUploadRequest').mockResolvedValue(expected); - const received = await client.startUpload(idBucket, { uploads }); + const received = await client.startUpload(idBucket, validSize, abortSignal); expect(received).toStrictEqual(expected); }); @@ -120,7 +71,7 @@ describe('network ', () => { }; try { - const promise = await client.finishUpload(idBucket, invalidIndexPayload); + const promise = await client.finishUpload(idBucket, invalidIndexPayload, abortSignal); expect(promise).toBeUndefined(); } catch (err) { expect(err).toBeInstanceOf(InvalidFileIndexError); @@ -139,7 +90,7 @@ describe('network ', () => { // Act try { - const promise = await client.finishUpload(idBucket, invalidUUIDPayload); + const promise = await client.finishUpload(idBucket, invalidUUIDPayload, abortSignal); expect(promise).toBeUndefined(); } catch (err) { // Assert @@ -152,17 +103,18 @@ describe('network ', () => { // Arrange const { client, headers } = clientAndHeadersWithBasicAuth(); const idBucket = 'id-bucket'; + const size = 40; const validStartUploadPayload: StartUploadPayload = { - uploads: [{ index: 0, size: 40 }], + uploads: [{ index: 0, size }], }; const resolvesTo: StartUploadResponse = { - uploads: [{ index: validIndex, uuid: validUUID, url: '', urls: null }], + uploads: [{ index: 0, uuid: validUUID, url: '', urls: null }], }; const callStub = vi.spyOn(HttpClient.prototype, 'post').mockResolvedValue(resolvesTo); const staticStartUpload = vi.spyOn(Network.prototype, 'startUpload'); // Act - const response = await client.startUpload(idBucket, validStartUploadPayload); + const response = await client.startUpload(idBucket, size, abortSignal); // Assert expect(response).toEqual(resolvesTo); @@ -171,6 +123,7 @@ describe('network ', () => { `/v2/buckets/${idBucket}/files/start?multiparts=1`, validStartUploadPayload, headers, + abortSignal, ); }); @@ -201,12 +154,17 @@ describe('network ', () => { const staticFinishUpload = vi.spyOn(Network.prototype, 'finishUpload'); // Act - const response = await client.finishUpload(idBucket, validFinishUploadPayload); + const response = await client.finishUpload(idBucket, validFinishUploadPayload, abortSignal); // Assert expect(response).toEqual(resolvesTo); expect(staticFinishUpload).toHaveBeenCalled(); - expect(callStub).toHaveBeenCalledWith(`/v2/buckets/${idBucket}/files/finish`, validFinishUploadPayload, headers); + expect(callStub).toHaveBeenCalledWith( + `/v2/buckets/${idBucket}/files/finish`, + validFinishUploadPayload, + headers, + abortSignal, + ); }); it('should call static getDownloadLinks with correct parameters', async () => { diff --git a/test/network/upload.test.ts b/test/network/upload.test.ts index 4f9368b..a00fb76 100644 --- a/test/network/upload.test.ts +++ b/test/network/upload.test.ts @@ -10,6 +10,7 @@ const fakeFileId = 'aaaaaa'; const fakeBucketId = 'fake-bucket-id'; const fakeHash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; const fakeMnemonic = 'test test test test test test test test'; +const abortSignal = new AbortController().signal; const crypto: Crypto = { validateMnemonic: () => { return false; @@ -77,44 +78,39 @@ describe('network/upload', () => { const encryptFileMock = vi.fn(); const uploadFileMock = vi.fn().mockReturnValue(fakeHash); - try { - const receivedFileId = await uploadFile( - network, - crypto, - bucketId, - mnemonic, - fileSize, - encryptFileMock, - uploadFileMock, - ); - - expect(validateMnemonicStub).toHaveBeenCalledOnce(); - expect(validateMnemonicStub).toHaveBeenCalledWith(mnemonic); - - expect(randomBytesStub).toHaveBeenCalledOnce(); - expect(randomBytesStub).toHaveBeenCalledWith(ALGORITHMS.AES256CTR.ivSize); - - expect(generateFileKeyStub).toHaveBeenCalledOnce(); - expect(generateFileKeyStub).toHaveBeenCalledWith(mnemonic, bucketId, bufferizedIndex); - - expect(startUploadStub).toHaveBeenCalledOnce(); - expect(startUploadStub).toHaveBeenCalledWith(bucketId, { - uploads: [ - { - index: 0, - size: fileSize, - }, - ], - }); + const receivedFileId = await uploadFile( + network, + crypto, + bucketId, + mnemonic, + fileSize, + encryptFileMock, + uploadFileMock, + abortSignal, + ); + + expect(validateMnemonicStub).toHaveBeenCalledOnce(); + expect(validateMnemonicStub).toHaveBeenCalledWith(mnemonic); - expect(encryptFileMock).toHaveBeenCalledTimes(1); - expect(encryptFileMock).toHaveBeenCalledWith(ALGORITHMS.AES256CTR.type, key, bufferizedIndex.slice(0, 16)); + expect(randomBytesStub).toHaveBeenCalledOnce(); + expect(randomBytesStub).toHaveBeenCalledWith(ALGORITHMS.AES256CTR.ivSize); - expect(uploadFileMock).toHaveBeenCalledTimes(1); - expect(uploadFileMock).toHaveBeenCalledWith(fakeUrl); + expect(generateFileKeyStub).toHaveBeenCalledOnce(); + expect(generateFileKeyStub).toHaveBeenCalledWith(mnemonic, bucketId, bufferizedIndex); - expect(finishUploadStub).toHaveBeenCalledOnce(); - expect(finishUploadStub).toHaveBeenCalledWith(bucketId, { + expect(startUploadStub).toHaveBeenCalledOnce(); + expect(startUploadStub).toHaveBeenCalledWith(bucketId, fileSize, abortSignal); + + expect(encryptFileMock).toHaveBeenCalledTimes(1); + expect(encryptFileMock).toHaveBeenCalledWith(ALGORITHMS.AES256CTR.type, key, bufferizedIndex.slice(0, 16)); + + expect(uploadFileMock).toHaveBeenCalledTimes(1); + expect(uploadFileMock).toHaveBeenCalledWith(fakeUrl); + + expect(finishUploadStub).toHaveBeenCalledOnce(); + expect(finishUploadStub).toHaveBeenCalledWith( + bucketId, + { index, shards: [ { @@ -122,12 +118,11 @@ describe('network/upload', () => { uuid: fakeUuid, }, ], - }); + }, + abortSignal, + ); - expect(receivedFileId).toEqual(fileId); - } catch { - fail('Expected function to not throw an error, but it did.'); - } + expect(receivedFileId).toEqual(fileId); }); it('Should throw if the mnemonic is invalid', async () => { @@ -139,7 +134,7 @@ describe('network/upload', () => { const randomBytes = vi.spyOn(crypto, 'randomBytes').mockReturnValue(Buffer.from('')); try { - await uploadFile(network, crypto, bucketId, mnemonic, fileSize, vi.fn(), vi.fn()); + await uploadFile(network, crypto, bucketId, mnemonic, fileSize, vi.fn(), vi.fn(), abortSignal); fail('Expected function to throw an error, but it did not.'); } catch (err) {