diff --git a/src/commands/download-file.ts b/src/commands/download-file.ts index ab54696c..b4f6c7d0 100644 --- a/src/commands/download-file.ts +++ b/src/commands/download-file.ts @@ -5,7 +5,6 @@ import { NetworkFacade } from '../services/network/network-facade.service'; import { AuthService } from '../services/auth.service'; import { CryptoService } from '../services/crypto.service'; import { DownloadService } from '../services/network/download.service'; -import { UploadService } from '../services/network/upload.service'; import { SdkManager } from '../services/sdk-manager.service'; import { createWriteStream } from 'node:fs'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; @@ -16,6 +15,8 @@ import { StreamUtils } from '../utils/stream.utils'; import { ErrorUtils } from '../utils/errors.utils'; import { NotValidDirectoryError, NotValidFileUuidError } from '../types/command.types'; import { ValidationService } from '../services/validation.service'; +import { Environment } from '@internxt/inxt-js'; +import { ConfigService } from '../services/config.service'; export default class DownloadFile extends Command { static readonly args = {}; @@ -198,9 +199,15 @@ export default class DownloadFile extends Command { user: user.bridgeUser, pass: user.userId, }); + const environment = new Environment({ + bridgeUser: user.bridgeUser, + bridgePass: user.userId, + bridgeUrl: ConfigService.instance.get('NETWORK_URL'), + encryptionKey: user.mnemonic, + }); const networkFacade = new NetworkFacade( networkModule, - UploadService.instance, + environment, DownloadService.instance, CryptoService.instance, ); diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index 76f8dbe6..60033297 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -8,7 +8,6 @@ import { CLIUtils } from '../utils/cli.utils'; import { ConfigService } from '../services/config.service'; import path from 'node:path'; import { DriveFileService } from '../services/drive/drive-file.service'; -import { UploadService } from '../services/network/upload.service'; import { CryptoService } from '../services/crypto.service'; import { DownloadService } from '../services/network/download.service'; import { ErrorUtils } from '../utils/errors.utils'; @@ -19,6 +18,7 @@ import { ThumbnailService } from '../services/thumbnail.service'; import { BufferStream } from '../utils/stream.utils'; import { isFileThumbnailable } from '../utils/thumbnail.utils'; import { Readable } from 'node:stream'; +import { Environment } from '@internxt/inxt-js'; export default class UploadFile extends Command { static readonly args = {}; @@ -72,9 +72,15 @@ export default class UploadFile extends Command { user: user.bridgeUser, pass: user.userId, }); + const environment = new Environment({ + bridgeUser: user.bridgeUser, + bridgePass: user.userId, + bridgeUrl: ConfigService.instance.get('NETWORK_URL'), + encryptionKey: user.mnemonic, + }); const networkFacade = new NetworkFacade( networkModule, - UploadService.instance, + environment, DownloadService.instance, CryptoService.instance, ); @@ -99,32 +105,35 @@ export default class UploadFile extends Command { } const progressCallback = (progress: number) => { - progressBar.update(progress * 0.99); + progressBar.update(progress * 100 * 0.99); }; - const [uploadPromise, abortable] = await UploadService.instance.uploadFileStream( - fileStream, - user.bucket, - user.mnemonic, - stats.size, - networkFacade, - progressCallback, - ); - - process.on('SIGINT', () => { - abortable.abort('SIGINT received'); - process.exit(1); + const fileId = await new Promise((resolve: (fileId: string) => void, reject) => { + const state = networkFacade.uploadFile( + fileStream, + stats.size, + user.bucket, + (err: Error | null, res: string | null) => { + if (err) { + return reject(err); + } + resolve(res as string); + }, + progressCallback, + ); + process.on('SIGINT', () => { + state.stop(); + process.exit(1); + }); }); - const uploadResult = await uploadPromise; - // 3. Create the file in Drive const createdDriveFile = await DriveFileService.instance.createFile({ plain_name: fileInfo.name, type: fileType, size: stats.size, folder_id: destinationFolderUuid, - id: uploadResult.fileId, + id: fileId, bucket: user.bucket, encrypt_version: EncryptionVersion.Aes03, name: '', @@ -139,7 +148,6 @@ export default class UploadFile extends Command { thumbnailBuffer, fileType, user.bucket, - user.mnemonic, createdDriveFile.id, networkFacade, ); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index c71febbe..2d39bb06 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,7 +1,6 @@ import { CryptoProvider } from '@internxt/sdk'; import { Keys, Password } from '@internxt/sdk/dist/auth'; import { createCipheriv, createDecipheriv, createHash, Decipher, pbkdf2Sync, randomBytes } from 'node:crypto'; -import { Readable, Transform } from 'node:stream'; import { KeysService } from './keys.service'; import { ConfigService } from '../services/config.service'; import { StreamUtils } from '../utils/stream.utils'; @@ -116,20 +115,6 @@ export class CryptoService { return Buffer.concat([decipher.update(contentsToDecrypt), decipher.final()]).toString('utf8'); }; - public encryptStreamInParts = ( - readable: Readable, - cipher: Transform, - size: number, - parts: number, - ): Transform => { - // We include a marginChunkSize because if we split the chunk directly, there will always be one more chunk left, this will cause a mismatch with the urls provided - const marginChunkSize = 1024; - const chunkSize = size / parts + marginChunkSize; - const readableChunks = StreamUtils.streamReadableIntoChunks(readable, chunkSize); - - return readableChunks.pipe(cipher); - }; - public decryptStream = ( inputSlices: ReadableStream[], key: Buffer, @@ -180,11 +165,6 @@ export class CryptoService { return decryptedStream; }; - public getEncryptionTransform = (key: Buffer, iv: Buffer): Transform => { - const cipher = createCipheriv('aes-256-ctr', key, iv); - return cipher; - }; - /** * Generates the key and the iv by transforming a secret and a salt. * It will generate the same key and iv if the same secret and salt is used. diff --git a/src/services/network/network-facade.service.ts b/src/services/network/network-facade.service.ts index 061ad02f..50191bf7 100644 --- a/src/services/network/network-facade.service.ts +++ b/src/services/network/network-facade.service.ts @@ -1,38 +1,22 @@ import { Network } from '@internxt/sdk'; -import * as NetworkUpload from '@internxt/sdk/dist/network/upload'; import * as NetworkDownload from '@internxt/sdk/dist/network/download'; -import { - DecryptFileFunction, - DownloadFileFunction, - EncryptFileFunction, - UploadFileFunction, - UploadFileMultipartFunction, -} from '@internxt/sdk/dist/network'; +import { DecryptFileFunction, DownloadFileFunction } from '@internxt/sdk/dist/network'; import { Environment } from '@internxt/inxt-js'; import { randomBytes } from 'node:crypto'; -import { Readable, Transform } from 'node:stream'; -import { - DownloadOptions, - UploadOptions, - UploadProgressCallback, - DownloadProgressCallback, - UploadMultipartOptions, - UploadTask, -} from '../../types/network.types'; +import { Readable } from 'node:stream'; +import { DownloadOptions, DownloadProgressCallback } from '../../types/network.types'; import { CryptoService } from '../crypto.service'; -import { UploadService } from './upload.service'; import { DownloadService } from './download.service'; import { ValidationService } from '../validation.service'; -import { HashStream } from '../../utils/hash.utils'; import { RangeOptions } from '../../utils/network.utils'; -import { queue, QueueObject } from 'async'; +import { ActionState } from '@internxt/inxt-js/build/api'; export class NetworkFacade { private readonly cryptoLib: Network.Crypto; constructor( private readonly network: Network.Network, - private readonly uploadService: UploadService, + private readonly environment: Environment, private readonly downloadService: DownloadService, private readonly cryptoService: CryptoService, ) { @@ -132,204 +116,34 @@ export class NetworkFacade { * Performs an upload encrypting the stream content * * @param bucketId The bucket where the file will be uploaded - * @param mnemonic The plain mnemonic of the user * @param size The total size of the stream content * @param from The source ReadStream to upload from - * @param options The upload options * @returns A promise to execute the upload and an abort controller to cancel the upload */ - async uploadFromStream( - bucketId: string, - mnemonic: string, - size: number, + uploadFile( from: Readable, - options?: UploadOptions, - ): Promise<[Promise<{ fileId: string; hash: Buffer }>, AbortController]> { - const hashStream = new HashStream(); - const abortable = options?.abortController ?? new AbortController(); - let encryptionTransform: Transform; - let hash: Buffer; - - const onProgress: UploadProgressCallback = (loadedBytes: number) => { - if (!options?.progressCallback) return; - const reportedProgress = Math.round((loadedBytes / size) * 100); - options.progressCallback(reportedProgress); - }; - - const encryptFile: EncryptFileFunction = async (_, key, iv) => { - const encryptionCipher = this.cryptoService.getEncryptionTransform( - Buffer.from(key as ArrayBuffer), - Buffer.from(iv as ArrayBuffer), - ); - encryptionTransform = from.pipe(encryptionCipher).pipe(hashStream); - }; - - const uploadFile: UploadFileFunction = async (url) => { - await this.uploadService.uploadFileToNetwork(url, encryptionTransform, { - abortController: abortable, - progressCallback: onProgress, - }); - hash = hashStream.getHash(); - return hash.toString('hex'); - }; - - const uploadOperation = async () => { - const uploadResult = await NetworkUpload.uploadFile( - this.network, - this.cryptoLib, - bucketId, - mnemonic, - size, - encryptFile, - uploadFile, - ); - - return { - fileId: uploadResult, - hash: hash, - }; - }; - - return [uploadOperation(), abortable]; - } - - /** - * Performs a multi-part upload encrypting the stream content - * - * @param bucketId The bucket where the file will be uploaded - * @param mnemonic The plain mnemonic of the user - * @param size The total size of the stream content - * @param from The source ReadStream to upload from - * @param options The upload options - * @returns A promise to execute the upload and an abort controller to cancel the upload - */ - async uploadMultipartFromStream( - bucketId: string, - mnemonic: string, size: number, - from: Readable, - options: UploadMultipartOptions, - ): Promise<[Promise<{ fileId: string; hash: Buffer }>, AbortController]> { - const hashStream = new HashStream(); - const abortable = options?.abortController ?? new AbortController(); - let encryptionTransform: Transform; - let hash: Buffer; - - const partsUploadedBytes: Record = {}; - type Part = { - PartNumber: number; - ETag: string; - }; - const fileParts: Part[] = []; - - const onProgress = (partId: number, loadedBytes: number) => { - if (!options?.progressCallback) return; - partsUploadedBytes[partId] = loadedBytes; - const currentTotalLoadedBytes = Object.values(partsUploadedBytes).reduce((a, p) => a + p, 0); - const reportedProgress = Math.round((currentTotalLoadedBytes / size) * 100); - options.progressCallback(reportedProgress); - }; - - const encryptFile: EncryptFileFunction = async (_, key, iv) => { - const encryptionCipher = this.cryptoService.getEncryptionTransform( - Buffer.from(key as ArrayBuffer), - Buffer.from(iv as ArrayBuffer), - ); - const streamInParts = this.cryptoService.encryptStreamInParts(from, encryptionCipher, size, options.parts); - encryptionTransform = streamInParts.pipe(hashStream); - }; - - const uploadFileMultipart: UploadFileMultipartFunction = async (urls: string[]) => { - let partIndex = 0; - const limitConcurrency = 6; - - const uploadPart = async (upload: UploadTask) => { - const { etag } = await this.uploadService.uploadFileToNetwork(upload.urlToUpload, upload.contentToUpload, { - abortController: abortable, - progressCallback: (loadedBytes: number) => { - onProgress(upload.index, loadedBytes); - }, - }); - - fileParts.push({ - ETag: etag, - PartNumber: upload.index + 1, - }); - }; - - const uploadQueue: QueueObject = queue(function (task, callback) { - uploadPart(task) - .then(() => { - callback(); - }) - .catch((e) => { - callback(e); - }); - }, limitConcurrency); - - for await (const chunk of encryptionTransform) { - const part: Buffer = chunk; - - if (uploadQueue.running() === limitConcurrency) { - await uploadQueue.unsaturated(); - } - - if (abortable.signal.aborted) { - throw new Error('Upload cancelled by user'); - } - - let errorAlreadyThrown = false; - - uploadQueue - .pushAsync({ - contentToUpload: part, - urlToUpload: urls[partIndex], - index: partIndex++, - }) - .catch((err: Error) => { - if (errorAlreadyThrown) return; - - errorAlreadyThrown = true; - if (err) { - uploadQueue.kill(); - if (!abortable?.signal.aborted) { - abortable.abort(); - } - } - }); - } - - while (uploadQueue.running() > 0 || uploadQueue.length() > 0) { - await uploadQueue.drain(); - } - - hash = hashStream.getHash(); - const compareParts = (pA: Part, pB: Part) => pA.PartNumber - pB.PartNumber; - const sortedParts = fileParts.sort(compareParts); - return { - hash: hash.toString('hex'), - parts: sortedParts, - }; - }; - - const uploadOperation = async () => { - const uploadResult = await NetworkUpload.uploadMultipartFile( - this.network, - this.cryptoLib, - bucketId, - mnemonic, - size, - encryptFile, - uploadFileMultipart, - options.parts, - ); - - return { - fileId: uploadResult, - hash: hash, - }; - }; - - return [uploadOperation(), abortable]; + bucketId: string, + finishedCallback: (err: Error | null, res: string | null) => void, + progressCallback: (progress: number) => void, + ): ActionState { + const minimumMultipartThreshold = 100 * 1024 * 1024; + const useMultipart = size > minimumMultipartThreshold; + + if (useMultipart) { + return this.environment.uploadMultipartFile(bucketId, { + source: from, + fileSize: size, + finishedCallback, + progressCallback, + }); + } else { + return this.environment.upload(bucketId, { + source: from, + fileSize: size, + finishedCallback, + progressCallback, + }); + } } } diff --git a/src/services/network/upload.service.ts b/src/services/network/upload.service.ts deleted file mode 100644 index 8ef51e3e..00000000 --- a/src/services/network/upload.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Readable } from 'node:stream'; -import axios from 'axios'; -import { UploadOptions } from '../../types/network.types'; -import { NetworkFacade } from './network-facade.service'; - -export class UploadService { - public static readonly instance: UploadService = new UploadService(); - - public uploadFileToNetwork = async ( - url: string, - from: Readable | Buffer, - options: UploadOptions, - ): Promise<{ etag: string }> => { - const response = await axios.put(url, from, { - signal: options.abortController?.signal, - onUploadProgress: (progressEvent) => { - if (options.progressCallback && progressEvent.loaded) { - options.progressCallback(progressEvent.loaded); - } - }, - }); - - const etag = response.headers['etag']; - if (!etag) { - throw new Error('Missing Etag in response when uploading file'); - } - return { etag }; - }; - - public uploadFileStream = async ( - fileStream: Readable, - userBucket: string, - userMnemonic: string, - fileSize: number, - networkFacade: NetworkFacade, - progressCallback?: (progress: number) => void, - ) => { - const minimumMultipartThreshold = 100 * 1024 * 1024; - const useMultipart = fileSize > minimumMultipartThreshold; - const partSize = 30 * 1024 * 1024; - const parts = Math.ceil(fileSize / partSize); - - let uploadOperation: Promise< - [ - Promise<{ - fileId: string; - hash: Buffer; - }>, - AbortController, - ] - >; - - if (useMultipart) { - uploadOperation = networkFacade.uploadMultipartFromStream(userBucket, userMnemonic, fileSize, fileStream, { - parts, - progressCallback, - }); - } else { - uploadOperation = networkFacade.uploadFromStream(userBucket, userMnemonic, fileSize, fileStream, { - progressCallback, - }); - } - - const uploadFileOperation = await uploadOperation; - - return uploadFileOperation; - }; -} diff --git a/src/services/thumbnail.service.ts b/src/services/thumbnail.service.ts index 98350848..4f106793 100644 --- a/src/services/thumbnail.service.ts +++ b/src/services/thumbnail.service.ts @@ -2,7 +2,6 @@ import { Readable } from 'node:stream'; import { DriveFileService } from './drive/drive-file.service'; import { StorageTypes } from '@internxt/sdk/dist/drive'; import { NetworkFacade } from './network/network-facade.service'; -import { UploadService } from './network/upload.service'; import { isImageThumbnailable, ThumbnailConfig } from '../utils/thumbnail.utils'; import sharp from 'sharp'; @@ -13,7 +12,6 @@ export class ThumbnailService { fileContent: Buffer, fileType: string, userBucket: string, - userMnemonic: string, file_id: number, networkFacade: NetworkFacade, ): Promise => { @@ -23,16 +21,21 @@ export class ThumbnailService { } if (thumbnailBuffer) { const size = thumbnailBuffer.length; - const [thumbnailPromise] = await UploadService.instance.uploadFileStream( - Readable.from(thumbnailBuffer), - userBucket, - userMnemonic, - size, - networkFacade, - () => {}, - ); - const thumbnailUploadResult = await thumbnailPromise; + const fileId = await new Promise((resolve: (fileId: string) => void, reject) => { + networkFacade.uploadFile( + Readable.from(thumbnailBuffer), + size, + userBucket, + (err: Error | null, res: string | null) => { + if (err) { + return reject(err); + } + resolve(res as string); + }, + () => {}, + ); + }); const createdThumbnailFile = await DriveFileService.instance.createThumbnail({ file_id: file_id, @@ -41,7 +44,7 @@ export class ThumbnailService { type: ThumbnailConfig.Type, size: size, bucket_id: userBucket, - bucket_file: thumbnailUploadResult.fileId, + bucket_file: fileId, encrypt_version: StorageTypes.EncryptionVersion.Aes03, }); return createdThumbnailFile; diff --git a/src/utils/hash.utils.ts b/src/utils/hash.utils.ts deleted file mode 100644 index d4eb9488..00000000 --- a/src/utils/hash.utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createHash, Hash } from 'node:crypto'; -import { Transform, TransformCallback, TransformOptions } from 'node:stream'; - -export class HashStream extends Transform { - hasher: Hash; - finalHash: Buffer; - - constructor(opts?: TransformOptions) { - super(opts); - this.hasher = createHash('sha256'); - this.finalHash = Buffer.alloc(0); - } - - _transform(chunk: Buffer, enc: BufferEncoding, cb: TransformCallback) { - this.hasher.update(chunk); - cb(null, chunk); - } - - _flush(cb: (err: Error | null) => void) { - return this.hasher.end(cb); - } - - reset() { - this.hasher = createHash('sha256'); - } - - readHash() { - if (!this.finalHash.length) { - this.finalHash = this.hasher.read(); - } - - return this.finalHash; - } - - getHash() { - if (!this.finalHash.length) { - this.readHash(); - } - - return createHash('ripemd160').update(this.finalHash).digest(); - } -} diff --git a/src/utils/stream.utils.ts b/src/utils/stream.utils.ts index e1eeb719..756e6fab 100644 --- a/src/utils/stream.utils.ts +++ b/src/utils/stream.utils.ts @@ -1,5 +1,5 @@ import { ReadStream, WriteStream } from 'node:fs'; -import { Readable, Transform, TransformCallback, TransformOptions } from 'node:stream'; +import { Transform, TransformCallback, TransformOptions } from 'node:stream'; export class StreamUtils { static readStreamToReadableStream(readStream: ReadStream): ReadableStream { @@ -64,71 +64,6 @@ export class StreamUtils { return stream; } - - /** - * Given a readable stream, it enqueues its parts into chunks as it is being read - * @param readable Readable stream - * @param chunkSize The chunkSize in bytes that we want each chunk to be - * @returns A readable stream whose output is chunks of size chunkSize - */ - static streamReadableIntoChunks(readable: Readable, chunkSize: number): Readable { - let buffer = Buffer.alloc(0); - - const mergeBuffers = (buffer1: Buffer, buffer2: Buffer): Buffer => { - return Buffer.concat([buffer1, buffer2]); - }; - - const outputStream = new Readable({ - read() { - // noop - }, - }); - - readable.on('data', (chunk: Buffer) => { - buffer = mergeBuffers(buffer, chunk); - - while (buffer.length >= chunkSize) { - outputStream.push(buffer.subarray(0, chunkSize)); - buffer = buffer.subarray(chunkSize); - } - }); - - readable.on('end', () => { - if (buffer.length > 0) { - outputStream.push(buffer); - } - outputStream.push(null); // Signal the end of the stream - }); - - readable.on('error', (err) => { - outputStream.destroy(err); - }); - - return outputStream; - } -} - -export class ProgressTransform extends Transform { - private receivedBytes = 0; - - constructor( - private options: { totalBytes: number }, - private progressCallback: (percentage: number) => void, - ) { - super(); - } - - _transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback) { - this.receivedBytes += chunk.length; - const percentage = this.receivedBytes / this.options.totalBytes; - this.progressCallback(percentage); - this.push(chunk); - callback(); - } - - _flush(callback: (err: Error | null) => void) { - callback(null); - } } export class BufferStream extends Transform { diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index fd1cb849..214b9c55 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -12,7 +12,6 @@ import { DriveFolderService } from '../../services/drive/drive-folder.service'; import { TrashService } from '../../services/drive/trash.service'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { CLIUtils } from '../../utils/cli.utils'; -import { UploadService } from '../../services/network/upload.service'; import { BufferStream } from '../../utils/stream.utils'; import { Readable } from 'node:stream'; import { isFileThumbnailable } from '../../utils/thumbnail.utils'; @@ -86,28 +85,37 @@ export class PUTRequestHandler implements WebDavMethodHandler { fileStream = req.pipe(bufferStream); } + let uploaded = false, + aborted = false; + const progressCallback = (progress: number) => { - webdavLogger.info(`[PUT] Upload progress for file ${resource.name}: ${progress}%`); + if (!uploaded && !aborted) { + webdavLogger.info(`[PUT] Upload progress for file ${resource.name}: ${(progress * 100).toFixed(2)}%`); + } }; - const [uploadPromise, abortable] = await UploadService.instance.uploadFileStream( - fileStream, - user.bucket, - user.mnemonic, - contentLength, - networkFacade, - progressCallback, - ); - - let uploaded = false; - res.on('close', () => { - if (!uploaded) { - webdavLogger.info('[PUT] ❌ HTTP Client has been disconnected, res has been closed.'); - abortable.abort('HTTP Client has been disconnected.'); - } + const fileId = await new Promise((resolve: (fileId: string) => void, reject) => { + const state = networkFacade.uploadFile( + fileStream, + contentLength, + user.bucket, + (err: Error | null, res: string | null) => { + if (err) { + aborted = true; + return reject(err); + } + resolve(res as string); + }, + progressCallback, + ); + res.on('close', async () => { + aborted = true; + if (!uploaded) { + webdavLogger.info('[PUT] ❌ HTTP Client has been disconnected, res has been closed.'); + state.stop(); + } + }); }); - - const uploadResult = await uploadPromise; uploaded = true; webdavLogger.info('[PUT] ✅ File uploaded to network'); @@ -117,7 +125,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { type: fileType, size: contentLength, folder_id: parentFolderItem.uuid, - id: uploadResult.fileId, + id: fileId, bucket: user.bucket, encrypt_version: EncryptionVersion.Aes03, name: '', @@ -132,7 +140,6 @@ export class PUTRequestHandler implements WebDavMethodHandler { thumbnailBuffer, fileType, user.bucket, - user.mnemonic, file.id, networkFacade, ); diff --git a/src/webdav/index.ts b/src/webdav/index.ts index 67143a30..ef847dcc 100644 --- a/src/webdav/index.ts +++ b/src/webdav/index.ts @@ -7,7 +7,6 @@ import { DriveDatabaseManager } from '../services/database/drive-database-manage import { DriveFileRepository } from '../services/database/drive-file/drive-file.repository'; import { DriveFolderRepository } from '../services/database/drive-folder/drive-folder.repository'; import { DriveFileService } from '../services/drive/drive-file.service'; -import { UploadService } from '../services/network/upload.service'; import { DownloadService } from '../services/network/download.service'; import { AuthService } from '../services/auth.service'; import { CryptoService } from '../services/crypto.service'; @@ -36,7 +35,6 @@ const init = async () => { DriveFileService.instance, DriveFolderService.instance, new DriveDatabaseManager(new DriveFileRepository(), new DriveFolderRepository()), - UploadService.instance, DownloadService.instance, AuthService.instance, CryptoService.instance, diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index a141f83f..7062f70c 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -13,7 +13,6 @@ import { DriveDatabaseManager } from '../services/database/drive-database-manage import { GETRequestHandler } from './handlers/GET.handler'; import { HEADRequestHandler } from './handlers/HEAD.handler'; import { DriveFileService } from '../services/drive/drive-file.service'; -import { UploadService } from '../services/network/upload.service'; import { DownloadService } from '../services/network/download.service'; import { AuthService } from '../services/auth.service'; import { CryptoService } from '../services/crypto.service'; @@ -29,6 +28,7 @@ import { PROPPATCHRequestHandler } from './handlers/PROPPATCH.handler'; import { MOVERequestHandler } from './handlers/MOVE.handler'; import { COPYRequestHandler } from './handlers/COPY.handler'; import { TrashService } from '../services/drive/trash.service'; +import { Environment } from '@internxt/inxt-js'; export class WebDavServer { constructor( @@ -37,7 +37,6 @@ export class WebDavServer { private readonly driveFileService: DriveFileService, private readonly driveFolderService: DriveFolderService, private readonly driveDatabaseManager: DriveDatabaseManager, - private readonly uploadService: UploadService, private readonly downloadService: DownloadService, private readonly authService: AuthService, private readonly cryptoService: CryptoService, @@ -52,8 +51,20 @@ export class WebDavServer { user: credentials.user.bridgeUser, pass: credentials.user.userId, }); + const environment = new Environment({ + bridgeUser: credentials.user.bridgeUser, + bridgePass: credentials.user.userId, + bridgeUrl: ConfigService.instance.get('NETWORK_URL'), + encryptionKey: credentials.user.mnemonic, + }); + const networkFacade = new NetworkFacade( + networkModule, + environment, + DownloadService.instance, + CryptoService.instance, + ); - return new NetworkFacade(networkModule, this.uploadService, this.downloadService, this.cryptoService); + return networkFacade; }; private readonly registerMiddlewares = async () => { diff --git a/test/services/network/network-facade.service.test.ts b/test/services/network/network-facade.service.test.ts index 9fe4ed2c..811568fa 100644 --- a/test/services/network/network-facade.service.test.ts +++ b/test/services/network/network-facade.service.test.ts @@ -1,18 +1,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import * as NetworkUpload from '@internxt/sdk/dist/network/upload'; import { NetworkFacade } from '../../../src/services/network/network-facade.service'; import { SdkManager } from '../../../src/services/sdk-manager.service'; import path from 'node:path'; import { createReadStream } from 'node:fs'; -import { UploadService } from '../../../src/services/network/upload.service'; import { CryptoService } from '../../../src/services/crypto.service'; import { DownloadService } from '../../../src/services/network/download.service'; import { Readable } from 'node:stream'; import axios from 'axios'; import { fail } from 'node:assert'; -import crypto from 'node:crypto'; -import { HashStream } from '../../../src/utils/hash.utils'; -import { UploadMultipartOptions } from '../../../src/types/network.types'; +import { Environment } from '@internxt/inxt-js'; +import { ConfigService } from '../../../src/services/config.service'; +import { UserFixture } from '../../fixtures/auth.fixture'; describe('Network Facade Service', () => { beforeEach(() => { @@ -26,120 +24,62 @@ describe('Network Facade Service', () => { }); }; - it('When a file is prepared to upload, then it should return the abort controller and upload promise', async () => { - const sut = new NetworkFacade( - getNetworkMock(), - UploadService.instance, - DownloadService.instance, - CryptoService.instance, - ); - const file = path.join(process.cwd(), 'test/fixtures/test-content.fixture.txt'); - const readStream = createReadStream(file); - const options = { - progressCallback: vi.fn(), - abortController: new AbortController(), - }; - - const result = await sut.uploadFromStream( - 'f1858bc9675f9e4f7ab29429', - 'animal fog wink trade december thumb sight cousin crunch plunge captain enforce letter creek text', - 100, - readStream, - options, - ); - - expect(result[0]).to.be.instanceOf(Promise); - expect(result[1]).to.be.instanceOf(AbortController); - }); + const getEnvironmentMock = () => { + return new Environment({ + bridgeUser: 'user', + bridgePass: 'pass', + bridgeUrl: ConfigService.instance.get('NETWORK_URL'), + encryptionKey: UserFixture.mnemonic, + }); + }; - it('When a file is uploaded, should return the fileId', async () => { + it('When a file is uploaded, then it should call the inxt-js upload functionality', async () => { + const mockEnvironment = { + upload: vi.fn(), + uploadMultipartFile: vi.fn(), + }; const sut = new NetworkFacade( getNetworkMock(), - UploadService.instance, + // @ts-expect-error - We only mock the properties we need + mockEnvironment, DownloadService.instance, CryptoService.instance, ); const file = path.join(process.cwd(), 'test/fixtures/test-content.fixture.txt'); const readStream = createReadStream(file); - const options = { - progressCallback: vi.fn(), - abortController: new AbortController(), - }; - vi.spyOn(NetworkUpload, 'uploadFile').mockResolvedValue('uploaded_file_id'); - const [executeUpload] = await sut.uploadFromStream( - 'f1858bc9675f9e4f7ab29429', - 'animal fog wink trade december thumb sight cousin crunch plunge captain enforce letter creek text', - 100, - readStream, - options, - ); + const finishedCallback = vi.fn(); + const progressCallback = vi.fn(); - const uploadResult = await executeUpload; + sut.uploadFile(readStream, 100, 'f1858bc9675f9e4f7ab29429', finishedCallback, progressCallback); - expect(uploadResult.fileId).to.be.equal('uploaded_file_id'); + expect(mockEnvironment.upload).toHaveBeenCalledOnce(); + expect(mockEnvironment.uploadMultipartFile).not.toHaveBeenCalled(); }); - it('When a file is uploaded, then it should report progress', async () => { - const bucket = 'f1858bc9675f9e4f7ab29429'; - const networkMock = getNetworkMock(); + it('When a file is uploaded via multipart, then it should call the inxt-js upload multipart', async () => { + const mockEnvironment = { + upload: vi.fn(), + uploadMultipartFile: vi.fn(), + }; const sut = new NetworkFacade( - networkMock, - UploadService.instance, + getNetworkMock(), + // @ts-expect-error - We only mock the properties we need + mockEnvironment, DownloadService.instance, CryptoService.instance, ); - const file = crypto.randomBytes(16).toString('hex'); - const readStream = new Readable({ - read() { - this.push(file); - this.push(null); - }, - }); - const options = { - progressCallback: vi.fn(), - abortController: new AbortController(), - }; - - vi.spyOn(HashStream.prototype, 'getHash').mockImplementation(() => Buffer.from('')); - - vi.spyOn(axios, 'put').mockImplementation((_, __, config) => { - config?.onUploadProgress?.({ - loaded: file.length, - total: file.length, - bytes: file.length, - lengthComputable: true, - }); - return Promise.resolve({ - data: readStream, - headers: { - etag: 'any-etag', - }, - }); - }); - - vi.spyOn(networkMock, 'startUpload').mockResolvedValue({ - uploads: [{ index: 0, url: 'any-url', uuid: 'any-uuid', urls: [] }], - }); - - vi.spyOn(networkMock, 'finishUpload') - // @ts-expect-error - We only mock the properties we need - .mockResolvedValue({ - id: 'any-id', - }); + const file = path.join(process.cwd(), 'test/fixtures/test-content.fixture.txt'); + const readStream = createReadStream(file); - const [executeUpload] = await sut.uploadFromStream( - bucket, - 'animal fog wink trade december thumb sight cousin crunch plunge captain enforce letter creek text', - file.length, - readStream, - options, - ); + const finishedCallback = vi.fn(); + const progressCallback = vi.fn(); - await executeUpload; + sut.uploadFile(readStream, 101 * 1024 * 1024, 'f1858bc9675f9e4f7ab29429', finishedCallback, progressCallback); - expect(options.progressCallback).toHaveBeenCalledWith(100); + expect(mockEnvironment.uploadMultipartFile).toHaveBeenCalledOnce(); + expect(mockEnvironment.upload).not.toHaveBeenCalled(); }); it('When a file is downloaded, should write it to a stream', async () => { @@ -170,7 +110,7 @@ describe('Network Facade Service', () => { }); const downloadServiceStub = DownloadService.instance; vi.spyOn(downloadServiceStub, 'downloadFile').mockResolvedValue(readableContent); - const sut = new NetworkFacade(networkMock, UploadService.instance, downloadServiceStub, CryptoService.instance); + const sut = new NetworkFacade(networkMock, getEnvironmentMock(), downloadServiceStub, CryptoService.instance); const chunks: Uint8Array[] = []; @@ -223,7 +163,7 @@ describe('Network Facade Service', () => { }); const downloadServiceStub = DownloadService.instance; vi.spyOn(downloadServiceStub, 'downloadFile').mockResolvedValue(readableContent); - const sut = new NetworkFacade(networkMock, UploadService.instance, downloadServiceStub, CryptoService.instance); + const sut = new NetworkFacade(networkMock, getEnvironmentMock(), downloadServiceStub, CryptoService.instance); const writable = new WritableStream(); @@ -274,7 +214,7 @@ describe('Network Facade Service', () => { }); const downloadServiceStub = DownloadService.instance; - const sut = new NetworkFacade(networkMock, UploadService.instance, downloadServiceStub, CryptoService.instance); + const sut = new NetworkFacade(networkMock, getEnvironmentMock(), downloadServiceStub, CryptoService.instance); const writable = new WritableStream(); @@ -305,76 +245,4 @@ describe('Network Facade Service', () => { expect(options.progressCallback).toHaveBeenCalledWith(100); }); - - it('When a file is uploaded via multipart, then it should report progress', async () => { - const bucket = 'f1858bc9675f9e4f7ab29429'; - const networkMock = getNetworkMock(); - - const sut = new NetworkFacade( - networkMock, - UploadService.instance, - DownloadService.instance, - CryptoService.instance, - ); - const file = crypto.randomBytes(16).toString('hex'); - const readStream = new Readable({ - read() { - this.push(file); - this.push(null); - }, - }); - const options: UploadMultipartOptions = { - progressCallback: vi.fn(), - abortController: new AbortController(), - parts: 2, - }; - - vi.spyOn(HashStream.prototype, 'getHash').mockImplementation(() => Buffer.from('')); - - vi.spyOn(axios, 'put').mockImplementation((_, __, config) => { - config?.onUploadProgress?.({ - loaded: file.length, - total: file.length, - bytes: file.length, - lengthComputable: true, - }); - return Promise.resolve({ - data: readStream, - headers: { - etag: 'any-etag', - }, - }); - }); - - vi.spyOn(networkMock, 'startUpload').mockResolvedValue({ - uploads: [ - { - index: 0, - url: 'any-url', - uuid: 'any-uuid', - UploadId: 'any-UploadId', - urls: ['url_1', 'url_2'], - }, - ], - }); - - vi.spyOn(networkMock, 'finishUpload') - // @ts-expect-error - We only mock the properties we need - .mockResolvedValue({ - id: 'uploaded_file_id', - }); - - const [executeUpload] = await sut.uploadMultipartFromStream( - bucket, - 'animal fog wink trade december thumb sight cousin crunch plunge captain enforce letter creek text', - file.length, - readStream, - options, - ); - - const uploadResult = await executeUpload; - - expect(uploadResult.fileId).to.be.equal('uploaded_file_id'); - expect(options.progressCallback).toHaveBeenCalledWith(100); - }); }); diff --git a/test/services/network/upload.service.test.ts b/test/services/network/upload.service.test.ts deleted file mode 100644 index b72de065..00000000 --- a/test/services/network/upload.service.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { UploadService } from '../../../src/services/network/upload.service'; -import nock from 'nock'; -import { Readable } from 'node:stream'; -import crypto from 'node:crypto'; - -describe('Upload Service', () => { - const sut = UploadService.instance; - - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it('When a file is uploaded and etag is missing, should throw an error', async () => { - const url = 'https://example.com/upload'; - const file = crypto.randomBytes(16).toString('hex'); - const data = new Readable({ - read() { - this.push(file); - this.push(null); - }, - }); - const options = { - progressCallback: vi.fn(), - abortController: new AbortController(), - }; - - nock('https://example.com').put('/upload').reply(200, '', {}); - - try { - await sut.uploadFileToNetwork(url, data, options); - } catch (error) { - expect((error as Error).message).to.contain('Missing Etag'); - } - }); - - it('When a file is uploaded and etag is returned, the etag should be returned', async () => { - const url = 'https://example.com/upload'; - const file = crypto.randomBytes(16).toString('hex'); - const data = new Readable({ - read() { - this.push(file); - this.push(null); - }, - }); - const options = { - progressCallback: vi.fn(), - abortController: new AbortController(), - }; - - nock('https://example.com').put('/upload').reply(200, '', { - etag: 'test-etag', - }); - - const result = await sut.uploadFileToNetwork(url, data, options); - expect(result.etag).to.be.equal('test-etag'); - }); - - it('When a file is uploaded, should update the progress', async () => { - const url = 'https://example.com/upload'; - const file = crypto.randomBytes(16).toString('hex'); - const data = new Readable({ - read() { - this.push(file); - this.push(null); - }, - }); - const options = { - progressCallback: vi.fn(), - abortController: new AbortController(), - }; - - nock('https://example.com').put('/upload').reply(200, '', { - etag: 'test-etag', - }); - - await sut.uploadFileToNetwork(url, data, options); - expect(options.progressCallback).toHaveBeenCalledWith(file.length); - }); - - it('When a file is uploaded and the upload is aborted, should cancel the request', async () => { - /*const url = 'https://example.com/upload'; - const data = new Readable({ - read() { - this.push('test content'); - this.push(null); - }, - }); - const options = { - progressCallback: vi.fn(), - abortController: new AbortController(), - }; - - nock('https://example.com').put('/upload').reply(200, '', { - etag: 'test-etag', - }); - - try { - await sut.uploadFile(url, data, options); - fail('Expected function to throw an error, but it did not.'); - } catch (error) { - expect((error as Error).message).to.contain('The user aborted a request'); - }*/ - }); -}); diff --git a/test/utils/hash.utils.test.ts b/test/utils/hash.utils.test.ts deleted file mode 100644 index 1321091b..00000000 --- a/test/utils/hash.utils.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { HashStream } from '../../src/utils/hash.utils'; - -describe('Hash Utils', () => { - let hashStream: HashStream; - - beforeEach(() => { - hashStream = new HashStream(); - vi.restoreAllMocks(); - }); - - it('should update the hasher with data chunk on _transform call', async () => { - const spy = vi.spyOn(hashStream.hasher, 'update'); - const chunk = Buffer.from('Test data'); - - await new Promise((resolve) => { - hashStream._transform(chunk, 'utf8', () => { - expect(spy).toHaveBeenCalledOnce(); - resolve(); - }); - }); - }); - - it('should successfully calculate hash on readHash call', async () => { - const testData = 'Some test data'; - await new Promise((resolve) => { - hashStream.on('data', () => {}); - hashStream.on('end', () => { - const readHash = hashStream.readHash(); - expect(readHash).toBeInstanceOf(Buffer); - expect(readHash.length).toBeGreaterThan(0); - resolve(); - }); - hashStream.write(testData); - hashStream.end(); - }); - }); -}); diff --git a/test/webdav/handlers/GET.handler.test.ts b/test/webdav/handlers/GET.handler.test.ts index 145c928c..40da55ef 100644 --- a/test/webdav/handlers/GET.handler.test.ts +++ b/test/webdav/handlers/GET.handler.test.ts @@ -10,7 +10,6 @@ import { DriveFileService } from '../../../src/services/drive/drive-file.service import { getDriveDatabaseManager } from '../../fixtures/drive-database.fixture'; import { CryptoService } from '../../../src/services/crypto.service'; import { DownloadService } from '../../../src/services/network/download.service'; -import { UploadService } from '../../../src/services/network/upload.service'; import { AuthService } from '../../../src/services/auth.service'; import { NotFoundError } from '../../../src/utils/errors.utils'; import { SdkManager } from '../../../src/services/sdk-manager.service'; @@ -22,6 +21,9 @@ import { LoginCredentials } from '../../../src/types/command.types'; import { UserCredentialsFixture } from '../../fixtures/login.fixture'; import { randomInt } from 'node:crypto'; import { NetworkUtils } from '../../../src/utils/network.utils'; +import { Environment } from '@internxt/inxt-js'; +import { ConfigService } from '../../../src/services/config.service'; +import { UserFixture } from '../../fixtures/auth.fixture'; describe('GET request handler', () => { const getNetworkMock = () => { @@ -31,6 +33,15 @@ describe('GET request handler', () => { }); }; + const getEnvironmentMock = () => { + return new Environment({ + bridgeUser: 'user', + bridgePass: 'pass', + bridgeUrl: ConfigService.instance.get('NETWORK_URL'), + encryptionKey: UserFixture.mnemonic, + }); + }; + beforeEach(() => { vi.restoreAllMocks(); }); @@ -38,9 +49,8 @@ describe('GET request handler', () => { it('When the Drive file is not found, then it should throw a NotFoundError', async () => { const driveDatabaseManager = getDriveDatabaseManager(); const downloadService = DownloadService.instance; - const uploadService = UploadService.instance; const cryptoService = CryptoService.instance; - const networkFacade = new NetworkFacade(getNetworkMock(), uploadService, downloadService, cryptoService); + const networkFacade = new NetworkFacade(getNetworkMock(), getEnvironmentMock(), downloadService, cryptoService); const requestHandler = new GETRequestHandler({ driveFileService: DriveFileService.instance, downloadService, @@ -83,10 +93,9 @@ describe('GET request handler', () => { it('When file is requested, then it should write a response with the content', async () => { const driveDatabaseManager = getDriveDatabaseManager(); const downloadService = DownloadService.instance; - const uploadService = UploadService.instance; const cryptoService = CryptoService.instance; const authService = AuthService.instance; - const networkFacade = new NetworkFacade(getNetworkMock(), uploadService, downloadService, cryptoService); + const networkFacade = new NetworkFacade(getNetworkMock(), getEnvironmentMock(), downloadService, cryptoService); const requestHandler = new GETRequestHandler({ driveFileService: DriveFileService.instance, downloadService, @@ -143,10 +152,9 @@ describe('GET request handler', () => { it('When file is requested with Range, then it should write a response with the ranged content', async () => { const driveDatabaseManager = getDriveDatabaseManager(); const downloadService = DownloadService.instance; - const uploadService = UploadService.instance; const cryptoService = CryptoService.instance; const authService = AuthService.instance; - const networkFacade = new NetworkFacade(getNetworkMock(), uploadService, downloadService, cryptoService); + const networkFacade = new NetworkFacade(getNetworkMock(), getEnvironmentMock(), downloadService, cryptoService); const requestHandler = new GETRequestHandler({ driveFileService: DriveFileService.instance, downloadService, diff --git a/test/webdav/handlers/PUT.handler.test.ts b/test/webdav/handlers/PUT.handler.test.ts index 8a201157..41b81276 100644 --- a/test/webdav/handlers/PUT.handler.test.ts +++ b/test/webdav/handlers/PUT.handler.test.ts @@ -8,7 +8,6 @@ import { import { DriveFileService } from '../../../src/services/drive/drive-file.service'; import { CryptoService } from '../../../src/services/crypto.service'; import { DownloadService } from '../../../src/services/network/download.service'; -import { UploadService } from '../../../src/services/network/upload.service'; import { AuthService } from '../../../src/services/auth.service'; import { UnsupportedMediaTypeError } from '../../../src/utils/errors.utils'; import { SdkManager } from '../../../src/services/sdk-manager.service'; @@ -22,6 +21,9 @@ import { WebDavRequestedResource } from '../../../src/types/webdav.types'; import { WebDavUtils } from '../../../src/utils/webdav.utils'; import { newDriveFile, newFolderItem } from '../../fixtures/drive.fixture'; import { UserCredentialsFixture } from '../../fixtures/login.fixture'; +import { Environment } from '@internxt/inxt-js'; +import { ConfigService } from '../../../src/services/config.service'; +import { UserFixture } from '../../fixtures/auth.fixture'; describe('PUT request handler', () => { const getNetworkMock = () => { @@ -31,6 +33,15 @@ describe('PUT request handler', () => { }); }; + const getEnvironmentMock = () => { + return new Environment({ + bridgeUser: 'user', + bridgePass: 'pass', + bridgeUrl: ConfigService.instance.get('NETWORK_URL'), + encryptionKey: UserFixture.mnemonic, + }); + }; + beforeEach(() => { vi.restoreAllMocks(); }); @@ -38,7 +49,7 @@ describe('PUT request handler', () => { it('When the content-length request is 0, then it should throw an UnsupportedMediaTypeError', async () => { const networkFacade = new NetworkFacade( getNetworkMock(), - UploadService.instance, + getEnvironmentMock(), DownloadService.instance, CryptoService.instance, ); @@ -74,10 +85,9 @@ describe('PUT request handler', () => { it('When the Drive destination folder is found, then it should upload the file to the folder', async () => { const driveDatabaseManager = getDriveDatabaseManager(); const downloadService = DownloadService.instance; - const uploadService = UploadService.instance; const cryptoService = CryptoService.instance; const authService = AuthService.instance; - const networkFacade = new NetworkFacade(getNetworkMock(), uploadService, downloadService, cryptoService); + const networkFacade = new NetworkFacade(getNetworkMock(), getEnvironmentMock(), downloadService, cryptoService); const sut = new PUTRequestHandler({ driveFileService: DriveFileService.instance, driveFolderService: DriveFolderService.instance, @@ -116,12 +126,12 @@ describe('PUT request handler', () => { .mockResolvedValueOnce(folderFixture) .mockRejectedValue(new Error()); const getAuthDetailsStub = vi.spyOn(authService, 'getAuthDetails').mockResolvedValue(UserCredentialsFixture); - const uploadFromStreamStub = vi - .spyOn(networkFacade, 'uploadFromStream') - .mockResolvedValue([ - Promise.resolve({ fileId: '09218313209', hash: Buffer.from('test') }), - new AbortController(), - ]); + const uploadStub = vi.spyOn(networkFacade, 'uploadFile').mockImplementation( + // @ts-expect-error - We only mock the properties we need + (_, __, ___, callback: (err: Error | null, res: string | null) => void) => { + return callback(null, 'uploaded-file-id'); + }, + ); const createDriveFileStub = vi .spyOn(DriveFileService.instance, 'createFile') .mockResolvedValue(fileFixture.toItem()); @@ -132,7 +142,7 @@ describe('PUT request handler', () => { expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2); expect(getAuthDetailsStub).toHaveBeenCalledOnce(); - expect(uploadFromStreamStub).toHaveBeenCalledOnce(); + expect(uploadStub).toHaveBeenCalledOnce(); expect(createDriveFileStub).toHaveBeenCalledOnce(); expect(createDBFileStub).toHaveBeenCalledOnce(); }); @@ -140,11 +150,10 @@ describe('PUT request handler', () => { it('When the file already exists, then it should upload and replace the file to the folder', async () => { const driveDatabaseManager = getDriveDatabaseManager(); const downloadService = DownloadService.instance; - const uploadService = UploadService.instance; const cryptoService = CryptoService.instance; const authService = AuthService.instance; const trashService = TrashService.instance; - const networkFacade = new NetworkFacade(getNetworkMock(), uploadService, downloadService, cryptoService); + const networkFacade = new NetworkFacade(getNetworkMock(), getEnvironmentMock(), downloadService, cryptoService); const sut = new PUTRequestHandler({ driveFileService: DriveFileService.instance, driveFolderService: DriveFolderService.instance, @@ -185,12 +194,12 @@ describe('PUT request handler', () => { const deleteDBFileStub = vi.spyOn(driveDatabaseManager, 'deleteFileById').mockResolvedValue(); const deleteDriveFileStub = vi.spyOn(trashService, 'trashItems').mockResolvedValue(); const getAuthDetailsStub = vi.spyOn(authService, 'getAuthDetails').mockResolvedValue(UserCredentialsFixture); - const uploadFromStreamStub = vi - .spyOn(networkFacade, 'uploadFromStream') - .mockResolvedValue([ - Promise.resolve({ fileId: '09218313209', hash: Buffer.from('test') }), - new AbortController(), - ]); + const uploadStub = vi.spyOn(networkFacade, 'uploadFile').mockImplementation( + // @ts-expect-error - We only mock the properties we need + (_, __, ___, callback: (err: Error | null, res: string | null) => void) => { + return callback(null, 'uploaded-file-id'); + }, + ); const createDriveFileStub = vi .spyOn(DriveFileService.instance, 'createFile') .mockResolvedValue(fileFixture.toItem()); @@ -201,82 +210,10 @@ describe('PUT request handler', () => { expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2); expect(getAuthDetailsStub).toHaveBeenCalledOnce(); - expect(uploadFromStreamStub).toHaveBeenCalledOnce(); + expect(uploadStub).toHaveBeenCalledOnce(); expect(createDriveFileStub).toHaveBeenCalledOnce(); expect(createDBFileStub).toHaveBeenCalledOnce(); expect(deleteDBFileStub).toHaveBeenCalledOnce(); expect(deleteDriveFileStub).toHaveBeenCalledOnce(); }); - - it('When the Drive destination folder is found, then it should upload the multipart file', async () => { - const driveDatabaseManager = getDriveDatabaseManager(); - const downloadService = DownloadService.instance; - const uploadService = UploadService.instance; - const cryptoService = CryptoService.instance; - const authService = AuthService.instance; - const networkFacade = new NetworkFacade(getNetworkMock(), uploadService, downloadService, cryptoService); - const sut = new PUTRequestHandler({ - driveFileService: DriveFileService.instance, - driveFolderService: DriveFolderService.instance, - authService: AuthService.instance, - trashService: TrashService.instance, - networkFacade, - driveDatabaseManager, - }); - - const multipartFileSize = 105 * 1024 * 1024; - - const requestedFileResource: WebDavRequestedResource = getRequestedFileResource(); - const requestedParentFolderResource: WebDavRequestedResource = getRequestedFolderResource({ - parentFolder: '/', - folderName: '', - }); - const folderFixture = newFolderItem({ name: requestedParentFolderResource.name }); - const fileFixture = newDriveFile({ - folderId: folderFixture.id, - folderUuid: folderFixture.uuid, - size: multipartFileSize, - }); - - const request = createWebDavRequestFixture({ - method: 'PUT', - url: requestedFileResource.url, - headers: { - 'content-length': multipartFileSize.toString(), - }, - }); - - const response = createWebDavResponseFixture({ - status: vi.fn().mockReturnValue({ send: vi.fn() }), - }); - - const getRequestedResourceStub = vi - .spyOn(WebDavUtils, 'getRequestedResource') - .mockResolvedValueOnce(requestedFileResource) - .mockResolvedValueOnce(requestedParentFolderResource); - const getAndSearchItemFromResourceStub = vi - .spyOn(WebDavUtils, 'getAndSearchItemFromResource') - .mockResolvedValueOnce(folderFixture) - .mockRejectedValue(new Error()); - const getAuthDetailsStub = vi.spyOn(authService, 'getAuthDetails').mockResolvedValue(UserCredentialsFixture); - const uploadMultipartFromStreamStub = vi - .spyOn(networkFacade, 'uploadMultipartFromStream') - .mockResolvedValue([ - Promise.resolve({ fileId: '09218313209', hash: Buffer.from('test') }), - new AbortController(), - ]); - const createDriveFileStub = vi - .spyOn(DriveFileService.instance, 'createFile') - .mockResolvedValue(fileFixture.toItem()); - const createDBFileStub = vi.spyOn(driveDatabaseManager, 'createFile').mockResolvedValue(fileFixture); - - await sut.handle(request, response); - expect(response.status).toHaveBeenCalledWith(200); - expect(getRequestedResourceStub).toHaveBeenCalledTimes(2); - expect(getAndSearchItemFromResourceStub).toHaveBeenCalledTimes(2); - expect(getAuthDetailsStub).toHaveBeenCalledOnce(); - expect(uploadMultipartFromStreamStub).toHaveBeenCalledOnce(); - expect(createDriveFileStub).toHaveBeenCalledOnce(); - expect(createDBFileStub).toHaveBeenCalledOnce(); - }); }); diff --git a/test/webdav/webdav-server.test.ts b/test/webdav/webdav-server.test.ts index 30ae57ab..2f9b574a 100644 --- a/test/webdav/webdav-server.test.ts +++ b/test/webdav/webdav-server.test.ts @@ -7,7 +7,6 @@ import { ConfigService } from '../../src/services/config.service'; import { DriveFolderService } from '../../src/services/drive/drive-folder.service'; import { WebDavServer } from '../../src/webdav/webdav-server'; import { getDriveDatabaseManager } from '../fixtures/drive-database.fixture'; -import { UploadService } from '../../src/services/network/upload.service'; import { DriveFileService } from '../../src/services/drive/drive-file.service'; import { DownloadService } from '../../src/services/network/download.service'; import { AuthService } from '../../src/services/auth.service'; @@ -56,7 +55,6 @@ describe('WebDav server', () => { DriveFileService.instance, DriveFolderService.instance, getDriveDatabaseManager(), - UploadService.instance, DownloadService.instance, AuthService.instance, CryptoService.instance, @@ -93,7 +91,6 @@ describe('WebDav server', () => { DriveFileService.instance, DriveFolderService.instance, getDriveDatabaseManager(), - UploadService.instance, DownloadService.instance, AuthService.instance, CryptoService.instance,