diff --git a/package.json b/package.json index 1af68ea8..352d65a9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "/oclif.manifest.json" ], "dependencies": { + "@dashlane/pqc-kem-kyber512-node": "1.0.0", "@inquirer/prompts": "8.2.0", "@internxt/inxt-js": "2.2.9", "@internxt/lib": "1.4.1", @@ -51,6 +52,7 @@ "express": "5.2.1", "express-async-handler": "1.2.0", "fast-xml-parser": "5.3.3", + "hash-wasm": "4.12.0", "mime-types": "3.0.2", "open": "11.0.0", "openpgp": "6.3.0", diff --git a/src/commands/add-cert.ts b/src/commands/add-cert.ts index 05370994..0b0d4742 100644 --- a/src/commands/add-cert.ts +++ b/src/commands/add-cert.ts @@ -8,7 +8,7 @@ import { WEBDAV_SSL_CERTS_DIR } from '../constants/configs'; export default class AddCert extends Command { static readonly args = {}; static readonly description = 'Add a self-signed certificate to the trusted store for macOS, Linux, and Windows.'; - static readonly aliases = []; + static readonly aliases = ['add:cert']; static readonly examples = ['<%= config.bin %> <%= command.id %>']; static readonly flags = {}; static readonly enableJsonFlag = true; diff --git a/src/commands/config.ts b/src/commands/config.ts index e026a409..4e42b8b5 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,4 +1,4 @@ -import { Command } from '@oclif/core'; +import { Command, Flags } from '@oclif/core'; import { ConfigService } from '../services/config.service'; import { CLIUtils } from '../utils/cli.utils'; import { UsageService } from '../services/usage.service'; @@ -10,26 +10,42 @@ export default class Config extends Command { static readonly description = 'Display useful information from the user logged into the Internxt CLI.'; static readonly aliases = []; static readonly examples = ['<%= config.bin %> <%= command.id %>']; - static readonly flags = {}; + static readonly flags = { + ...CLIUtils.CommonFlags, + extended: Flags.boolean({ + char: 'e', + description: 'Displays additional information in the list.', + required: false, + }), + }; static readonly enableJsonFlag = true; public run = async () => { + const { flags } = await this.parse(Config); + const userCredentials = await ConfigService.instance.readUser(); if (userCredentials?.user) { - const usedSpace = FormatUtils.humanFileSize((await UsageService.instance.fetchUsage()).total); + const usedSpace = FormatUtils.humanFileSize(await UsageService.instance.fetchUsage()); const availableSpace = FormatUtils.formatLimit(await UsageService.instance.fetchSpaceLimit()); const configList = [ { key: 'Email', value: userCredentials.user.email }, + { key: 'User name', value: `${userCredentials.user.name} ${userCredentials.user.lastname}` }, { key: 'Root folder ID', value: userCredentials.user.rootFolderId }, { key: 'Used space', value: usedSpace }, { key: 'Available space', value: availableSpace }, ]; - const header: Header[] = [ + if (flags.extended) { + configList.push( + { key: 'User ID', value: userCredentials.user.uuid }, + { key: 'Created at', value: userCredentials.user.createdAt }, + ); + } + const headers: Header[] = [ { value: 'key', alias: 'Key' }, { value: 'value', alias: 'Value' }, ]; - CLIUtils.table(this.log.bind(this), header, configList); + CLIUtils.table(this.log.bind(this), headers, configList); return { success: true, config: Object.fromEntries(configList.map(({ key, value }) => [key, value])) }; } else { diff --git a/src/commands/create-folder.ts b/src/commands/create-folder.ts index df539e8f..cb5679db 100644 --- a/src/commands/create-folder.ts +++ b/src/commands/create-folder.ts @@ -9,7 +9,7 @@ import { AsyncUtils } from '../utils/async.utils'; export default class CreateFolder extends Command { static readonly args = {}; static readonly description = 'Create a folder in your Internxt Drive'; - static readonly aliases = []; + static readonly aliases = ['create:folder']; static readonly examples = ['<%= config.bin %> <%= command.id %>']; static readonly flags = { ...CLIUtils.CommonFlags, @@ -36,14 +36,12 @@ export default class CreateFolder extends Command { if (!userCredentials) throw new MissingCredentialsError(); const folderName = await this.getFolderName(flags['name'], nonInteractive); - let folderUuid = await this.getFolderUuid(flags['id'], nonInteractive); - if (folderUuid.trim().length === 0) { - // folderId is empty from flags&prompt, which means we should use RootFolderUuid - folderUuid = userCredentials.user.rootFolderId; - } + + const folderUuidFromFlag = await this.getFolderUuid(flags['id'], nonInteractive); + const folderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(folderUuidFromFlag, userCredentials); CLIUtils.doing('Creating folder...', flags['json']); - const [createNewFolder, requestCanceler] = DriveFolderService.instance.createFolder({ + const [createNewFolder, requestCanceler] = await DriveFolderService.instance.createFolder({ plainName: folderName, parentFolderUuid: folderUuid, }); diff --git a/src/commands/download-file.ts b/src/commands/download-file.ts index 688fb38f..0f0d6d01 100644 --- a/src/commands/download-file.ts +++ b/src/commands/download-file.ts @@ -75,13 +75,16 @@ export default class DownloadFile extends Command { // Prepare the network const { user } = await AuthService.instance.getAuthDetails(); - const networkFacade = CLIUtils.prepareNetwork({ loginUserDetails: user, jsonFlag: flags['json'] }); + + CLIUtils.doing('Preparing Network', flags['json']); + const { networkFacade, bucket, mnemonic } = await CLIUtils.prepareNetwork(user); + CLIUtils.done(flags['json']); // Download the file const fileWriteStream = createWriteStream(downloadPath); const [executeDownload, abortable] = await networkFacade.downloadToStream( - driveFile.bucket, - user.mnemonic, + bucket, + mnemonic, driveFile.fileId, driveFile.size, StreamUtils.writeStreamToWritableStream(fileWriteStream), diff --git a/src/commands/list.ts b/src/commands/list.ts index c2c40d63..b0428472 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -35,11 +35,8 @@ export default class List extends Command { const userCredentials = await ConfigService.instance.readUser(); if (!userCredentials) throw new MissingCredentialsError(); - let folderUuid = await this.getFolderUuid(flags['id'], nonInteractive); - if (folderUuid.trim().length === 0) { - // folderId is empty from flags&prompt, which means we should use RootFolderUuid - folderUuid = userCredentials.user.rootFolderId; - } + const folderUuidFromFlag = await this.getFolderUuid(flags['id'], nonInteractive); + const folderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty(folderUuidFromFlag, userCredentials); const { folders, files } = await DriveFolderService.instance.getFolderContent(folderUuid); diff --git a/src/commands/move-file.ts b/src/commands/move-file.ts index 71eff208..b6a01f7e 100644 --- a/src/commands/move-file.ts +++ b/src/commands/move-file.ts @@ -1,7 +1,7 @@ import { Command, Flags } from '@oclif/core'; import { ConfigService } from '../services/config.service'; import { CLIUtils } from '../utils/cli.utils'; -import { MissingCredentialsError, NotValidFileUuidError, NotValidFolderUuidError } from '../types/command.types'; +import { MissingCredentialsError, NotValidFileUuidError } from '../types/command.types'; import { ValidationService } from '../services/validation.service'; import { DriveFileService } from '../services/drive/drive-file.service'; @@ -34,11 +34,17 @@ export default class MoveFile extends Command { if (!userCredentials) throw new MissingCredentialsError(); const fileUuid = await this.getFileUuid(flags['id'], nonInteractive); - let destinationFolderUuid = await this.getDestinationFolderUuid(flags['destination'], nonInteractive); - if (destinationFolderUuid.trim().length === 0) { - // destination id is empty from flags&prompt, which means we should use RootFolderUuid - destinationFolderUuid = userCredentials.user.rootFolderId; - } + + const destinationFolderUuidFromFlag = await CLIUtils.getDestinationFolderUuid({ + destinationFolderUuidFlag: flags['destination'], + destinationFlagName: MoveFile.flags['destination'].name, + nonInteractive, + reporter: this.log.bind(this), + }); + const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty( + destinationFolderUuidFromFlag, + userCredentials, + ); const newFile = await DriveFileService.instance.moveFile(fileUuid, { destinationFolder: destinationFolderUuid }); const message = `File moved successfully to: ${destinationFolderUuid}`; @@ -78,30 +84,4 @@ export default class MoveFile extends Command { ); return fileUuid; }; - - private getDestinationFolderUuid = async ( - destinationFolderUuidFlag: string | undefined, - nonInteractive: boolean, - ): Promise => { - const destinationFolderUuid = await CLIUtils.getValueFromFlag( - { - value: destinationFolderUuidFlag, - name: MoveFile.flags['destination'].name, - }, - { - nonInteractive, - prompt: { - message: 'What is the destination folder id? (leave empty for the root folder)', - options: { type: 'input' }, - }, - }, - { - validate: ValidationService.instance.validateUUIDv4, - error: new NotValidFolderUuidError(), - canBeEmpty: true, - }, - this.log.bind(this), - ); - return destinationFolderUuid; - }; } diff --git a/src/commands/move-folder.ts b/src/commands/move-folder.ts index 3021acc1..d0b9de07 100644 --- a/src/commands/move-folder.ts +++ b/src/commands/move-folder.ts @@ -34,11 +34,17 @@ export default class MoveFolder extends Command { if (!userCredentials) throw new MissingCredentialsError(); const folderUuid = await this.getFolderUuid(flags['id'], nonInteractive); - let destinationFolderUuid = await this.getDestinationFolderUuid(flags['destination'], nonInteractive); - if (destinationFolderUuid.trim().length === 0) { - // destination id is empty from flags&prompt, which means we should use RootFolderUuid - destinationFolderUuid = userCredentials.user.rootFolderId; - } + + const destinationFolderUuidFromFlag = await CLIUtils.getDestinationFolderUuid({ + destinationFolderUuidFlag: flags['destination'], + destinationFlagName: MoveFolder.flags['destination'].name, + nonInteractive, + reporter: this.log.bind(this), + }); + const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty( + destinationFolderUuidFromFlag, + userCredentials, + ); const newFolder = await DriveFolderService.instance.moveFolder(folderUuid, { destinationFolder: destinationFolderUuid, @@ -80,30 +86,4 @@ export default class MoveFolder extends Command { ); return folderUuid; }; - - private getDestinationFolderUuid = async ( - destinationFolderUuidFlag: string | undefined, - nonInteractive: boolean, - ): Promise => { - const destinationFolderUuid = await CLIUtils.getValueFromFlag( - { - value: destinationFolderUuidFlag, - name: MoveFolder.flags['destination'].name, - }, - { - nonInteractive, - prompt: { - message: 'What is the destination folder id? (leave empty for the root folder)', - options: { type: 'input' }, - }, - }, - { - validate: ValidationService.instance.validateUUIDv4, - error: new NotValidFolderUuidError(), - canBeEmpty: true, - }, - this.log.bind(this), - ); - return destinationFolderUuid; - }; } diff --git a/src/commands/trash-restore-file.ts b/src/commands/trash-restore-file.ts index b18ceb4d..b6945956 100644 --- a/src/commands/trash-restore-file.ts +++ b/src/commands/trash-restore-file.ts @@ -1,7 +1,7 @@ import { Command, Flags } from '@oclif/core'; import { ConfigService } from '../services/config.service'; import { CLIUtils } from '../utils/cli.utils'; -import { MissingCredentialsError, NotValidFileUuidError, NotValidFolderUuidError } from '../types/command.types'; +import { MissingCredentialsError, NotValidFileUuidError } from '../types/command.types'; import { ValidationService } from '../services/validation.service'; import { DriveFileService } from '../services/drive/drive-file.service'; @@ -35,12 +35,17 @@ export default class TrashRestoreFile extends Command { if (!userCredentials) throw new MissingCredentialsError(); const fileUuid = await this.getFileUuid(flags['id'], nonInteractive); - let destinationFolderUuid = await this.getDestinationFolderUuid(flags['destination'], nonInteractive); - if (destinationFolderUuid.trim().length === 0) { - // destinationFolderUuid is empty from flags&prompt, which means we should use RootFolderUuid - destinationFolderUuid = userCredentials.user.rootFolderId; - } + const destinationFolderUuidFromFlag = await CLIUtils.getDestinationFolderUuid({ + destinationFolderUuidFlag: flags['destination'], + destinationFlagName: TrashRestoreFile.flags['destination'].name, + nonInteractive, + reporter: this.log.bind(this), + }); + const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty( + destinationFolderUuidFromFlag, + userCredentials, + ); const file = await DriveFileService.instance.moveFile(fileUuid, { destinationFolder: destinationFolderUuid }); const message = `File restored successfully to: ${destinationFolderUuid}`; @@ -80,30 +85,4 @@ export default class TrashRestoreFile extends Command { ); return fileUuid; }; - - private getDestinationFolderUuid = async ( - destinationFolderUuidFlag: string | undefined, - nonInteractive: boolean, - ): Promise => { - const destinationFolderUuid = await CLIUtils.getValueFromFlag( - { - value: destinationFolderUuidFlag, - name: TrashRestoreFile.flags['destination'].name, - }, - { - nonInteractive, - prompt: { - message: 'What is the destination folder id? (leave empty for the root folder)', - options: { type: 'input' }, - }, - }, - { - validate: ValidationService.instance.validateUUIDv4, - error: new NotValidFolderUuidError(), - canBeEmpty: true, - }, - this.log.bind(this), - ); - return destinationFolderUuid; - }; } diff --git a/src/commands/trash-restore-folder.ts b/src/commands/trash-restore-folder.ts index f20b060f..532549cc 100644 --- a/src/commands/trash-restore-folder.ts +++ b/src/commands/trash-restore-folder.ts @@ -35,12 +35,17 @@ export default class TrashRestoreFolder extends Command { if (!userCredentials) throw new MissingCredentialsError(); const folderUuid = await this.getFolderUuid(flags['id'], nonInteractive); - let destinationFolderUuid = await this.getDestinationFolderUuid(flags['destination'], nonInteractive); - if (destinationFolderUuid.trim().length === 0) { - // destinationFolderUuid is empty from flags&prompt, which means we should use RootFolderUuid - destinationFolderUuid = userCredentials.user.rootFolderId; - } + const destinationFolderUuidFromFlag = await CLIUtils.getDestinationFolderUuid({ + destinationFolderUuidFlag: flags['destination'], + destinationFlagName: TrashRestoreFolder.flags['destination'].name, + nonInteractive, + reporter: this.log.bind(this), + }); + const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty( + destinationFolderUuidFromFlag, + userCredentials, + ); const folder = await DriveFolderService.instance.moveFolder(folderUuid, { destinationFolder: destinationFolderUuid, @@ -82,30 +87,4 @@ export default class TrashRestoreFolder extends Command { ); return folderUuid; }; - - private getDestinationFolderUuid = async ( - destinationFolderUuidFlag: string | undefined, - nonInteractive: boolean, - ): Promise => { - const destinationFolderUuid = await CLIUtils.getValueFromFlag( - { - value: destinationFolderUuidFlag, - name: TrashRestoreFolder.flags['destination'].name, - }, - { - nonInteractive, - prompt: { - message: 'What is the destination folder id? (leave empty for the root folder)', - options: { type: 'input' }, - }, - }, - { - validate: ValidationService.instance.validateUUIDv4, - error: new NotValidFolderUuidError(), - canBeEmpty: true, - }, - this.log.bind(this), - ); - return destinationFolderUuid; - }; } diff --git a/src/commands/upload-file.ts b/src/commands/upload-file.ts index 6b2c71d0..d671e985 100644 --- a/src/commands/upload-file.ts +++ b/src/commands/upload-file.ts @@ -1,17 +1,17 @@ import { Command, Flags } from '@oclif/core'; import { stat } from 'node:fs/promises'; import { createReadStream } from 'node:fs'; -import { AuthService } from '../services/auth.service'; 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 { NotValidDirectoryError } from '../types/command.types'; +import { MissingCredentialsError, NotValidFileError } from '../types/command.types'; import { ValidationService } from '../services/validation.service'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { BufferStream } from '../utils/stream.utils'; -import { isFileThumbnailable, tryUploadThumbnail } from '../utils/thumbnail.utils'; import { Readable } from 'node:stream'; +import { ThumbnailUtils } from '../utils/thumbnail.utils'; +import { ThumbnailService } from '../services/thumbnail.service'; export default class UploadFile extends Command { static readonly args = {}; @@ -39,7 +39,8 @@ export default class UploadFile extends Command { const nonInteractive = flags['non-interactive']; - const { user } = await AuthService.instance.getAuthDetails(); + const userCredentials = await ConfigService.instance.readUser(); + if (!userCredentials) throw new MissingCredentialsError(); const filePath = await this.getFilePath(flags['file'], nonInteractive); @@ -48,14 +49,16 @@ export default class UploadFile extends Command { const fileInfo = path.parse(filePath); const fileType = fileInfo.ext.replaceAll('.', ''); - // If destinationFolderUuid is empty from flags&prompt, means we should use RootFolderUuid - const destinationFolderUuid = - (await CLIUtils.getDestinationFolderUuid({ - destinationFolderUuidFlag: flags['destination'], - destinationFlagName: UploadFile.flags['destination'].name, - nonInteractive, - reporter: this.log.bind(this), - })) ?? user.rootFolderId; + const destinationFolderUuidFromFlag = await CLIUtils.getDestinationFolderUuid({ + destinationFolderUuidFlag: flags['destination'], + destinationFlagName: UploadFile.flags['destination'].name, + nonInteractive, + reporter: this.log.bind(this), + }); + const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty( + destinationFolderUuidFromFlag, + userCredentials, + ); const timings = { networkUpload: 0, @@ -64,7 +67,9 @@ export default class UploadFile extends Command { }; // Prepare the network - const networkFacade = CLIUtils.prepareNetwork({ loginUserDetails: user, jsonFlag: flags['json'] }); + CLIUtils.doing('Preparing Network', flags['json']); + const { networkFacade, bucket } = await CLIUtils.prepareNetwork(userCredentials.user); + CLIUtils.done(flags['json']); const networkUploadTimer = CLIUtils.timer(); const progressBar = CLIUtils.progress( @@ -78,7 +83,7 @@ export default class UploadFile extends Command { let fileId: string | undefined; let bufferStream: BufferStream | undefined; - const isThumbnailable = isFileThumbnailable(fileType); + const isThumbnailable = ThumbnailUtils.isFileThumbnailable(fileType); const fileSize = stats.size ?? 0; if (fileSize > 0) { @@ -99,7 +104,7 @@ export default class UploadFile extends Command { const state = networkFacade.uploadFile( fileStream, fileSize, - user.bucket, + bucket, (err: Error | null, res: string | null) => { if (err) { return reject(err); @@ -123,8 +128,8 @@ export default class UploadFile extends Command { type: fileType, size: fileSize, folderUuid: destinationFolderUuid, - fileId: fileId, - bucket: user.bucket, + fileId, + bucket, encryptVersion: EncryptionVersion.Aes03, creationTime: stats.birthtime?.toISOString(), modificationTime: stats.mtime?.toISOString(), @@ -133,10 +138,10 @@ export default class UploadFile extends Command { const thumbnailTimer = CLIUtils.timer(); if (fileSize > 0 && isThumbnailable && bufferStream) { - void tryUploadThumbnail({ + void ThumbnailService.instance.tryUploadThumbnail({ bufferStream, fileType, - userBucket: user.bucket, + bucket, fileUuid: createdDriveFile.uuid, networkFacade, }); @@ -151,10 +156,10 @@ export default class UploadFile extends Command { this.log('\n'); this.log( - `[PUT] Timing breakdown:\n - Network upload: ${CLIUtils.formatDuration(timings.networkUpload)} (${throughputMBps.toFixed(2)} MB/s)\n - Drive upload: ${CLIUtils.formatDuration(timings.driveUpload)}\n - Thumbnail: ${CLIUtils.formatDuration(timings.thumbnailUpload)}\n`, + '[PUT] Timing breakdown:\n' + + `Network upload: ${CLIUtils.formatDuration(timings.networkUpload)} (${throughputMBps.toFixed(2)} MB/s)\n` + + `Drive upload: ${CLIUtils.formatDuration(timings.driveUpload)}\n` + + `Thumbnail: ${CLIUtils.formatDuration(timings.thumbnailUpload)}\n`, ); this.log('\n'); const message = @@ -197,7 +202,7 @@ export default class UploadFile extends Command { }, { validate: ValidationService.instance.validateFileExists, - error: new NotValidDirectoryError(), + error: new NotValidFileError(), }, this.log.bind(this), ); diff --git a/src/commands/upload-folder.ts b/src/commands/upload-folder.ts index c810cd8e..a7061374 100644 --- a/src/commands/upload-folder.ts +++ b/src/commands/upload-folder.ts @@ -1,10 +1,9 @@ import { Command, Flags } from '@oclif/core'; import { CLIUtils } from '../utils/cli.utils'; -import { AuthService } from '../services/auth.service'; import { ValidationService } from '../services/validation.service'; import { ConfigService } from '../services/config.service'; import { UploadFacade } from '../services/network/upload/upload-facade.service'; -import { NotValidDirectoryError } from '../types/command.types'; +import { MissingCredentialsError, NotValidDirectoryError } from '../types/command.types'; export default class UploadFolder extends Command { static readonly args = {}; @@ -28,18 +27,23 @@ export default class UploadFolder extends Command { static readonly enableJsonFlag = true; public run = async () => { - const { user } = await AuthService.instance.getAuthDetails(); const { flags } = await this.parse(UploadFolder); + + const userCredentials = await ConfigService.instance.readUser(); + if (!userCredentials) throw new MissingCredentialsError(); + const localPath = await this.getFolderPath(flags['folder'], flags['non-interactive']); - // If destinationFolderUuid is empty from flags&prompt, means we should use RootFolderUuid - const destinationFolderUuid = - (await CLIUtils.getDestinationFolderUuid({ - destinationFolderUuidFlag: flags['destination'], - destinationFlagName: UploadFolder.flags['destination'].name, - nonInteractive: flags['non-interactive'], - reporter: this.log.bind(this), - })) ?? user.rootFolderId; + const destinationFolderUuidFromFlag = await CLIUtils.getDestinationFolderUuid({ + destinationFolderUuidFlag: flags['destination'], + destinationFlagName: UploadFolder.flags['destination'].name, + nonInteractive: flags['non-interactive'], + reporter: this.log.bind(this), + }); + const destinationFolderUuid = await CLIUtils.fallbackToRootFolderIdIfEmpty( + destinationFolderUuidFromFlag, + userCredentials, + ); const progressBar = CLIUtils.progress( { @@ -52,7 +56,7 @@ export default class UploadFolder extends Command { const data = await UploadFacade.instance.uploadFolder({ localPath, destinationFolderUuid, - loginUserDetails: user, + loginUserDetails: userCredentials.user, jsonFlag: flags['json'], onProgress: (progress) => { progressBar?.update(progress.percentage); diff --git a/src/commands/webdav.ts b/src/commands/webdav.ts index 69e9ee43..5a8d342e 100644 --- a/src/commands/webdav.ts +++ b/src/commands/webdav.ts @@ -8,14 +8,14 @@ export default class Webdav extends Command { static readonly args = { action: Args.string({ required: true, - options: ['enable', 'disable', 'restart', 'status'], + options: ['enable', 'start', 'disable', 'stop', 'restart', 'status'], }), }; - static readonly description = 'Enable, disable, restart or get the status of the Internxt CLI WebDav server'; + static readonly description = 'Start, stop, restart or get the status of the Internxt CLI WebDAV server'; static readonly aliases = []; static readonly examples = [ - '<%= config.bin %> <%= command.id %> enable', - '<%= config.bin %> <%= command.id %> disable', + '<%= config.bin %> <%= command.id %> enable | start', + '<%= config.bin %> <%= command.id %> disable | stop', '<%= config.bin %> <%= command.id %> restart', '<%= config.bin %> <%= command.id %> status', ]; @@ -29,12 +29,14 @@ export default class Webdav extends Command { let success = true; await PM2Utils.connect(); switch (args.action) { + case 'start': case 'enable': { await AuthService.instance.getAuthDetails(); message = await this.enableWebDav(flags['json']); break; } + case 'stop': case 'disable': { message = await this.disableWebDav(flags['json']); break; @@ -47,7 +49,6 @@ export default class Webdav extends Command { } case 'status': { - await AuthService.instance.getAuthDetails(); message = await this.webDAVStatus(); break; } diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 235e2ce4..2710ee66 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -29,7 +29,11 @@ export default class Whoami extends Command { } else { if (validCreds.refreshRequired) { try { - await AuthService.instance.refreshUserToken(userCredentials.token, userCredentials.user.mnemonic); + const refreshedCreds = await AuthService.instance.refreshUserToken( + userCredentials.token, + userCredentials.user.mnemonic, + ); + await ConfigService.instance.saveUser(refreshedCreds); } catch { /* noop */ } diff --git a/src/commands/workspaces-list.ts b/src/commands/workspaces-list.ts new file mode 100644 index 00000000..2a1f5c56 --- /dev/null +++ b/src/commands/workspaces-list.ts @@ -0,0 +1,78 @@ +import { Command, Flags } from '@oclif/core'; +import { ConfigService } from '../services/config.service'; +import { CLIUtils } from '../utils/cli.utils'; +import { MissingCredentialsError, PaginatedWorkspace } from '../types/command.types'; +import { WorkspaceService } from '../services/drive/workspace.service'; +import { Header } from 'tty-table'; +import { FormatUtils } from '../utils/format.utils'; + +export default class WorkspacesList extends Command { + static readonly args = {}; + static readonly description = 'Get the list of workspaces.'; + static readonly aliases = ['workspaces:list']; + static readonly examples = ['<%= config.bin %> <%= command.id %>']; + static readonly flags = { + ...CLIUtils.CommonFlags, + extended: Flags.boolean({ + char: 'e', + description: 'Displays additional information in the list.', + required: false, + }), + }; + static readonly enableJsonFlag = true; + + public run = async () => { + const { flags } = await this.parse(WorkspacesList); + + const userCredentials = await ConfigService.instance.readUser(); + if (!userCredentials) throw new MissingCredentialsError(); + + const workspaces = await WorkspaceService.instance.getAvailableWorkspaces(userCredentials.user); + + const allItems: PaginatedWorkspace[] = workspaces.map((workspaceData) => { + const totalUsedSpace = + Number(workspaceData.workspaceUser?.driveUsage ?? 0) + Number(workspaceData.workspaceUser?.backupsUsage ?? 0); + const spaceLimit = Number(workspaceData.workspaceUser?.spaceLimit ?? 0); + const usedSpace = FormatUtils.humanFileSize(totalUsedSpace); + const availableSpace = FormatUtils.formatLimit(spaceLimit); + + return { + name: workspaceData.workspace.name, + id: workspaceData.workspace.id, + usedSpace, + availableSpace, + owner: workspaceData.workspace.ownerId, + address: workspaceData.workspace.address, + created: FormatUtils.formatDate(workspaceData.workspace.createdAt), + }; + }); + + const headers: Header[] = [ + { value: 'name', alias: 'Name' }, + { value: 'id', alias: 'Workspace ID' }, + { value: 'usedSpace', alias: 'Used space' }, + { value: 'availableSpace', alias: 'Available space' }, + ]; + if (flags.extended) { + headers.push( + { value: 'owner', alias: 'Owner ID' }, + { value: 'address', alias: 'Address' }, + { value: 'created', alias: 'Created at' }, + ); + } + CLIUtils.table(this.log.bind(this), headers, allItems); + + return { success: true, list: { workspaces } }; + }; + + public catch = async (error: Error) => { + const { flags } = await this.parse(WorkspacesList); + CLIUtils.catchError({ + error, + command: this.id, + logReporter: this.log.bind(this), + jsonFlag: flags['json'], + }); + this.exit(1); + }; +} diff --git a/src/commands/workspaces-unset.ts b/src/commands/workspaces-unset.ts new file mode 100644 index 00000000..a53a3d60 --- /dev/null +++ b/src/commands/workspaces-unset.ts @@ -0,0 +1,42 @@ +import { Command } from '@oclif/core'; +import { ConfigService } from '../services/config.service'; +import { CLIUtils } from '../utils/cli.utils'; +import { MissingCredentialsError } from '../types/command.types'; +import { SdkManager } from '../services/sdk-manager.service'; + +export default class WorkspacesUnset extends Command { + static readonly args = {}; + static readonly description = + 'Unset the active workspace context for the current user session. ' + + 'Once a workspace is unset, all subsequent commands (list, upload, download, etc.) ' + + 'will operate within the personal drive space until it is changed or set again.'; + static readonly aliases = ['workspaces:unset']; + static readonly examples = ['<%= config.bin %> <%= command.id %>']; + static readonly flags = { + ...CLIUtils.CommonFlags, + }; + static readonly enableJsonFlag = true; + + public run = async () => { + await this.parse(WorkspacesUnset); + + const userCredentials = await ConfigService.instance.readUser(); + if (!userCredentials) throw new MissingCredentialsError(); + + SdkManager.init({ token: userCredentials.token }); + await ConfigService.instance.saveUser({ ...userCredentials, workspace: undefined }); + CLIUtils.success(this.log.bind(this), 'Personal drive space selected successfully.'); + return { success: true, message: 'Personal drive space selected successfully.' }; + }; + + public catch = async (error: Error) => { + const { flags } = await this.parse(WorkspacesUnset); + CLIUtils.catchError({ + error, + command: this.id, + logReporter: this.log.bind(this), + jsonFlag: flags['json'], + }); + this.exit(1); + }; +} diff --git a/src/commands/workspaces-use.ts b/src/commands/workspaces-use.ts new file mode 100644 index 00000000..bdbc0084 --- /dev/null +++ b/src/commands/workspaces-use.ts @@ -0,0 +1,125 @@ +import { Command, Flags } from '@oclif/core'; +import { ConfigService } from '../services/config.service'; +import { CLIUtils } from '../utils/cli.utils'; +import { MissingCredentialsError, NotValidWorkspaceUuidError } from '../types/command.types'; +import { WorkspaceService } from '../services/drive/workspace.service'; +import { FormatUtils } from '../utils/format.utils'; +import { ValidationService } from '../services/validation.service'; +import { SdkManager } from '../services/sdk-manager.service'; + +export default class WorkspacesUse extends Command { + static readonly args = {}; + static readonly description = + 'Set the active workspace context for the current user session. ' + + 'Once a workspace is selected, all subsequent commands (list, upload, download, etc.) ' + + 'will operate within that workspace until it is changed or unset.'; + static readonly aliases = ['workspaces:use']; + static readonly examples = ['<%= config.bin %> <%= command.id %>']; + static readonly flags = { + ...CLIUtils.CommonFlags, + id: Flags.string({ + char: 'i', + description: + 'The id of the workspace to activate. ' + + 'Use <%= config.bin %> workspaces list to view your available workspace ids.' + + 'If the ID is "personal" the personal drive space will be selected.', + required: false, + exclusive: ['personal'], + }), + }; + static readonly enableJsonFlag = true; + + public run = async () => { + const { flags } = await this.parse(WorkspacesUse); + const nonInteractive = flags['non-interactive']; + + const userCredentials = await ConfigService.instance.readUser(); + if (!userCredentials) throw new MissingCredentialsError(); + + if (flags['id']?.trim().toLowerCase() === 'personal') { + SdkManager.init({ token: userCredentials.token }); + await ConfigService.instance.saveUser({ ...userCredentials, workspace: undefined }); + CLIUtils.success(this.log.bind(this), 'Personal drive space selected successfully.'); + return { success: true, message: 'Personal drive space selected successfully.' }; + } + + const workspaces = await WorkspaceService.instance.getAvailableWorkspaces(userCredentials.user); + const availableWorkspaces: string[] = workspaces.map((workspaceData) => { + const name = workspaceData.workspace.name; + const id = workspaceData.workspace.id; + const totalUsedSpace = + Number(workspaceData.workspaceUser?.driveUsage ?? 0) + Number(workspaceData.workspaceUser?.backupsUsage ?? 0); + const spaceLimit = Number(workspaceData.workspaceUser?.spaceLimit ?? 0); + const usedSpace = FormatUtils.humanFileSize(totalUsedSpace); + const availableSpace = FormatUtils.formatLimit(spaceLimit); + + return `[${id}] Name: ${name} | Used Space: ${usedSpace} | Available Space: ${availableSpace}`; + }); + const workspaceUuid = await this.getWorkspaceUuid(flags['id'], availableWorkspaces, nonInteractive); + + const workspaceCredentials = await WorkspaceService.instance.getWorkspaceCredentials(workspaceUuid); + const selectedWorkspace = workspaces.find((workspace) => workspace.workspace.id === workspaceUuid); + if (!selectedWorkspace) throw new NotValidWorkspaceUuidError(); + + SdkManager.init({ token: userCredentials.token, workspaceToken: workspaceCredentials.token }); + + await ConfigService.instance.saveUser({ + ...userCredentials, + workspace: { + workspaceCredentials, + workspaceData: selectedWorkspace, + }, + }); + + const message = + `Workspace ${workspaceUuid} selected successfully. Now all drive commands (list, upload, download, etc.) ` + + 'will operate within this workspace until it is changed or unset.'; + CLIUtils.success(this.log.bind(this), message); + + return { success: true, list: { workspaces } }; + }; + + public catch = async (error: Error) => { + const { flags } = await this.parse(WorkspacesUse); + CLIUtils.catchError({ + error, + command: this.id, + logReporter: this.log.bind(this), + jsonFlag: flags['json'], + }); + this.exit(1); + }; + + private getWorkspaceUuid = async ( + workspaceUuidFlag: string | undefined, + availableWorkspaces: string[], + nonInteractive: boolean, + ): Promise => { + const workspaceUuid = await CLIUtils.getValueFromFlag( + { + value: workspaceUuidFlag ? `[${workspaceUuidFlag}]` : undefined, + name: WorkspacesUse.flags['id'].name, + }, + { + nonInteractive, + prompt: { + message: 'What is the workspace you want to use?', + options: { + type: 'list', + choices: { values: availableWorkspaces }, + }, + }, + }, + { + validate: (value: string) => + ValidationService.instance.validateUUIDv4(this.extractUuidFromWorkspaceString(value)), + error: new NotValidWorkspaceUuidError(), + }, + this.log.bind(this), + ); + return this.extractUuidFromWorkspaceString(workspaceUuid); + }; + + private extractUuidFromWorkspaceString = (workspaceString: string) => + workspaceString.match(/\[(.*?)\]/)?.[1] ?? workspaceString; +} diff --git a/src/hooks/prerun/auth_check.ts b/src/hooks/prerun/auth_check.ts index c9d49874..ad2a071a 100644 --- a/src/hooks/prerun/auth_check.ts +++ b/src/hooks/prerun/auth_check.ts @@ -18,8 +18,8 @@ const hook: Hook<'prerun'> = async function (opts) { if (!CommandsToSkip.map((command) => command.name).includes(Command.name)) { CLIUtils.doing('Checking credentials', jsonFlag); try { - const { token } = await AuthService.instance.getAuthDetails(); - SdkManager.init({ token }); + const { token, workspace } = await AuthService.instance.getAuthDetails(); + SdkManager.init({ token, workspaceToken: workspace?.workspaceCredentials.token }); CLIUtils.done(jsonFlag); CLIUtils.clearPreviousLine(jsonFlag); } catch (error) { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index bcf1ef42..e95e2cad 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -7,8 +7,10 @@ import { InvalidCredentialsError, LoginCredentials, MissingCredentialsError, + Workspace, } from '../types/command.types'; import { ValidationService } from './validation.service'; +import { WorkspaceService } from './drive/workspace.service'; export class AuthService { public static readonly instance: AuthService = new AuthService(); @@ -67,7 +69,7 @@ export class AuthService { * @throws {ExpiredCredentialsError} When token has expired */ public getAuthDetails = async (): Promise => { - const loginCreds = await ConfigService.instance.readUser(); + let loginCreds = await ConfigService.instance.readUser(); if (!loginCreds?.token || !loginCreds?.user?.mnemonic) { throw new MissingCredentialsError(); } @@ -82,19 +84,25 @@ export class AuthService { throw new ExpiredCredentialsError(); } - if (!tokenDetails.expiration.refreshRequired) { - return loginCreds; - } - try { - return await this.refreshUserToken(loginCreds.token, loginCreds.user.mnemonic); - } catch (error) { - await ConfigService.instance.clearUser(); - throw error; + if (tokenDetails.expiration.refreshRequired) { + try { + loginCreds = await this.refreshUserToken(loginCreds.token, loginCreds.user.mnemonic); + } catch (error) { + await ConfigService.instance.clearUser(); + throw error; + } } + + const workspaceCreds = await this.refreshWorkspaceCredentials(loginCreds); + loginCreds.workspace = workspaceCreds; + + SdkManager.init({ token: loginCreds.token, workspaceToken: workspaceCreds?.workspaceCredentials.token }); + await ConfigService.instance.saveUser(loginCreds); + return loginCreds; }; /** - * Refreshes the user tokens and stores them in the credentials file + * Refreshes the user tokens * * @returns The user details and the renewed auth token * @throws {InvalidCredentialsError} When the mnemonic is invalid @@ -120,10 +128,51 @@ export class AuthService { token: newCreds.newToken, }; - await ConfigService.instance.saveUser(newLoginCreds); return newLoginCreds; }; + /** + * Returns the workspace details and refreshes them if needed + * + * @returns The workspace details and the renewed auth token + * @throws {InvalidCredentialsError} When the workspace token is invalid + * @throws {ExpiredCredentialsError} When the workspace token has expired + */ + public refreshWorkspaceCredentials = async (loginCreds: LoginCredentials): Promise => { + if (loginCreds.workspace?.workspaceCredentials && loginCreds.workspace?.workspaceData) { + const workspaceToken = loginCreds.workspace.workspaceCredentials.token; + const workspaceUuid = loginCreds.workspace.workspaceCredentials.id; + const workspaceTokenDetails = ValidationService.instance.validateTokenAndCheckExpiration(workspaceToken); + + if (!workspaceTokenDetails.isValid) { + throw new InvalidCredentialsError(); + } + if (workspaceTokenDetails.expiration.expired) { + throw new ExpiredCredentialsError(); + } + + if (workspaceTokenDetails.expiration.refreshRequired) { + SdkManager.init({ token: loginCreds.token, workspaceToken: loginCreds.workspace.workspaceCredentials.token }); + const workspaceCredentials = await WorkspaceService.instance.getWorkspaceCredentials(workspaceUuid); + // TODO refresh also workspace data + return { + workspaceCredentials, + workspaceData: loginCreds.workspace.workspaceData, + }; + } else { + return loginCreds.workspace; + } + } + }; + + public getCurrentWorkspace = async (): Promise => { + const loginCreds = await ConfigService.instance.readUser(); + if (!loginCreds?.token || !loginCreds?.user?.mnemonic) { + throw new MissingCredentialsError(); + } + return loginCreds.workspace; + }; + /** * Logs the user out of the application by invoking the logout method * from the authentication client. This will terminate the user's session diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 136e5830..835fafbe 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -2,7 +2,7 @@ import fs from 'node:fs/promises'; import { ConfigKeys } from '../types/config.types'; import { LoginCredentials, WebdavConfig } from '../types/command.types'; import { CryptoService } from './crypto.service'; -import { isFileNotFoundError } from '../utils/errors.utils'; +import { ErrorUtils } from '../utils/errors.utils'; import { CREDENTIALS_FILE, INTERNXT_CLI_DATA_DIR, @@ -53,11 +53,12 @@ export class ConfigService { if (stat.size === 0) return; await fs.writeFile(CREDENTIALS_FILE, '', 'utf8'); } catch (error) { - if (!isFileNotFoundError(error)) { + if (!ErrorUtils.isFileNotFoundError(error)) { throw error; } } }; + /** * Returns the authenticated user credentials * @returns {CLICredentials} The authenticated user credentials diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index cd8dcc0e..253d838a 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -4,6 +4,8 @@ import { createCipheriv, createDecipheriv, createHash, Decipheriv, pbkdf2Sync, r import { KeysService } from './keys.service'; import { ConfigService } from '../services/config.service'; import { StreamUtils } from '../utils/stream.utils'; +import { LoginCredentials } from '../types/command.types'; +import { WorkspaceData } from '@internxt/sdk/dist/workspaces'; export class CryptoService { public static readonly instance: CryptoService = new CryptoService(); @@ -188,4 +190,40 @@ export class CryptoService { const iv = md5Hashes[2]; return { key, iv }; }; + + private readonly decryptMnemonic = async (encryptionKey: string, user: LoginCredentials['user']): Promise => { + let decryptedKey: string | undefined; + const privateKeyInBase64 = user.keys?.ecc?.privateKey ?? ''; + const privateKyberKeyInBase64 = user.keys?.kyber?.privateKey ?? ''; + try { + decryptedKey = await KeysService.instance.hybridDecryptMessageWithPrivateKey({ + encryptedMessageInBase64: encryptionKey, + privateKeyInBase64, + privateKyberKeyInBase64, + }); + } catch { + // noop + } + if (!decryptedKey) { + decryptedKey = user.mnemonic; + } + return decryptedKey; + }; + + public decryptWorkspacesMnemonic = async ( + workspaces: WorkspaceData[], + user: LoginCredentials['user'], + ): Promise => { + return await Promise.all( + workspaces.map(async (workspace) => { + return { + ...workspace, + workspaceUser: { + ...workspace.workspaceUser, + key: await this.decryptMnemonic(workspace.workspaceUser.key, user), + }, + }; + }), + ); + }; } diff --git a/src/services/drive/drive-file.service.ts b/src/services/drive/drive-file.service.ts index 3c64d948..709744a9 100644 --- a/src/services/drive/drive-file.service.ts +++ b/src/services/drive/drive-file.service.ts @@ -2,13 +2,36 @@ import { StorageTypes } from '@internxt/sdk/dist/drive'; import { SdkManager } from '../sdk-manager.service'; import { DriveFileItem } from '../../types/drive.types'; import { DriveUtils } from '../../utils/drive.utils'; +import { AuthService } from '../auth.service'; export class DriveFileService { static readonly instance = new DriveFileService(); public createFile = async (payload: StorageTypes.FileEntryByUuid): Promise => { - const storageClient = SdkManager.instance.getStorage(); - const driveFile = await storageClient.createFileEntryByUuid(payload); + let driveFile: StorageTypes.DriveFileData; + + const currentWorkspace = await AuthService.instance.getCurrentWorkspace(); + if (currentWorkspace) { + const workspaceClient = SdkManager.instance.getWorkspaces(); + driveFile = await workspaceClient.createFileEntry( + { + name: payload.plainName, + plainName: payload.plainName, + bucket: payload.bucket, + fileId: payload.fileId ?? '', + encryptVersion: StorageTypes.EncryptionVersion.Aes03, + folderUuid: payload.folderUuid, + size: payload.size, + type: payload.type ?? '', + modificationTime: payload.modificationTime ?? new Date().toISOString(), + date: payload.date ?? new Date().toISOString(), + }, + currentWorkspace.workspaceCredentials.id, + ); + } else { + const storageClient = SdkManager.instance.getStorage(); + driveFile = await storageClient.createFileEntryByUuid(payload); + } return { itemType: 'file', diff --git a/src/services/drive/drive-folder.service.ts b/src/services/drive/drive-folder.service.ts index 2437d793..e828bfb2 100644 --- a/src/services/drive/drive-folder.service.ts +++ b/src/services/drive/drive-folder.service.ts @@ -1,9 +1,11 @@ import { FetchPaginatedFile, FetchPaginatedFolder } from '@internxt/sdk/dist/drive/storage/types'; import { SdkManager } from '../sdk-manager.service'; -import { Storage, StorageTypes } from '@internxt/sdk/dist/drive'; +import { StorageTypes } from '@internxt/sdk/dist/drive'; import { DriveFolderItem } from '../../types/drive.types'; import { DriveUtils } from '../../utils/drive.utils'; import { RequestCanceler } from '@internxt/sdk/dist/shared/http/types'; +import { AuthService } from '../auth.service'; +import { WorkspaceCredentialsDetails } from '../../types/command.types'; export class DriveFolderService { static readonly instance = new DriveFolderService(); @@ -21,37 +23,76 @@ export class DriveFolderService { }; public getFolderContent = async (folderUuid: string) => { - const storageClient = SdkManager.instance.getStorage(); - const folders = await this.getAllSubfolders(storageClient, folderUuid, 0); - const files = await this.getAllSubfiles(storageClient, folderUuid, 0); + const currentWorkspace = await AuthService.instance.getCurrentWorkspace(); + const currentWorkspaceCreds = currentWorkspace?.workspaceCredentials; + const folders = await this.getAllSubfolders(currentWorkspaceCreds, folderUuid, 0); + const files = await this.getAllSubfiles(currentWorkspaceCreds, folderUuid, 0); return { folders, files }; }; private readonly getAllSubfolders = async ( - storageClient: Storage, + currentWorkspace: WorkspaceCredentialsDetails | undefined, folderUuid: string, offset: number, ): Promise => { - const [folderContentPromise] = storageClient.getFolderFoldersByUuid(folderUuid, offset, 50, 'plainName', 'ASC'); - const { folders } = await folderContentPromise; + let folders: FetchPaginatedFolder[]; + + if (currentWorkspace) { + const workspaceClient = SdkManager.instance.getWorkspaces(); + const [workspaceContentPromise] = workspaceClient.getFolders( + currentWorkspace.id, + folderUuid, + offset, + 50, + 'plainName', + 'ASC', + ); + folders = (await workspaceContentPromise).result as unknown as FetchPaginatedFolder[]; + } else { + const storageClient = SdkManager.instance.getStorage(); + const [personalFolderContentPromise] = storageClient.getFolderFoldersByUuid( + folderUuid, + offset, + 50, + 'plainName', + 'ASC', + ); + folders = (await personalFolderContentPromise).folders; + } if (folders.length > 0) { - return folders.concat(await this.getAllSubfolders(storageClient, folderUuid, offset + folders.length)); + return folders.concat(await this.getAllSubfolders(currentWorkspace, folderUuid, offset + folders.length)); } else { return folders; } }; private readonly getAllSubfiles = async ( - storageClient: Storage, + currentWorkspace: WorkspaceCredentialsDetails | undefined, folderUuid: string, offset: number, ): Promise => { - const [folderContentPromise] = storageClient.getFolderFilesByUuid(folderUuid, offset, 50, 'plainName', 'ASC'); - const { files } = await folderContentPromise; + let files: FetchPaginatedFile[]; + + if (currentWorkspace) { + const workspaceClient = SdkManager.instance.getWorkspaces(); + const [workspaceContentPromise] = workspaceClient.getFiles( + currentWorkspace.id, + folderUuid, + offset, + 50, + 'plainName', + 'ASC', + ); + files = (await workspaceContentPromise).result as unknown as FetchPaginatedFile[]; + } else { + const storageClient = SdkManager.instance.getStorage(); + const [folderContentPromise] = storageClient.getFolderFilesByUuid(folderUuid, offset, 50, 'plainName', 'ASC'); + files = (await folderContentPromise).files; + } if (files.length > 0) { - return files.concat(await this.getAllSubfiles(storageClient, folderUuid, offset + files.length)); + return files.concat(await this.getAllSubfiles(currentWorkspace, folderUuid, offset + files.length)); } else { return files; } @@ -70,12 +111,22 @@ export class DriveFolderService { * @param {number} payload.parentFolderId - The ID of the parent folder. * @return {[Promise, RequestCanceler]} - A tuple containing a promise that resolves to the response of creating the folder and a request canceler. */ - public createFolder( + public createFolder = async ( payload: StorageTypes.CreateFolderByUuidPayload, - ): [Promise, RequestCanceler] { - const storageClient = SdkManager.instance.getStorage(); - return storageClient.createFolderByUuid(payload); - } + ): Promise<[Promise, RequestCanceler]> => { + const currentWorkspace = await AuthService.instance.getCurrentWorkspace(); + if (currentWorkspace) { + const workspaceClient = SdkManager.instance.getWorkspaces(); + return workspaceClient.createFolder({ + workspaceId: currentWorkspace.workspaceCredentials.id, + parentFolderUuid: payload.parentFolderUuid, + plainName: payload.plainName, + }); + } else { + const storageClient = SdkManager.instance.getStorage(); + return storageClient.createFolderByUuid(payload); + } + }; public renameFolder = (payload: { folderUuid: string; name: string }): Promise => { const storageClient = SdkManager.instance.getStorage(); diff --git a/src/services/drive/trash.service.ts b/src/services/drive/trash.service.ts index f5e97310..663ab222 100644 --- a/src/services/drive/trash.service.ts +++ b/src/services/drive/trash.service.ts @@ -1,6 +1,8 @@ -import { StorageTypes, Trash } from '@internxt/sdk/dist/drive'; +import { StorageTypes } from '@internxt/sdk/dist/drive'; import { SdkManager } from '../sdk-manager.service'; import { FetchPaginatedFile, FetchPaginatedFolder } from '@internxt/sdk/dist/drive/storage/types'; +import { AuthService } from '../auth.service'; +import { WorkspaceCredentialsDetails } from '../../types/command.types'; export class TrashService { static readonly instance = new TrashService(); @@ -20,43 +22,69 @@ export class TrashService { return storageClient.deleteFolderByUuid(folderId); }; - public clearTrash = () => { - const storageClient = SdkManager.instance.getTrash(); - return storageClient.clearTrash(); + public clearTrash = async () => { + const currentWorkspace = await AuthService.instance.getCurrentWorkspace(); + const currentWorkspaceCreds = currentWorkspace?.workspaceCredentials; + if (currentWorkspaceCreds) { + const workspaceClient = SdkManager.instance.getWorkspaces(); + return workspaceClient.emptyPersonalTrash(currentWorkspaceCreds.id); + } else { + const trashClient = SdkManager.instance.getTrash(); + return trashClient.clearTrash(); + } }; public getTrashFolderContent = async () => { - const storageClient = SdkManager.instance.getTrash(); - const folders = await this.getAllTrashSubfolders(storageClient, 0); - const files = await this.getAllTrashSubfiles(storageClient, 0); + const currentWorkspace = await AuthService.instance.getCurrentWorkspace(); + const currentWorkspaceCreds = currentWorkspace?.workspaceCredentials; + const folders = await this.getAllTrashSubfolders(currentWorkspaceCreds, 0); + const files = await this.getAllTrashSubfiles(currentWorkspaceCreds, 0); return { folders, files }; }; private readonly getAllTrashSubfolders = async ( - storageClient: Trash, + currentWorkspace: WorkspaceCredentialsDetails | undefined, offset: number, ): Promise => { - const folderContentPromise = storageClient.getTrashedFilesPaginated(50, offset, 'folders', true); - const { result: folders } = (await folderContentPromise) as unknown as { result: FetchPaginatedFolder[] }; + let folders: FetchPaginatedFolder[]; + + if (currentWorkspace) { + const workspaceClient = SdkManager.instance.getWorkspaces(); + const promise = workspaceClient.getPersonalTrash(currentWorkspace.id, 'folder', offset, 50); + folders = (await promise).result as unknown as FetchPaginatedFolder[]; + } else { + const trashClient = SdkManager.instance.getTrash(); + const promise = trashClient.getTrashedFilesPaginated(50, offset, 'folders', true); + folders = (await promise).result as unknown as FetchPaginatedFolder[]; + } if (folders.length > 0) { - return folders.concat(await this.getAllTrashSubfolders(storageClient, offset + folders.length)); + return folders.concat(await this.getAllTrashSubfolders(currentWorkspace, offset + folders.length)); } else { return folders; } }; private readonly getAllTrashSubfiles = async ( - storageClient: Trash, + currentWorkspace: WorkspaceCredentialsDetails | undefined, offset: number, ): Promise => { - const folderContentPromise = storageClient.getTrashedFilesPaginated(50, offset, 'files', true); - const { result: folders } = (await folderContentPromise) as unknown as { result: FetchPaginatedFile[] }; + let files: FetchPaginatedFile[]; - if (folders.length > 0) { - return folders.concat(await this.getAllTrashSubfiles(storageClient, offset + folders.length)); + if (currentWorkspace) { + const workspaceClient = SdkManager.instance.getWorkspaces(); + const promise = workspaceClient.getPersonalTrash(currentWorkspace.id, 'file', offset, 50); + files = (await promise).result as unknown as FetchPaginatedFile[]; } else { - return folders; + const trashClient = SdkManager.instance.getTrash(); + const promise = trashClient.getTrashedFilesPaginated(50, offset, 'files', true); + files = (await promise).result as unknown as FetchPaginatedFile[]; + } + + if (files.length > 0) { + return files.concat(await this.getAllTrashSubfiles(currentWorkspace, offset + files.length)); + } else { + return files; } }; } diff --git a/src/services/drive/workspace.service.ts b/src/services/drive/workspace.service.ts new file mode 100644 index 00000000..0a2164cd --- /dev/null +++ b/src/services/drive/workspace.service.ts @@ -0,0 +1,35 @@ +import { WorkspaceData } from '@internxt/sdk/dist/workspaces'; +import { SdkManager } from '../sdk-manager.service'; +import { LoginCredentials, WorkspaceCredentialsDetails } from '../../types/command.types'; +import { CryptoService } from '../crypto.service'; + +export class WorkspaceService { + static readonly instance = new WorkspaceService(); + + public getAvailableWorkspaces = async (user: LoginCredentials['user']): Promise => { + const workspacesClient = SdkManager.instance.getWorkspaces(); + const workspaces = await workspacesClient.getWorkspaces(); + const decryptedMnemonicWorkspaces = await CryptoService.instance.decryptWorkspacesMnemonic( + workspaces.availableWorkspaces, + user, + ); + return decryptedMnemonicWorkspaces; + }; + + public getWorkspaceCredentials = async (workspaceId: string): Promise => { + const workspacesClient = SdkManager.instance.getWorkspaces(); + const workspaceCredentialsRaw = await workspacesClient.getWorkspaceCredentials(workspaceId); + + const workspaceCredentials: WorkspaceCredentialsDetails = { + id: workspaceCredentialsRaw.workspaceId, + bucket: workspaceCredentialsRaw.bucket, + workspaceUserId: workspaceCredentialsRaw.workspaceUserId, + credentials: { + user: workspaceCredentialsRaw.credentials.networkUser, + pass: workspaceCredentialsRaw.credentials.networkPass, + }, + token: workspaceCredentialsRaw.tokenHeader, + }; + return workspaceCredentials; + }; +} diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index f24460c2..a6e81b9d 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -1,6 +1,10 @@ import { aes } from '@internxt/lib'; import * as openpgp from 'openpgp'; import { CryptoUtils } from '../utils/crypto.utils'; +import kemBuilder from '@dashlane/pqc-kem-kyber512-node'; +import { Data } from '@openpgp/web-stream-tools'; + +const WORDS_HYBRID_MODE_IN_BASE64 = 'SHlicmlkTW9kZQ=='; // 'HybridMode' in BASE64 format export class KeysService { public static readonly instance: KeysService = new KeysService(); @@ -44,4 +48,91 @@ export class KeysService { revocationCertificate: Buffer.from(revocationCertificate).toString('base64'), }; }; + + /** + * Decrypts ciphertext using hybrid method (ecc and kyber) if kyber key is given, else uses ecc only + * @param {string} encryptedMessageInBase64 - The encrypted message + * @param {string} privateKeyInBase64 - The ecc private key in Base64 + * @param {string=}[privateKyberKeyInBase64] - The kyber private key in Base64 + * @returns {Promise} The encrypted message. + */ + public hybridDecryptMessageWithPrivateKey = async ({ + encryptedMessageInBase64, + privateKeyInBase64, + privateKyberKeyInBase64, + }: { + encryptedMessageInBase64: string; + privateKeyInBase64: string; + privateKyberKeyInBase64?: string; + }): Promise => { + let eccCiphertextStr = encryptedMessageInBase64; + let kyberSecret: Uint8Array | undefined; + const ciphertexts = encryptedMessageInBase64.split('$'); + const prefix = ciphertexts[0]; + const isHybridMode = prefix === WORDS_HYBRID_MODE_IN_BASE64; + + if (isHybridMode) { + if (!privateKyberKeyInBase64) { + throw new Error('Attempted to decrypt hybrid ciphertex without Kyber key'); + } + const kem = await kemBuilder(); + + const kyberCiphertextBase64 = ciphertexts[1]; + eccCiphertextStr = ciphertexts[2]; + + const privateKyberKey = Buffer.from(privateKyberKeyInBase64, 'base64'); + const kyberCiphertext = Buffer.from(kyberCiphertextBase64, 'base64'); + const decapsulate = await kem.decapsulate(new Uint8Array(kyberCiphertext), new Uint8Array(privateKyberKey)); + kyberSecret = decapsulate.sharedSecret; + } + + const decryptedMessage = await this.decryptMessageWithPrivateKey({ + encryptedMessage: atob(eccCiphertextStr), + privateKeyInBase64, + }); + let result = decryptedMessage as string; + + if (isHybridMode && kyberSecret) { + const bits = result.length * 4; + const secretHex = await CryptoUtils.extendSecret(kyberSecret, bits); + const xored = CryptoUtils.XORhex(result, secretHex); + result = Buffer.from(xored, 'hex').toString('utf8'); + } + + return result; + }; + + private readonly decryptMessageWithPrivateKey = async ({ + encryptedMessage, + privateKeyInBase64, + }: { + encryptedMessage: string; + privateKeyInBase64: string; + }): Promise & string> => { + const privateKeyArmored = Buffer.from(privateKeyInBase64, 'base64').toString(); + const privateKey = await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }); + + const message = await openpgp.readMessage({ + armoredMessage: encryptedMessage, + }); + + if (!this.comparePrivateKeyCiphertextIDs(privateKey, message)) { + throw new Error('The key does not correspond to the ciphertext'); + } + const { data: decryptedMessage } = await openpgp.decrypt({ + message, + decryptionKeys: privateKey, + }); + + return decryptedMessage; + }; + + private readonly comparePrivateKeyCiphertextIDs = ( + privateKey: openpgp.PrivateKey, + encryptedMessage: openpgp.Message, + ): boolean => { + const messageKeyID = encryptedMessage.getEncryptionKeyIDs()[0].toHex(); + const privateKeyID = privateKey.getSubkeys()[0].getKeyID().toHex(); + return messageKeyID === privateKeyID; + }; } diff --git a/src/services/local-filesystem/local-filesystem.service.ts b/src/services/local-filesystem/local-filesystem.service.ts index 660e3a49..b326f282 100644 --- a/src/services/local-filesystem/local-filesystem.service.ts +++ b/src/services/local-filesystem/local-filesystem.service.ts @@ -6,7 +6,7 @@ import { logger } from '../../utils/logger.utils'; export class LocalFilesystemService { static readonly instance = new LocalFilesystemService(); - async scanLocalDirectory(path: string): Promise { + public scanLocalDirectory = async (path: string): Promise => { const folders: FileSystemNode[] = []; const files: FileSystemNode[] = []; @@ -18,13 +18,14 @@ export class LocalFilesystemService { totalItems: folders.length + files.length, totalBytes, }; - } - async scanRecursive( + }; + + public scanRecursive = async ( currentPath: string, parentPath: string, folders: FileSystemNode[], files: FileSystemNode[], - ): Promise { + ): Promise => { try { const stats = await promises.stat(currentPath); const relativePath = relative(parentPath, currentPath); @@ -63,5 +64,5 @@ export class LocalFilesystemService { logger.warn(`Error scanning path ${currentPath}: ${(error as Error).message} - skipping...`); return 0; } - } + }; } diff --git a/src/services/network/download.service.ts b/src/services/network/download.service.ts index 028cc2a7..0a280b0f 100644 --- a/src/services/network/download.service.ts +++ b/src/services/network/download.service.ts @@ -4,14 +4,14 @@ import { DownloadProgressCallback } from '../../types/network.types'; export class DownloadService { static readonly instance = new DownloadService(); - async downloadFile( + public downloadFile = async ( url: string, options: { progressCallback?: DownloadProgressCallback; abortController?: AbortController; rangeHeader?: string; }, - ): Promise> { + ): Promise> => { const response = await axios.get(url, { responseType: 'stream', onDownloadProgress(progressEvent) { @@ -35,5 +35,5 @@ export class DownloadService { }, }); return readable; - } + }; } diff --git a/src/services/network/network-facade.service.ts b/src/services/network/network-facade.service.ts index f9f7399b..b405102e 100644 --- a/src/services/network/network-facade.service.ts +++ b/src/services/network/network-facade.service.ts @@ -19,8 +19,6 @@ export class NetworkFacade { constructor( private readonly network: Network.Network, private readonly environment: Environment, - private readonly downloadService: DownloadService, - private readonly cryptoService: CryptoService, ) { this.cryptoLib = { algorithm: Network.ALGORITHMS.AES256CTR, @@ -44,7 +42,7 @@ export class NetworkFacade { * @param options The download options * @returns A promise to execute the download and an abort controller to cancel the download */ - async downloadToStream( + public downloadToStream = async ( bucketId: string, mnemonic: string, fileId: string, @@ -52,7 +50,7 @@ export class NetworkFacade { to: WritableStream, rangeOptions?: RangeOptions, options?: DownloadOptions, - ): Promise<[Promise, AbortController]> { + ): Promise<[Promise, AbortController]> => { const encryptedContentStreams: ReadableStream[] = []; let fileStream: ReadableStream; const abortable = options?.abortController ?? new AbortController(); @@ -68,7 +66,7 @@ export class NetworkFacade { if (rangeOptions) { startOffsetByte = rangeOptions.parsed.start; } - fileStream = this.cryptoService.decryptStream( + fileStream = CryptoService.instance.decryptStream( encryptedContentStreams, Buffer.from(key as ArrayBuffer), Buffer.from(iv as ArrayBuffer), @@ -88,7 +86,7 @@ export class NetworkFacade { throw new Error('Download aborted'); } - const encryptedContentStream = await this.downloadService.downloadFile(downloadable.url, { + const encryptedContentStream = await DownloadService.instance.downloadFile(downloadable.url, { progressCallback: onProgress, abortController: options?.abortController, rangeHeader: rangeOptions?.range, @@ -112,7 +110,7 @@ export class NetworkFacade { }; return [downloadOperation(), abortable]; - } + }; /** * Performs an upload encrypting the stream content @@ -122,13 +120,13 @@ export class NetworkFacade { * @param from The source ReadStream to upload from * @returns A promise to execute the upload and an abort controller to cancel the upload */ - uploadFile( + public uploadFile = ( from: Readable, size: number, bucketId: string, finishedCallback: (err: Error | null, res: string | null) => void, progressCallback: (progress: number) => void, - ): ActionState { + ): ActionState => { if (size > TWENTY_GIGABYTES) { throw new Error('File is too big (more than 20 GB)'); } @@ -150,5 +148,5 @@ export class NetworkFacade { progressCallback, }); } - } + }; } diff --git a/src/services/network/upload/upload-facade.service.ts b/src/services/network/upload/upload-facade.service.ts index a5aef4e7..37ef3973 100644 --- a/src/services/network/upload/upload-facade.service.ts +++ b/src/services/network/upload/upload-facade.service.ts @@ -9,9 +9,18 @@ import { AsyncUtils } from '../../../utils/async.utils'; export class UploadFacade { static readonly instance = new UploadFacade(); - async uploadFolder({ localPath, destinationFolderUuid, loginUserDetails, jsonFlag, onProgress }: UploadFolderParams) { + + public uploadFolder = async ({ + localPath, + destinationFolderUuid, + loginUserDetails, + jsonFlag, + onProgress, + }: UploadFolderParams) => { const timer = CLIUtils.timer(); - const network = CLIUtils.prepareNetwork({ jsonFlag, loginUserDetails }); + CLIUtils.doing('Preparing Network', jsonFlag); + const { networkFacade, bucket } = await CLIUtils.prepareNetwork(loginUserDetails); + CLIUtils.done(jsonFlag); const scanResult = await LocalFilesystemService.instance.scanLocalDirectory(localPath); logger.info( `Scanned folder ${localPath}: found ${scanResult.totalItems} items, total size ${scanResult.totalBytes} bytes.`, @@ -39,10 +48,10 @@ export class UploadFacade { await AsyncUtils.sleep(500); const totalBytes = await UploadFileService.instance.uploadFilesConcurrently({ - network, + network: networkFacade, filesToUpload: scanResult.files, folderMap, - bucket: loginUserDetails.bucket, + bucket, destinationFolderUuid, currentProgress, emitProgress, @@ -55,5 +64,5 @@ export class UploadFacade { rootFolderId, uploadTimeMs: timer.stop(), }; - } + }; } diff --git a/src/services/network/upload/upload-file.service.ts b/src/services/network/upload/upload-file.service.ts index fb3127a5..c895f63f 100644 --- a/src/services/network/upload/upload-file.service.ts +++ b/src/services/network/upload/upload-file.service.ts @@ -8,18 +8,18 @@ import { } from './upload.types'; import { DriveFileService } from '../../drive/drive-file.service'; import { dirname, extname } from 'node:path'; -import { isAlreadyExistsError } from '../../../utils/errors.utils'; +import { ErrorUtils } from '../../../utils/errors.utils'; import { stat } from 'node:fs/promises'; import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; -import { createFileStreamWithBuffer, tryUploadThumbnail } from '../../../utils/thumbnail.utils'; import { BufferStream } from '../../../utils/stream.utils'; import { DriveFileItem } from '../../../types/drive.types'; import { CLIUtils } from '../../../utils/cli.utils'; +import { ThumbnailService } from '../../thumbnail.service'; export class UploadFileService { static readonly instance = new UploadFileService(); - async uploadFilesConcurrently({ + public uploadFilesConcurrently = async ({ network, filesToUpload, folderMap, @@ -27,7 +27,7 @@ export class UploadFileService { destinationFolderUuid, currentProgress, emitProgress, - }: UploadFilesConcurrentlyParams): Promise { + }: UploadFilesConcurrentlyParams): Promise => { let bytesUploaded = 0; const concurrentFiles = this.concurrencyArray(filesToUpload, MAX_CONCURRENT_UPLOADS); @@ -59,14 +59,14 @@ export class UploadFileService { ); } return bytesUploaded; - } + }; - async uploadFileWithRetry({ + public uploadFileWithRetry = async ({ file, network, bucket, parentFolderUuid, - }: UploadFileWithRetryParams): Promise { + }: UploadFileWithRetryParams): Promise => { for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { try { const stats = await stat(file.absolutePath); @@ -84,7 +84,7 @@ export class UploadFileService { }; if (fileSize > 0) { - const { fileStream, bufferStream } = createFileStreamWithBuffer({ + const { fileStream, bufferStream } = ThumbnailService.instance.createFileStreamWithBuffer({ path: file.absolutePath, fileType, }); @@ -125,10 +125,10 @@ export class UploadFileService { const thumbnailTimer = CLIUtils.timer(); if (thumbnailStream && fileSize > 0) { - void tryUploadThumbnail({ + void ThumbnailService.instance.tryUploadThumbnail({ bufferStream: thumbnailStream, fileType, - userBucket: bucket, + bucket, fileUuid: createdDriveFile.uuid, networkFacade: network, }); @@ -139,16 +139,16 @@ export class UploadFileService { const throughputMBps = CLIUtils.calculateThroughputMBps(stats.size, timings.networkUpload); logger.info(`Uploaded '${file.name}' (${CLIUtils.formatBytesToString(stats.size)})`); logger.info( - `Timing breakdown:\n - Network upload: ${CLIUtils.formatDuration(timings.networkUpload)} (${throughputMBps.toFixed(2)} MB/s)\n - Drive upload: ${CLIUtils.formatDuration(timings.driveUpload)}\n - Thumbnail: ${CLIUtils.formatDuration(timings.thumbnailUpload)}\n - Total: ${CLIUtils.formatDuration(totalTime)}\n`, + 'Timing breakdown:\n' + + `Network upload: ${CLIUtils.formatDuration(timings.networkUpload)} (${throughputMBps.toFixed(2)} MB/s)\n` + + `Drive upload: ${CLIUtils.formatDuration(timings.driveUpload)}\n` + + `Thumbnail: ${CLIUtils.formatDuration(timings.thumbnailUpload)}\n` + + `Total: ${CLIUtils.formatDuration(totalTime)}\n`, ); return createdDriveFile; } catch (error: unknown) { - if (isAlreadyExistsError(error)) { + if (ErrorUtils.isAlreadyExistsError(error)) { const msg = `File ${file.name} already exists, skipping...`; logger.info(msg); return null; @@ -166,12 +166,13 @@ export class UploadFileService { } } return null; - } - private concurrencyArray(array: T[], arraySize: number): T[][] { + }; + + private readonly concurrencyArray = (array: T[], arraySize: number): T[][] => { const arrays: T[][] = []; for (let i = 0; i < array.length; i += arraySize) { arrays.push(array.slice(i, i + arraySize)); } return arrays; - } + }; } diff --git a/src/services/network/upload/upload-folder.service.ts b/src/services/network/upload/upload-folder.service.ts index 3608534d..78b8efe7 100644 --- a/src/services/network/upload/upload-folder.service.ts +++ b/src/services/network/upload/upload-folder.service.ts @@ -1,17 +1,18 @@ import { dirname } from 'node:path'; -import { isAlreadyExistsError } from '../../../utils/errors.utils'; +import { ErrorUtils } from '../../../utils/errors.utils'; import { logger } from '../../../utils/logger.utils'; import { DriveFolderService } from '../../drive/drive-folder.service'; import { CreateFoldersParams, CreateFolderWithRetryParams, DELAYS_MS, MAX_RETRIES } from './upload.types'; export class UploadFolderService { static readonly instance = new UploadFolderService(); - async createFolders({ + + public createFolders = async ({ foldersToCreate, destinationFolderUuid, currentProgress, emitProgress, - }: CreateFoldersParams): Promise> { + }: CreateFoldersParams): Promise> => { const folderMap = new Map(); for (const folder of foldersToCreate) { const parentPath = dirname(folder.relativePath); @@ -34,12 +35,15 @@ export class UploadFolderService { } } return folderMap; - } + }; - async createFolderWithRetry({ folderName, parentFolderUuid }: CreateFolderWithRetryParams): Promise { + public createFolderWithRetry = async ({ + folderName, + parentFolderUuid, + }: CreateFolderWithRetryParams): Promise => { for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { try { - const [createFolderPromise] = DriveFolderService.instance.createFolder({ + const [createFolderPromise] = await DriveFolderService.instance.createFolder({ plainName: folderName, parentFolderUuid, }); @@ -47,7 +51,7 @@ export class UploadFolderService { const createdFolder = await createFolderPromise; return createdFolder.uuid; } catch (error: unknown) { - if (isAlreadyExistsError(error)) { + if (ErrorUtils.isAlreadyExistsError(error)) { logger.info(`Folder ${folderName} already exists, skipping...`); return null; } @@ -65,5 +69,5 @@ export class UploadFolderService { } } return null; - } + }; } diff --git a/src/services/sdk-manager.service.ts b/src/services/sdk-manager.service.ts index 1f5c6372..4339db2a 100644 --- a/src/services/sdk-manager.service.ts +++ b/src/services/sdk-manager.service.ts @@ -4,6 +4,7 @@ import { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; import { ConfigService } from './config.service'; import packageJson from '../../package.json'; import { NetworkUtils } from '../utils/network.utils'; +import { Workspaces } from '@internxt/sdk/dist/workspaces'; export type SdkManagerApiSecurity = ApiSecurity; @@ -56,57 +57,67 @@ export class SdkManager { }; /** Auth SDK */ - getAuth() { + public getAuth = () => { const DRIVE_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const apiSecurity = SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); return Auth.client(DRIVE_API_URL, appDetails, apiSecurity); - } + }; /** Users SDK */ - getUsers() { + public getUsers = () => { const DRIVE_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const apiSecurity = SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); return Drive.Users.client(DRIVE_API_URL, appDetails, apiSecurity); - } + }; /** Storage SDK */ - getStorage() { + public getStorage = () => { const DRIVE_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const apiSecurity = SdkManager.getApiSecurity(); const appDetails = SdkManager.getAppDetails(); return Drive.Storage.client(DRIVE_API_URL, appDetails, apiSecurity); - } + }; /** Trash SDK */ - getTrash() { + public getTrash = () => { const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const apiSecurity = SdkManager.getApiSecurity(); const appDetails = SdkManager.getAppDetails(); return Trash.client(DRIVE_NEW_API_URL, appDetails, apiSecurity); - } + }; /** Share SDK */ - getShare() { + public getShare = () => { const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const apiSecurity = SdkManager.getApiSecurity(); const appDetails = SdkManager.getAppDetails(); return Drive.Share.client(DRIVE_NEW_API_URL, appDetails, apiSecurity); - } + }; + + /** Workspaces SDK */ + public getWorkspaces = () => { + const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); + + const apiSecurity = SdkManager.getApiSecurity(); + const appDetails = SdkManager.getAppDetails(); + + return Workspaces.client(DRIVE_NEW_API_URL, appDetails, apiSecurity); + }; /** Network SDK */ - getNetwork(credentials: { user: string; pass: string }) { + public getNetwork = (credentials: { user: string; pass: string }) => { const appDetails = SdkManager.getAppDetails(); const auth = NetworkUtils.getAuthFromCredentials({ user: credentials.user, @@ -116,5 +127,5 @@ export class SdkManager { bridgeUser: auth.username, userId: auth.password, }); - } + }; } diff --git a/src/services/thumbnail.service.ts b/src/services/thumbnail.service.ts index a0ed8c9e..9505a775 100644 --- a/src/services/thumbnail.service.ts +++ b/src/services/thumbnail.service.ts @@ -1,8 +1,11 @@ import { Readable } from 'node:stream'; +import { createReadStream } from 'node:fs'; import { DriveFileService } from './drive/drive-file.service'; import { StorageTypes } from '@internxt/sdk/dist/drive'; import { NetworkFacade } from './network/network-facade.service'; -import { isImageThumbnailable, ThumbnailConfig } from '../utils/thumbnail.utils'; +import { ThumbnailConfig, ThumbnailUtils } from '../utils/thumbnail.utils'; +import { BufferStream } from '../utils/stream.utils'; +import { ErrorUtils } from '../utils/errors.utils'; let sharpDependency: typeof import('sharp') | null = null; @@ -28,7 +31,7 @@ export class ThumbnailService { networkFacade: NetworkFacade, ): Promise => { let thumbnailBuffer: Buffer | undefined; - if (isImageThumbnailable(fileType)) { + if (ThumbnailUtils.isImageThumbnailable(fileType)) { thumbnailBuffer = await this.getThumbnailFromImageBuffer(fileContent); } if (thumbnailBuffer) { @@ -78,4 +81,48 @@ export class ThumbnailService { .toBuffer(); } }; + + public tryUploadThumbnail = async ({ + bufferStream, + fileType, + bucket, + fileUuid, + networkFacade, + }: { + bufferStream?: BufferStream; + fileType: string; + bucket: string; + fileUuid: string; + networkFacade: NetworkFacade; + }) => { + try { + const thumbnailBuffer = bufferStream?.getBuffer(); + if (thumbnailBuffer) { + await ThumbnailService.instance.uploadThumbnail(thumbnailBuffer, fileType, bucket, fileUuid, networkFacade); + } + } catch (error) { + ErrorUtils.report(error); + } + }; + + public createFileStreamWithBuffer = ({ + path, + fileType, + }: { + path: string; + fileType: string; + }): { + bufferStream?: BufferStream; + fileStream: Readable; + } => { + const readable: Readable = createReadStream(path); + if (ThumbnailUtils.isFileThumbnailable(fileType)) { + const bufferStream = new BufferStream(); + return { + bufferStream, + fileStream: readable.pipe(bufferStream), + }; + } + return { fileStream: readable }; + }; } diff --git a/src/services/usage.service.ts b/src/services/usage.service.ts index 0a00352c..5f730077 100644 --- a/src/services/usage.service.ts +++ b/src/services/usage.service.ts @@ -1,15 +1,14 @@ -import { UsageResponseV2 } from '@internxt/sdk/dist/drive/storage/types'; import { SdkManager } from './sdk-manager.service'; export class UsageService { public static readonly instance: UsageService = new UsageService(); public static readonly INFINITE_LIMIT = 99 * Math.pow(1024, 4); - public fetchUsage = async (): Promise => { + public fetchUsage = async (): Promise => { const storageClient = SdkManager.instance.getStorage(); const driveUsage = await storageClient.spaceUsageV2(); - return driveUsage; + return driveUsage.total; }; public fetchSpaceLimit = async (): Promise => { diff --git a/src/services/validation.service.ts b/src/services/validation.service.ts index f400af19..0b228625 100644 --- a/src/services/validation.service.ts +++ b/src/services/validation.service.ts @@ -34,13 +34,21 @@ export class ValidationService { }; public validateDirectoryExists = async (path: string): Promise => { - const directoryStat = await fs.stat(path); - return directoryStat.isDirectory(); + try { + const directoryStat = await fs.stat(path); + return directoryStat.isDirectory(); + } catch { + return false; + } }; public validateFileExists = async (path: string): Promise => { - const fileStat = await fs.stat(path); - return fileStat.isFile(); + try { + const fileStat = await fs.stat(path); + return fileStat.isFile(); + } catch { + return false; + } }; /** diff --git a/src/types/command.types.ts b/src/types/command.types.ts index 74f51495..ef53f0ee 100644 --- a/src/types/command.types.ts +++ b/src/types/command.types.ts @@ -1,3 +1,6 @@ +import { WorkspaceData } from '@internxt/sdk/dist/workspaces'; +import { NetworkCredentials } from './network.types'; + export interface LoginUserDetails { userId: string; uuid: string; @@ -24,9 +27,23 @@ export interface LoginUserDetails { emailVerified: boolean; } +export interface WorkspaceCredentialsDetails { + id: string; + bucket: string; + workspaceUserId: string; + credentials: NetworkCredentials; + token: string; +} + +export interface Workspace { + workspaceData: WorkspaceData; + workspaceCredentials: WorkspaceCredentialsDetails; +} + export interface LoginCredentials { user: LoginUserDetails; token: string; + workspace?: Workspace; } export interface WebdavConfig { @@ -157,6 +174,14 @@ export class NotValidFileError extends Error { } } +export class NotValidWorkspaceUuidError extends Error { + constructor() { + super('Workspace UUID is not valid (it must be a valid v4 UUID)'); + + Object.setPrototypeOf(this, NotValidWorkspaceUuidError.prototype); + } +} + export interface PaginatedItem { name: string; type: string; @@ -166,6 +191,20 @@ export interface PaginatedItem { } export interface PromptOptions { - type: 'input' | 'password' | 'mask' | 'confirm'; + type: 'input' | 'password' | 'mask' | 'confirm' | 'list'; confirm?: { default: boolean }; + choices?: { + values: string[]; + default?: number; + }; +} + +export interface PaginatedWorkspace { + name: string; + id: string; + usedSpace: string; + availableSpace: string; + owner: string; + address: string; + created: string; } diff --git a/src/types/network.types.ts b/src/types/network.types.ts index 5c98b226..eb712c5a 100644 --- a/src/types/network.types.ts +++ b/src/types/network.types.ts @@ -1,8 +1,16 @@ +import { NetworkFacade } from '../services/network/network-facade.service'; + export interface NetworkCredentials { user: string; pass: string; } +export interface NetworkOptions { + networkFacade: NetworkFacade; + bucket: string; + mnemonic: string; +} + export type DownloadProgressCallback = (downloadedBytes: number) => void; export type UploadProgressCallback = (uploadedBytes: number) => void; export interface NetworkOperationBaseOptions { diff --git a/src/utils/async.utils.ts b/src/utils/async.utils.ts index 1b406516..5342e6d2 100644 --- a/src/utils/async.utils.ts +++ b/src/utils/async.utils.ts @@ -5,7 +5,7 @@ export class AsyncUtils { * @param {number} ms - The number of milliseconds to pause the execution. * @return {Promise} A promise that resolves once the specified time has elapsed. */ - static sleep(ms: number): Promise { + static readonly sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); - } + }; } diff --git a/src/utils/cli.utils.ts b/src/utils/cli.utils.ts index 8a195691..43de1a2b 100644 --- a/src/utils/cli.utils.ts +++ b/src/utils/cli.utils.ts @@ -1,7 +1,7 @@ import { ux, Flags } from '@oclif/core'; import cliProgress from 'cli-progress'; import Table, { Header } from 'tty-table'; -import { LoginUserDetails, NotValidFolderUuidError, PromptOptions } from '../types/command.types'; +import { LoginCredentials, LoginUserDetails, NotValidFolderUuidError, PromptOptions } from '../types/command.types'; import { InquirerUtils } from './inquirer.utils'; import { ErrorUtils } from './errors.utils'; import { ValidationService } from '../services/validation.service'; @@ -9,8 +9,8 @@ import { SdkManager } from '../services/sdk-manager.service'; import { Environment } from '@internxt/inxt-js'; import { ConfigService } from '../services/config.service'; import { NetworkFacade } from '../services/network/network-facade.service'; -import { DownloadService } from '../services/network/download.service'; -import { CryptoService } from '../services/crypto.service'; +import { AuthService } from '../services/auth.service'; +import { NetworkCredentials, NetworkOptions } from '../types/network.types'; export class CLIUtils { static readonly clearPreviousLine = (jsonFlag?: boolean) => { @@ -142,7 +142,7 @@ export class CLIUtils { destinationFlagName: string; nonInteractive: boolean; reporter: (message: string) => void; - }): Promise => { + }): Promise => { const destinationFolderUuid = await this.getValueFromFlag( { value: destinationFolderUuidFlag, @@ -162,11 +162,7 @@ export class CLIUtils { }, reporter, ); - if (destinationFolderUuid.trim().length === 0) { - return undefined; - } else { - return destinationFolderUuid; - } + return destinationFolderUuid; }; private static readonly promptWithAttempts = async ( @@ -282,34 +278,62 @@ export class CLIUtils { static readonly parseEmpty = async (input: string) => (input.trim().length === 0 ? ' ' : input); - static readonly prepareNetwork = ({ - jsonFlag, - loginUserDetails, - }: { - jsonFlag?: boolean; - loginUserDetails: LoginUserDetails; - }) => { - CLIUtils.doing('Preparing Network', jsonFlag); + static readonly prepareNetwork = async (loginUserDetails: LoginUserDetails): Promise => { + const { credentials, mnemonic, bucket } = await this.getNetworkCreds(loginUserDetails); + const networkModule = SdkManager.instance.getNetwork({ - user: loginUserDetails.bridgeUser, - pass: loginUserDetails.userId, + user: credentials.user, + pass: credentials.pass, }); const environment = new Environment({ - bridgeUser: loginUserDetails.bridgeUser, - bridgePass: loginUserDetails.userId, + bridgeUser: credentials.user, + bridgePass: credentials.pass, + encryptionKey: mnemonic, bridgeUrl: ConfigService.instance.get('NETWORK_URL'), - encryptionKey: loginUserDetails.mnemonic, appDetails: SdkManager.getAppDetails(), }); - const networkFacade = new NetworkFacade( - networkModule, - environment, - DownloadService.instance, - CryptoService.instance, - ); + const networkFacade = new NetworkFacade(networkModule, environment); + + return { networkFacade, bucket, mnemonic }; + }; - CLIUtils.done(jsonFlag); - return networkFacade; + static readonly fallbackToRootFolderIdIfEmpty = async (folderId: string, userCredentials: LoginCredentials) => { + if (folderId.trim().length === 0) { + const currentWorkspace = await AuthService.instance.getCurrentWorkspace(); + return currentWorkspace?.workspaceData.workspaceUser.rootFolderId ?? userCredentials.user.rootFolderId; + } else { + return folderId; + } + }; + + static readonly getNetworkCreds = async ( + userCredentials: LoginCredentials['user'], + ): Promise<{ + bucket: string; + credentials: NetworkCredentials; + mnemonic: string; + }> => { + const currentWorkspace = await AuthService.instance.getCurrentWorkspace(); + + if (currentWorkspace) { + return { + bucket: currentWorkspace.workspaceCredentials.bucket, + credentials: { + user: currentWorkspace.workspaceCredentials.credentials.user, + pass: currentWorkspace.workspaceCredentials.credentials.pass, + }, + mnemonic: currentWorkspace.workspaceData.workspaceUser.key, + }; + } else { + return { + bucket: userCredentials.bucket, + credentials: { + user: userCredentials.bridgeUser, + pass: userCredentials.userId, + }, + mnemonic: userCredentials.mnemonic, + }; + } }; } diff --git a/src/utils/crypto.utils.ts b/src/utils/crypto.utils.ts index bd3ed354..16acefb4 100644 --- a/src/utils/crypto.utils.ts +++ b/src/utils/crypto.utils.ts @@ -1,7 +1,36 @@ +import { blake3 } from 'hash-wasm'; import { ConfigService } from '../services/config.service'; export class CryptoUtils { - static getAesInit(): { iv: string; salt: string } { + static readonly getAesInit = (): { iv: string; salt: string } => { return { iv: ConfigService.instance.get('APP_MAGIC_IV'), salt: ConfigService.instance.get('APP_MAGIC_SALT') }; - } + }; + + /** + * Extends the given secret to the required number of bits + * @param {string} secret - The original secret + * @param {number} length - The desired bitlength + * @returns {Promise} The extended secret of the desired bitlength + */ + static readonly extendSecret = (secret: Uint8Array, length: number): Promise => { + return blake3(secret, length); + }; + + /** + * XORs two strings of the identical length + * @param {string} a - The first string + * @param {string} b - The second string + * @returns {string} The result of XOR of strings a and b. + */ + static readonly XORhex = (a: string, b: string): string => { + let res = '', + i = a.length, + j = b.length; + if (i != j) { + throw new Error('Can XOR only strings with identical length'); + } + while (i-- > 0 && j-- > 0) + res = (Number.parseInt(a.charAt(i), 16) ^ Number.parseInt(b.charAt(j), 16)).toString(16) + res; + return res; + }; } diff --git a/src/utils/errors.utils.ts b/src/utils/errors.utils.ts index 60d14f0c..97969239 100644 --- a/src/utils/errors.utils.ts +++ b/src/utils/errors.utils.ts @@ -1,33 +1,33 @@ import { logger } from './logger.utils'; -export function isError(error: unknown): error is Error { - return typeof Error.isError === 'function' - ? Error.isError(error) - : error instanceof Error || - (typeof error === 'object' && error !== null && 'message' in error && ('stack' in error || 'name' in error)); -} - -export function isAlreadyExistsError(error: unknown): error is Error { - return ( - (isError(error) && error.message.includes('already exists')) || - (typeof error === 'object' && error !== null && 'status' in error && error.status === 409) - ); -} - -export function isFileNotFoundError(error: unknown): error is NodeJS.ErrnoException { - return isError(error) && 'code' in error && error.code === 'ENOENT'; -} - export class ErrorUtils { - static report(error: unknown, props: Record = {}) { - if (isError(error)) { + static readonly isError = (error: unknown): error is Error => { + return typeof Error.isError === 'function' + ? Error.isError(error) + : error instanceof Error || + (typeof error === 'object' && error !== null && 'message' in error && ('stack' in error || 'name' in error)); + }; + + static readonly report = (error: unknown, props: Record = {}) => { + if (this.isError(error)) { logger.error( `[REPORTED_ERROR]: ${error.message}\nProperties => ${JSON.stringify(props, null, 2)}\nStack => ${error.stack}`, ); } else { logger.error(`[REPORTED_ERROR]: ${JSON.stringify(error)}\nProperties => ${JSON.stringify(props, null, 2)}\n`); } - } + }; + + static readonly isAlreadyExistsError = (error: unknown): error is Error => { + return ( + (this.isError(error) && error.message.includes('already exists')) || + (typeof error === 'object' && error !== null && 'status' in error && error.status === 409) + ); + }; + + static readonly isFileNotFoundError = (error: unknown): error is NodeJS.ErrnoException => { + return this.isError(error) && 'code' in error && error.code === 'ENOENT'; + }; } export class ConflictError extends Error { diff --git a/src/utils/inquirer.utils.ts b/src/utils/inquirer.utils.ts index 0d5374b2..abbbfc63 100644 --- a/src/utils/inquirer.utils.ts +++ b/src/utils/inquirer.utils.ts @@ -1,5 +1,5 @@ import { PromptOptions } from '../types/command.types'; -import { confirm, input, password } from '@inquirer/prompts'; +import { confirm, input, password, select } from '@inquirer/prompts'; export class InquirerUtils { static async prompt(message: string, options: PromptOptions): Promise { @@ -14,7 +14,15 @@ export class InquirerUtils { const confirmation = await confirm({ message, default: options.confirm?.default || false }); return confirmation ? 'y' : 'n'; } - case 'input': { + case 'list': + if (!options.choices) throw new Error('Missing choices'); + return select({ + message, + choices: options.choices.values, + default: options.choices.values[options.choices.default ?? 0], + }); + case 'input': + default: { return input({ message }); } } diff --git a/src/utils/thumbnail.utils.ts b/src/utils/thumbnail.utils.ts index 4ce818e5..480a631e 100644 --- a/src/utils/thumbnail.utils.ts +++ b/src/utils/thumbnail.utils.ts @@ -1,10 +1,3 @@ -import { Readable } from 'node:stream'; -import { NetworkFacade } from '../services/network/network-facade.service'; -import { ThumbnailService } from '../services/thumbnail.service'; -import { ErrorUtils } from './errors.utils'; -import { BufferStream } from './stream.utils'; -import { createReadStream } from 'node:fs'; - export const ThumbnailConfig = { MaxWidth: 300, MaxHeight: 300, @@ -37,58 +30,16 @@ const thumbnailableImageExtension: Set = new Set([ const thumbnailablePdfExtension: Set = new Set(pdfExtensions['pdf']); const thumbnailableExtension: Set = new Set(thumbnailableImageExtension); -export const isFileThumbnailable = (fileType: string) => { - return fileType.trim().length > 0 && thumbnailableExtension.has(fileType.trim().toLowerCase()); -}; - -export const isPDFThumbnailable = (fileType: string) => { - return fileType.trim().length > 0 && thumbnailablePdfExtension.has(fileType.trim().toLowerCase()); -}; - -export const isImageThumbnailable = (fileType: string) => { - return fileType.trim().length > 0 && thumbnailableImageExtension.has(fileType.trim().toLowerCase()); -}; +export class ThumbnailUtils { + static readonly isFileThumbnailable = (fileType: string) => { + return fileType.trim().length > 0 && thumbnailableExtension.has(fileType.trim().toLowerCase()); + }; -export const tryUploadThumbnail = async ({ - bufferStream, - fileType, - userBucket, - fileUuid, - networkFacade, -}: { - bufferStream?: BufferStream; - fileType: string; - userBucket: string; - fileUuid: string; - networkFacade: NetworkFacade; -}) => { - try { - const thumbnailBuffer = bufferStream?.getBuffer(); - if (thumbnailBuffer) { - await ThumbnailService.instance.uploadThumbnail(thumbnailBuffer, fileType, userBucket, fileUuid, networkFacade); - } - } catch (error) { - ErrorUtils.report(error); - } -}; + static readonly isPDFThumbnailable = (fileType: string) => { + return fileType.trim().length > 0 && thumbnailablePdfExtension.has(fileType.trim().toLowerCase()); + }; -export const createFileStreamWithBuffer = ({ - path, - fileType, -}: { - path: string; - fileType: string; -}): { - bufferStream?: BufferStream; - fileStream: Readable; -} => { - const readable: Readable = createReadStream(path); - if (isFileThumbnailable(fileType)) { - const bufferStream = new BufferStream(); - return { - bufferStream, - fileStream: readable.pipe(bufferStream), - }; - } - return { fileStream: readable }; -}; + static readonly isImageThumbnailable = (fileType: string) => { + return fileType.trim().length > 0 && thumbnailableImageExtension.has(fileType.trim().toLowerCase()); + }; +} diff --git a/src/utils/webdav.utils.ts b/src/utils/webdav.utils.ts index 05b4e26c..59c65588 100644 --- a/src/utils/webdav.utils.ts +++ b/src/utils/webdav.utils.ts @@ -52,72 +52,40 @@ export class WebDavUtils { }; } - static async tryGetFileOrFolderMetadata({ - url, - driveFileService, - driveFolderService, - }: { - url: string; - driveFolderService: DriveFolderService; - driveFileService: DriveFileService; - }): Promise { + static async tryGetFileOrFolderMetadata(url: string): Promise { try { - return await driveFileService.getFileMetadataByPath(url); + return await DriveFileService.instance.getFileMetadataByPath(url); } catch { - return await driveFolderService.getFolderMetadataByPath(url); + return await DriveFolderService.instance.getFolderMetadataByPath(url); } } - static async getDriveFileFromResource({ - url, - driveFileService, - }: { - url: string; - driveFileService: DriveFileService; - }): Promise { + static async getDriveFileFromResource(url: string): Promise { try { - return await driveFileService.getFileMetadataByPath(url); + return await DriveFileService.instance.getFileMetadataByPath(url); } catch (err) { webdavLogger.error('Exception while getting the file metadata by path', err); } } - static async getDriveFolderFromResource({ - url, - driveFolderService, - }: { - url: string; - driveFolderService: DriveFolderService; - }): Promise { + static async getDriveFolderFromResource(url: string): Promise { try { - return await driveFolderService.getFolderMetadataByPath(url); + return await DriveFolderService.instance.getFolderMetadataByPath(url); } catch (err) { webdavLogger.error('Exception while getting the folder metadata by path', err); } } - static async getDriveItemFromResource({ - resource, - driveFolderService, - driveFileService, - }: { - resource: WebDavRequestedResource; - driveFolderService: DriveFolderService; - driveFileService: DriveFileService; - }): Promise { + static async getDriveItemFromResource(resource: WebDavRequestedResource): Promise { let item: DriveItem | undefined = undefined; const isFolder = resource.url.endsWith('/'); try { if (isFolder) { - item = await driveFolderService.getFolderMetadataByPath(resource.url); + item = await DriveFolderService.instance.getFolderMetadataByPath(resource.url); } else { - item = await this.tryGetFileOrFolderMetadata({ - url: resource.url, - driveFileService, - driveFolderService, - }); + item = await this.tryGetFileOrFolderMetadata(resource.url); } } catch { //no op diff --git a/src/webdav/handlers/DELETE.handler.ts b/src/webdav/handlers/DELETE.handler.ts index 248004a7..0f9fd38d 100644 --- a/src/webdav/handlers/DELETE.handler.ts +++ b/src/webdav/handlers/DELETE.handler.ts @@ -3,36 +3,21 @@ import { WebDavMethodHandler } from '../../types/webdav.types'; import { WebDavUtils } from '../../utils/webdav.utils'; import { TrashService } from '../../services/drive/trash.service'; import { webdavLogger } from '../../utils/logger.utils'; -import { DriveFileService } from '../../services/drive/drive-file.service'; -import { DriveFolderService } from '../../services/drive/drive-folder.service'; import { NotFoundError } from '../../utils/errors.utils'; export class DELETERequestHandler implements WebDavMethodHandler { - constructor( - private readonly dependencies: { - trashService: TrashService; - driveFileService: DriveFileService; - driveFolderService: DriveFolderService; - }, - ) {} - handle = async (req: Request, res: Response) => { - const { driveFileService, driveFolderService, trashService } = this.dependencies; const resource = await WebDavUtils.getRequestedResource(req.url); webdavLogger.info(`[DELETE] Request received for item at ${resource.url}`); - const driveItem = await WebDavUtils.getDriveItemFromResource({ - resource, - driveFolderService, - driveFileService: driveFileService, - }); + const driveItem = await WebDavUtils.getDriveItemFromResource(resource); if (!driveItem) { throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`); } webdavLogger.info(`[DELETE] [${driveItem.uuid}] Trashing ${driveItem.itemType}`); - await trashService.trashItems({ + await TrashService.instance.trashItems({ items: [{ type: driveItem.itemType, uuid: driveItem.uuid }], }); diff --git a/src/webdav/handlers/GET.handler.ts b/src/webdav/handlers/GET.handler.ts index d529589b..fddd7063 100644 --- a/src/webdav/handlers/GET.handler.ts +++ b/src/webdav/handlers/GET.handler.ts @@ -1,36 +1,19 @@ import { WebDavMethodHandler } from '../../types/webdav.types'; import { Request, Response } from 'express'; import { WebDavUtils } from '../../utils/webdav.utils'; -import { DriveFileService } from '../../services/drive/drive-file.service'; -import { NetworkFacade } from '../../services/network/network-facade.service'; -import { DownloadService } from '../../services/network/download.service'; -import { CryptoService } from '../../services/crypto.service'; import { AuthService } from '../../services/auth.service'; import { NotFoundError } from '../../utils/errors.utils'; import { webdavLogger } from '../../utils/logger.utils'; import { NetworkUtils } from '../../utils/network.utils'; import { NotValidFileIdError } from '../../types/command.types'; +import { CLIUtils } from '../../utils/cli.utils'; export class GETRequestHandler implements WebDavMethodHandler { - constructor( - private readonly dependencies: { - driveFileService: DriveFileService; - downloadService: DownloadService; - cryptoService: CryptoService; - authService: AuthService; - networkFacade: NetworkFacade; - }, - ) {} - handle = async (req: Request, res: Response) => { - const { driveFileService, authService, networkFacade } = this.dependencies; const resource = await WebDavUtils.getRequestedResource(req.url); webdavLogger.info(`[GET] Request received item at ${resource.url}`); - const driveFile = await WebDavUtils.getDriveFileFromResource({ - url: resource.url, - driveFileService, - }); + const driveFile = await WebDavUtils.getDriveFileFromResource(resource.url); if (!driveFile) { throw new NotFoundError( @@ -40,7 +23,7 @@ export class GETRequestHandler implements WebDavMethodHandler { webdavLogger.info(`[GET] [${driveFile.uuid}] Found Drive File`); - const { user } = await authService.getAuthDetails(); + const { user } = await AuthService.instance.getAuthDetails(); webdavLogger.info(`[GET] [${driveFile.uuid}] Network ready for download`); res.header('Content-Type', 'application/octet-stream'); @@ -73,9 +56,11 @@ export class GETRequestHandler implements WebDavMethodHandler { throw new NotValidFileIdError(); } + const { networkFacade, bucket, mnemonic } = await CLIUtils.prepareNetwork(user); + const [executeDownload] = await networkFacade.downloadToStream( - driveFile.bucket, - user.mnemonic, + bucket, + mnemonic, driveFile.fileId, contentLength, writable, diff --git a/src/webdav/handlers/HEAD.handler.ts b/src/webdav/handlers/HEAD.handler.ts index c7a99214..38a921a6 100644 --- a/src/webdav/handlers/HEAD.handler.ts +++ b/src/webdav/handlers/HEAD.handler.ts @@ -2,28 +2,17 @@ import { Request, Response } from 'express'; import { WebDavMethodHandler } from '../../types/webdav.types'; import { WebDavUtils } from '../../utils/webdav.utils'; import { webdavLogger } from '../../utils/logger.utils'; -import { DriveFileService } from '../../services/drive/drive-file.service'; import { NetworkUtils } from '../../utils/network.utils'; import { NotFoundError } from '../../utils/errors.utils'; export class HEADRequestHandler implements WebDavMethodHandler { - constructor( - private readonly dependencies: { - driveFileService: DriveFileService; - }, - ) {} - handle = async (req: Request, res: Response) => { - const { driveFileService } = this.dependencies; const resource = await WebDavUtils.getRequestedResource(req.url); webdavLogger.info(`[HEAD] Request received for file at ${resource.url}`); try { - const driveFile = await WebDavUtils.getDriveFileFromResource({ - url: resource.url, - driveFileService, - }); + const driveFile = await WebDavUtils.getDriveFileFromResource(resource.url); if (!driveFile) { throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`); diff --git a/src/webdav/handlers/MKCOL.handler.ts b/src/webdav/handlers/MKCOL.handler.ts index 23264c9e..f015cc90 100644 --- a/src/webdav/handlers/MKCOL.handler.ts +++ b/src/webdav/handlers/MKCOL.handler.ts @@ -1,34 +1,22 @@ import { WebDavMethodHandler } from '../../types/webdav.types'; import { Request, Response } from 'express'; import { WebDavUtils } from '../../utils/webdav.utils'; -import { DriveFolderService } from '../../services/drive/drive-folder.service'; import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; import { WebDavFolderService } from '../services/webdav-folder.service'; import { MethodNotAllowed } from '../../utils/errors.utils'; export class MKCOLRequestHandler implements WebDavMethodHandler { - constructor( - private readonly dependencies: { - driveFolderService: DriveFolderService; - webDavFolderService: WebDavFolderService; - }, - ) {} - handle = async (req: Request, res: Response) => { - const { driveFolderService, webDavFolderService } = this.dependencies; const resource = await WebDavUtils.getRequestedResource(req.url); webdavLogger.info(`[MKCOL] Request received for folder at ${resource.url}`); const parentDriveFolderItem = - (await webDavFolderService.getDriveFolderItemFromPath(resource.parentPath)) ?? - (await webDavFolderService.createParentPathOrThrow(resource.parentPath)); + (await WebDavFolderService.instance.getDriveFolderItemFromPath(resource.parentPath)) ?? + (await WebDavFolderService.instance.createParentPathOrThrow(resource.parentPath)); - const driveFolderItem = await WebDavUtils.getDriveFolderFromResource({ - url: resource.url, - driveFolderService, - }); + const driveFolderItem = await WebDavUtils.getDriveFolderFromResource(resource.url); const folderAlreadyExists = !!driveFolderItem; @@ -37,7 +25,7 @@ export class MKCOLRequestHandler implements WebDavMethodHandler { throw new MethodNotAllowed('Folder already exists'); } - const newFolder = await webDavFolderService.createFolder({ + const newFolder = await WebDavFolderService.instance.createFolder({ folderName: resource.path.base, parentFolderUuid: parentDriveFolderItem.uuid, }); diff --git a/src/webdav/handlers/MOVE.handler.ts b/src/webdav/handlers/MOVE.handler.ts index 0d68fc12..c3df9e54 100644 --- a/src/webdav/handlers/MOVE.handler.ts +++ b/src/webdav/handlers/MOVE.handler.ts @@ -8,16 +8,7 @@ import { WebDavUtils } from '../../utils/webdav.utils'; import { WebDavFolderService } from '../services/webdav-folder.service'; export class MOVERequestHandler implements WebDavMethodHandler { - constructor( - private readonly dependencies: { - driveFolderService: DriveFolderService; - driveFileService: DriveFileService; - webDavFolderService: WebDavFolderService; - }, - ) {} - handle = async (req: Request, res: Response) => { - const { driveFolderService, driveFileService, webDavFolderService } = this.dependencies; const resource = await WebDavUtils.getRequestedResource(req.url); webdavLogger.info(`[MOVE] Request received for item at ${resource.url}`); @@ -31,11 +22,7 @@ export class MOVERequestHandler implements WebDavMethodHandler { webdavLogger.info('[MOVE] Destination resource found', { destinationResource }); - const originalDriveItem = await WebDavUtils.getDriveItemFromResource({ - resource, - driveFolderService, - driveFileService, - }); + const originalDriveItem = await WebDavUtils.getDriveItemFromResource(resource); if (!originalDriveItem) { throw new NotFoundError(`Resource not found on Internxt Drive at ${resource.url}`); @@ -50,7 +37,7 @@ export class MOVERequestHandler implements WebDavMethodHandler { if (originalDriveItem.itemType === 'folder') { const folder = originalDriveItem; - await driveFolderService.renameFolder({ + await DriveFolderService.instance.renameFolder({ folderUuid: folder.uuid, name: destinationResource.name, }); @@ -58,7 +45,7 @@ export class MOVERequestHandler implements WebDavMethodHandler { const file = originalDriveItem; const plainName = destinationResource.path.name; const fileType = destinationResource.path.ext.replace('.', ''); - await driveFileService.renameFile(file.uuid, { + await DriveFileService.instance.renameFile(file.uuid, { plainName: plainName, type: fileType, }); @@ -70,12 +57,12 @@ export class MOVERequestHandler implements WebDavMethodHandler { ); const destinationFolderItem = - (await webDavFolderService.getDriveFolderItemFromPath(destinationResource.parentPath)) ?? - (await webDavFolderService.createParentPathOrThrow(destinationResource.parentPath)); + (await WebDavFolderService.instance.getDriveFolderItemFromPath(destinationResource.parentPath)) ?? + (await WebDavFolderService.instance.createParentPathOrThrow(destinationResource.parentPath)); if (originalDriveItem.itemType === 'folder') { const folder = originalDriveItem; - await driveFolderService.moveFolder(folder.uuid, { + await DriveFolderService.instance.moveFolder(folder.uuid, { destinationFolder: destinationFolderItem.uuid, name: destinationResource.name, }); @@ -83,7 +70,7 @@ export class MOVERequestHandler implements WebDavMethodHandler { const file = originalDriveItem; const plainName = destinationResource.path.name; const fileType = destinationResource.path.ext.replace('.', ''); - await driveFileService.moveFile(file.uuid, { + await DriveFileService.instance.moveFile(file.uuid, { destinationFolder: destinationFolderItem.uuid, name: plainName, type: fileType, diff --git a/src/webdav/handlers/PROPFIND.handler.ts b/src/webdav/handlers/PROPFIND.handler.ts index b55be4a4..c60c7b1b 100644 --- a/src/webdav/handlers/PROPFIND.handler.ts +++ b/src/webdav/handlers/PROPFIND.handler.ts @@ -2,7 +2,6 @@ import { WebDavMethodHandler, WebDavRequestedResource } from '../../types/webdav import { XMLUtils } from '../../utils/xml.utils'; import { DriveFileItem, DriveFolderItem } from '../../types/drive.types'; import { DriveFolderService } from '../../services/drive/drive-folder.service'; -import { DriveFileService } from '../../services/drive/drive-file.service'; import { FormatUtils } from '../../utils/format.utils'; import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; @@ -12,23 +11,11 @@ import { webdavLogger } from '../../utils/logger.utils'; import { UsageService } from '../../services/usage.service'; export class PROPFINDRequestHandler implements WebDavMethodHandler { - constructor( - private readonly dependencies: { - driveFolderService: DriveFolderService; - driveFileService: DriveFileService; - }, - ) {} - handle = async (req: Request, res: Response) => { - const { driveFolderService, driveFileService } = this.dependencies; const resource = await WebDavUtils.getRequestedResource(req.url); webdavLogger.info(`[PROPFIND] Request received for item at ${resource.url}`); - const driveItem = await WebDavUtils.getDriveItemFromResource({ - resource, - driveFolderService, - driveFileService, - }); + const driveItem = await WebDavUtils.getDriveItemFromResource(resource); if (!driveItem) { res.status(404).send(); @@ -101,9 +88,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { }; private readonly getFolderChildsXMLNode = async (relativePath: string, folderUuid: string) => { - const { driveFolderService } = this.dependencies; - - const folderContent = await driveFolderService.getFolderContent(folderUuid); + const folderContent = await DriveFolderService.instance.getFolderContent(folderUuid); const foldersXML = folderContent.folders.map((folder) => { const folderRelativePath = WebDavUtils.joinURL(relativePath, folder.plainName, '/'); @@ -160,7 +145,7 @@ export class PROPFINDRequestHandler implements WebDavMethodHandler { driveFolderItem: DriveFolderItem, relativePath: string, ): Promise => { - const totalUsage = (await UsageService.instance.fetchUsage()).total; + const totalUsage = await UsageService.instance.fetchUsage(); const spaceLimit = await UsageService.instance.fetchSpaceLimit(); const driveFolderXML = { diff --git a/src/webdav/handlers/PUT.handler.ts b/src/webdav/handlers/PUT.handler.ts index b46caf45..8e3a155e 100644 --- a/src/webdav/handlers/PUT.handler.ts +++ b/src/webdav/handlers/PUT.handler.ts @@ -1,6 +1,5 @@ import { Request, Response } from 'express'; import { DriveFileService } from '../../services/drive/drive-file.service'; -import { NetworkFacade } from '../../services/network/network-facade.service'; import { AuthService } from '../../services/auth.service'; import { WebDavMethodHandler } from '../../types/webdav.types'; import { NotFoundError } from '../../utils/errors.utils'; @@ -11,23 +10,12 @@ import { EncryptionVersion } from '@internxt/sdk/dist/drive/storage/types'; import { CLIUtils } from '../../utils/cli.utils'; import { BufferStream } from '../../utils/stream.utils'; import { Readable } from 'node:stream'; -import { isFileThumbnailable, tryUploadThumbnail } from '../../utils/thumbnail.utils'; import { WebDavFolderService } from '../services/webdav-folder.service'; import { AsyncUtils } from '../../utils/async.utils'; -import { DriveFolderService } from '../../services/drive/drive-folder.service'; +import { ThumbnailUtils } from '../../utils/thumbnail.utils'; +import { ThumbnailService } from '../../services/thumbnail.service'; export class PUTRequestHandler implements WebDavMethodHandler { - constructor( - private readonly dependencies: { - driveFileService: DriveFileService; - driveFolderService: DriveFolderService; - webDavFolderService: WebDavFolderService; - trashService: TrashService; - authService: AuthService; - networkFacade: NetworkFacade; - }, - ) {} - handle = async (req: Request, res: Response) => { let contentLength = Number(req.headers['content-length']); if (!contentLength || Number.isNaN(contentLength) || contentLength <= 0) { @@ -38,11 +26,7 @@ export class PUTRequestHandler implements WebDavMethodHandler { // If the file already exists, the WebDAV specification states that 'PUT /…/file' should replace it. // http://www.webdav.org/specs/rfc4918.html#put-resources - const driveFileItem = await WebDavUtils.getDriveItemFromResource({ - resource: resource, - driveFileService: this.dependencies.driveFileService, - driveFolderService: this.dependencies.driveFolderService, - }); + const driveFileItem = await WebDavUtils.getDriveItemFromResource(resource); if (driveFileItem?.itemType === 'folder') { throw new NotFoundError('Folders cannot be created with PUT. Use MKCOL instead.'); } @@ -58,13 +42,13 @@ export class PUTRequestHandler implements WebDavMethodHandler { }; const parentDriveFolderItem = - (await this.dependencies.webDavFolderService.getDriveFolderItemFromPath(resource.parentPath)) ?? - (await this.dependencies.webDavFolderService.createParentPathOrThrow(resource.parentPath)); + (await WebDavFolderService.instance.getDriveFolderItemFromPath(resource.parentPath)) ?? + (await WebDavFolderService.instance.createParentPathOrThrow(resource.parentPath)); try { if (driveFileItem && driveFileItem.status === 'EXISTS') { webdavLogger.info(`[PUT] File '${resource.name}' already exists in '${resource.path.dir}', trashing it...`); - await this.dependencies.trashService.trashItems({ + await TrashService.instance.trashItems({ items: [{ type: driveFileItem.itemType, uuid: driveFileItem.uuid }], }); } @@ -72,18 +56,20 @@ export class PUTRequestHandler implements WebDavMethodHandler { //noop } - const { user } = await this.dependencies.authService.getAuthDetails(); + const { user } = await AuthService.instance.getAuthDetails(); const fileType = resource.path.ext.replace('.', ''); let bufferStream: BufferStream | undefined; let fileStream: Readable = req; - const isThumbnailable = isFileThumbnailable(fileType); + const isThumbnailable = ThumbnailUtils.isFileThumbnailable(fileType); if (isThumbnailable) { bufferStream = new BufferStream(); fileStream = req.pipe(bufferStream); } + const { networkFacade, bucket } = await CLIUtils.prepareNetwork(user); + let fileId: string | undefined; if (contentLength > 0) { @@ -98,10 +84,10 @@ export class PUTRequestHandler implements WebDavMethodHandler { const networkUploadTimer = CLIUtils.timer(); fileId = await new Promise((resolve: (fileId: string) => void, reject) => { - const state = this.dependencies.networkFacade.uploadFile( + const state = networkFacade.uploadFile( fileStream, contentLength, - user.bucket, + bucket, (err: Error | null, res: string | null) => { if (err) { aborted = true; @@ -131,20 +117,20 @@ export class PUTRequestHandler implements WebDavMethodHandler { type: fileType, size: contentLength, folderUuid: parentDriveFolderItem.uuid, - fileId: fileId, - bucket: user.bucket, + fileId, + bucket, encryptVersion: EncryptionVersion.Aes03, }); timings.driveUpload = driveTimer.stop(); const thumbnailTimer = CLIUtils.timer(); if (contentLength > 0 && isThumbnailable && bufferStream) { - void tryUploadThumbnail({ + void ThumbnailService.instance.tryUploadThumbnail({ + fileUuid: file.uuid, bufferStream, fileType, - userBucket: user.bucket, - fileUuid: file.uuid, - networkFacade: this.dependencies.networkFacade, + bucket, + networkFacade, }); } timings.thumbnailUpload = thumbnailTimer.stop(); @@ -155,10 +141,10 @@ export class PUTRequestHandler implements WebDavMethodHandler { webdavLogger.info(`[PUT] ✅ File uploaded in ${CLIUtils.formatDuration(totalTime)} to Internxt Drive`); webdavLogger.info( - `[PUT] Timing breakdown:\n - Network upload: ${CLIUtils.formatDuration(timings.networkUpload)} (${throughputMBps.toFixed(2)} MB/s)\n - Drive upload: ${CLIUtils.formatDuration(timings.driveUpload)}\n - Thumbnail: ${CLIUtils.formatDuration(timings.thumbnailUpload)}\n`, + '[PUT] Timing breakdown:\n' + + `Network upload: ${CLIUtils.formatDuration(timings.networkUpload)} (${throughputMBps.toFixed(2)} MB/s)\n` + + `Drive upload: ${CLIUtils.formatDuration(timings.driveUpload)}\n` + + `Thumbnail: ${CLIUtils.formatDuration(timings.thumbnailUpload)}\n`, ); // Wait for backend search index to propagate (same as folder creation delay in PB-1446) diff --git a/src/webdav/index.ts b/src/webdav/index.ts index 15e8d767..b6c8b06c 100644 --- a/src/webdav/index.ts +++ b/src/webdav/index.ts @@ -2,12 +2,7 @@ import dotenv from 'dotenv'; import { WebDavServer } from './webdav-server'; import express from 'express'; import { ConfigService } from '../services/config.service'; -import { DriveFolderService } from '../services/drive/drive-folder.service'; -import { DriveFileService } from '../services/drive/drive-file.service'; -import { DownloadService } from '../services/network/download.service'; import { AuthService } from '../services/auth.service'; -import { CryptoService } from '../services/crypto.service'; -import { TrashService } from '../services/drive/trash.service'; import { webdavLogger } from '../utils/logger.utils'; import { SdkManager } from '../services/sdk-manager.service'; @@ -18,19 +13,10 @@ const init = async () => { await ConfigService.instance.ensureWebdavCertsDirExists(); await ConfigService.instance.ensureInternxtLogsDirExists(); - const { token } = await AuthService.instance.getAuthDetails(); - SdkManager.init({ token }); + const { token, workspace } = await AuthService.instance.getAuthDetails(); + SdkManager.init({ token, workspaceToken: workspace?.workspaceCredentials.token }); - new WebDavServer( - express(), - ConfigService.instance, - DriveFileService.instance, - DriveFolderService.instance, - DownloadService.instance, - AuthService.instance, - CryptoService.instance, - TrashService.instance, - ) + new WebDavServer(express()) .start() .then() .catch((err) => webdavLogger.error('Failed to start WebDAV server', err)); diff --git a/src/webdav/middewares/auth.middleware.ts b/src/webdav/middewares/auth.middleware.ts index f00c45d1..d2e2190f 100644 --- a/src/webdav/middewares/auth.middleware.ts +++ b/src/webdav/middewares/auth.middleware.ts @@ -3,18 +3,18 @@ import { SdkManager } from '../../services/sdk-manager.service'; import { AuthService } from '../../services/auth.service'; import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; -import { isError } from '../../utils/errors.utils'; +import { ErrorUtils } from '../../utils/errors.utils'; -export const AuthMiddleware = (authService: AuthService): RequestHandler => { +export const AuthMiddleware = (): RequestHandler => { return (_, res, next) => { (async () => { try { - const { token } = await authService.getAuthDetails(); - SdkManager.init({ token }); + const { token, workspace } = await AuthService.instance.getAuthDetails(); + SdkManager.init({ token, workspaceToken: workspace?.workspaceCredentials.token }); next(); } catch (error) { let message = 'Authentication required to access this resource.'; - if (isError(error)) { + if (ErrorUtils.isError(error)) { message = error.message; if (error.stack) { webdavLogger.error(`Error from AuthMiddleware: ${message}\nStack: ${error.stack}`); diff --git a/src/webdav/middewares/errors.middleware.ts b/src/webdav/middewares/errors.middleware.ts index 4cc8200e..5ce536cd 100644 --- a/src/webdav/middewares/errors.middleware.ts +++ b/src/webdav/middewares/errors.middleware.ts @@ -1,13 +1,13 @@ import { ErrorRequestHandler } from 'express'; import { webdavLogger } from '../../utils/logger.utils'; import { XMLUtils } from '../../utils/xml.utils'; -import { isError } from '../../utils/errors.utils'; +import { ErrorUtils } from '../../utils/errors.utils'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const ErrorHandlingMiddleware: ErrorRequestHandler = (err, req, res, _) => { - const message = isError(err) ? err.message : 'Something went wrong'; + const message = ErrorUtils.isError(err) ? err.message : 'Something went wrong'; - if (isError(err) && err.stack) { + if (ErrorUtils.isError(err) && err.stack) { webdavLogger.error(`[ERROR MIDDLEWARE] [${req.method.toUpperCase()} - ${req.url}] ${message}\nStack: ${err.stack}`); } else { webdavLogger.error(`[ERROR MIDDLEWARE] [${req.method.toUpperCase()} - ${req.url}] ${message}`); diff --git a/src/webdav/services/webdav-folder.service.ts b/src/webdav/services/webdav-folder.service.ts index f38ad6cb..b49ed15d 100644 --- a/src/webdav/services/webdav-folder.service.ts +++ b/src/webdav/services/webdav-folder.service.ts @@ -8,19 +8,11 @@ import { AuthService } from '../../services/auth.service'; import { DriveUtils } from '../../utils/drive.utils'; export class WebDavFolderService { - constructor( - private readonly dependencies: { - driveFolderService: DriveFolderService; - configService: ConfigService; - }, - ) {} + public static readonly instance: WebDavFolderService = new WebDavFolderService(); public getDriveFolderItemFromPath = async (path: string): Promise => { const { url } = await WebDavUtils.getRequestedResource(path, false); - return await WebDavUtils.getDriveFolderFromResource({ - url, - driveFolderService: this.dependencies.driveFolderService, - }); + return await WebDavUtils.getDriveFolderFromResource(url); }; public createFolder = async ({ @@ -30,7 +22,7 @@ export class WebDavFolderService { folderName: string; parentFolderUuid: string; }): Promise => { - const [createFolderPromise] = this.dependencies.driveFolderService.createFolder({ + const [createFolderPromise] = await DriveFolderService.instance.createFolder({ plainName: folderName, parentFolderUuid: parentFolderUuid, }); @@ -44,7 +36,7 @@ export class WebDavFolderService { }; public createParentPathOrThrow = async (parentPath: string): Promise => { - const { createFullPath } = await this.dependencies.configService.readWebdavConfig(); + const { createFullPath } = await ConfigService.instance.readWebdavConfig(); if (!createFullPath) { // WebDAV RFC: https://datatracker.ietf.org/doc/html/rfc4918#section-9.7.1 // When the PUT operation creates a new resource, diff --git a/src/webdav/webdav-server.ts b/src/webdav/webdav-server.ts index e39905e5..4c13b994 100644 --- a/src/webdav/webdav-server.ts +++ b/src/webdav/webdav-server.ts @@ -6,19 +6,13 @@ import { OPTIONSRequestHandler } from './handlers/OPTIONS.handler'; import { PROPFINDRequestHandler } from './handlers/PROPFIND.handler'; import { webdavLogger } from '../utils/logger.utils'; import bodyParser from 'body-parser'; -import { DriveFolderService } from '../services/drive/drive-folder.service'; import { AuthMiddleware } from './middewares/auth.middleware'; import { RequestLoggerMiddleware } from './middewares/request-logger.middleware'; import { GETRequestHandler } from './handlers/GET.handler'; import { HEADRequestHandler } from './handlers/HEAD.handler'; -import { DriveFileService } from '../services/drive/drive-file.service'; -import { DownloadService } from '../services/network/download.service'; -import { AuthService } from '../services/auth.service'; -import { CryptoService } from '../services/crypto.service'; import { ErrorHandlingMiddleware } from './middewares/errors.middleware'; import asyncHandler from 'express-async-handler'; import { SdkManager } from '../services/sdk-manager.service'; -import { NetworkFacade } from '../services/network/network-facade.service'; import { NetworkUtils } from '../utils/network.utils'; import { PUTRequestHandler } from './handlers/PUT.handler'; import { MKCOLRequestHandler } from './handlers/MKCOL.handler'; @@ -26,50 +20,13 @@ import { DELETERequestHandler } from './handlers/DELETE.handler'; 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'; import { MkcolMiddleware } from './middewares/mkcol.middleware'; -import { WebDavFolderService } from './services/webdav-folder.service'; export class WebDavServer { - constructor( - private readonly app: Express, - private readonly configService: ConfigService, - private readonly driveFileService: DriveFileService, - private readonly driveFolderService: DriveFolderService, - private readonly downloadService: DownloadService, - private readonly authService: AuthService, - private readonly cryptoService: CryptoService, - private readonly trashService: TrashService, - ) {} - - private readonly getNetworkFacade = async () => { - const credentials = await this.configService.readUser(); - - if (!credentials) throw new Error('Credentials not found in Config service, do login first'); - const networkModule = SdkManager.instance.getNetwork({ - 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, - appDetails: SdkManager.getAppDetails(), - }); - const networkFacade = new NetworkFacade( - networkModule, - environment, - DownloadService.instance, - CryptoService.instance, - ); - - return networkFacade; - }; + constructor(private readonly app: Express) {} private readonly registerStartMiddlewares = () => { - this.app.use(AuthMiddleware(AuthService.instance)); + this.app.use(AuthMiddleware()); this.app.use( RequestLoggerMiddleware({ enable: true, @@ -85,91 +42,22 @@ export class WebDavServer { private readonly registerHandlers = async () => { const serverListenPath = /(.*)/; - const networkFacade = await this.getNetworkFacade(); - const webDavFolderService = new WebDavFolderService({ - driveFolderService: this.driveFolderService, - configService: this.configService, - }); - this.app.head( - serverListenPath, - asyncHandler( - new HEADRequestHandler({ - driveFileService: this.driveFileService, - }).handle, - ), - ); - this.app.get( - serverListenPath, - asyncHandler( - new GETRequestHandler({ - driveFileService: this.driveFileService, - downloadService: this.downloadService, - cryptoService: this.cryptoService, - authService: this.authService, - networkFacade: networkFacade, - }).handle, - ), - ); + this.app.head(serverListenPath, asyncHandler(new HEADRequestHandler().handle)); + this.app.get(serverListenPath, asyncHandler(new GETRequestHandler().handle)); this.app.options(serverListenPath, asyncHandler(new OPTIONSRequestHandler().handle)); - this.app.propfind( - serverListenPath, - asyncHandler( - new PROPFINDRequestHandler({ - driveFileService: this.driveFileService, - driveFolderService: this.driveFolderService, - }).handle, - ), - ); + this.app.propfind(serverListenPath, asyncHandler(new PROPFINDRequestHandler().handle)); - this.app.put( - serverListenPath, - asyncHandler( - new PUTRequestHandler({ - driveFileService: this.driveFileService, - driveFolderService: this.driveFolderService, - webDavFolderService: webDavFolderService, - authService: this.authService, - trashService: this.trashService, - networkFacade: networkFacade, - }).handle, - ), - ); + this.app.put(serverListenPath, asyncHandler(new PUTRequestHandler().handle)); - this.app.mkcol( - serverListenPath, - asyncHandler( - new MKCOLRequestHandler({ - driveFolderService: this.driveFolderService, - webDavFolderService: webDavFolderService, - }).handle, - ), - ); - this.app.delete( - serverListenPath, - asyncHandler( - new DELETERequestHandler({ - trashService: this.trashService, - driveFileService: this.driveFileService, - driveFolderService: this.driveFolderService, - }).handle, - ), - ); + this.app.mkcol(serverListenPath, asyncHandler(new MKCOLRequestHandler().handle)); + this.app.delete(serverListenPath, asyncHandler(new DELETERequestHandler().handle)); this.app.proppatch(serverListenPath, asyncHandler(new PROPPATCHRequestHandler().handle)); - this.app.move( - serverListenPath, - asyncHandler( - new MOVERequestHandler({ - driveFolderService: this.driveFolderService, - driveFileService: this.driveFileService, - webDavFolderService, - }).handle, - ), - ); + this.app.move(serverListenPath, asyncHandler(new MOVERequestHandler().handle)); this.app.copy(serverListenPath, asyncHandler(new COPYRequestHandler().handle)); }; start = async () => { - const configs = await this.configService.readWebdavConfig(); + const configs = await ConfigService.instance.readWebdavConfig(); this.app.disable('x-powered-by'); this.registerStartMiddlewares(); await this.registerHandlers(); diff --git a/test/commands/login.test.ts b/test/commands/login.test.ts index cd2e9655..a4d1407c 100644 --- a/test/commands/login.test.ts +++ b/test/commands/login.test.ts @@ -6,12 +6,6 @@ import LoginLegacy from '../../src/commands/login-legacy'; import { AuthService } from '../../src/services/auth.service'; import { CLIUtils, NoFlagProvidedError } from '../../src/utils/cli.utils'; -vi.mock('../../src/utils/logger.utils', () => ({ - logger: { - error: vi.fn(), - }, -})); - describe('Login Command', () => { beforeEach(() => { vi.restoreAllMocks(); diff --git a/test/commands/upload-folder.test.ts b/test/commands/upload-folder.test.ts index 02b44b11..2cb94223 100644 --- a/test/commands/upload-folder.test.ts +++ b/test/commands/upload-folder.test.ts @@ -1,48 +1,47 @@ import { beforeEach, describe, expect, it, MockInstance, vi } from 'vitest'; import UploadFolder from '../../src/commands/upload-folder'; -import { AuthService } from '../../src/services/auth.service'; -import { LoginCredentials, MissingCredentialsError } from '../../src/types/command.types'; +import { LoginCredentials } from '../../src/types/command.types'; import { ValidationService } from '../../src/services/validation.service'; import { UserFixture } from '../fixtures/auth.fixture'; import { CLIUtils, NoFlagProvidedError } from '../../src/utils/cli.utils'; import { UploadResult } from '../../src/services/network/upload/upload.types'; import { UploadFacade } from '../../src/services/network/upload/upload-facade.service'; - -vi.mock('../../src/utils/async.utils', () => ({ - AsyncUtils: { - sleep: vi.fn().mockResolvedValue(undefined), - }, -})); +import { AsyncUtils } from '../../src/utils/async.utils'; +import { ConfigService } from '../../src/services/config.service'; describe('Upload Folder Command', () => { - let getAuthDetailsSpy: MockInstance<() => Promise>; + let configReadUserSpy: MockInstance<() => Promise>; let validateDirectoryExistsSpy: MockInstance<(path: string) => Promise>; - let getDestinationFolderUuidSpy: MockInstance<() => Promise>; + let getDestinationFolderUuidSpy: MockInstance<() => Promise>; let UploadFacadeSpy: MockInstance<() => Promise>; let cliSuccessSpy: MockInstance<() => void>; + const uploadedResult: UploadResult = { totalBytes: 1024, rootFolderId: 'root-folder-id', uploadTimeMs: 1500, }; + beforeEach(() => { vi.restoreAllMocks(); - getAuthDetailsSpy = vi.spyOn(AuthService.instance, 'getAuthDetails').mockResolvedValue({ + configReadUserSpy = vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue({ user: UserFixture, token: 'mock-token', }); + vi.spyOn(ConfigService.instance, 'saveUser').mockResolvedValue(undefined); validateDirectoryExistsSpy = vi .spyOn(ValidationService.instance, 'validateDirectoryExists') .mockResolvedValue(true); - getDestinationFolderUuidSpy = vi.spyOn(CLIUtils, 'getDestinationFolderUuid').mockResolvedValue(undefined); + getDestinationFolderUuidSpy = vi.spyOn(CLIUtils, 'getDestinationFolderUuid').mockResolvedValue(''); UploadFacadeSpy = vi.spyOn(UploadFacade.instance, 'uploadFolder').mockResolvedValue(uploadedResult); cliSuccessSpy = vi.spyOn(CLIUtils, 'success').mockImplementation(() => {}); + vi.spyOn(AsyncUtils, 'sleep').mockResolvedValue(undefined); }); it('should call UploadFacade when user uploads a folder with valid path', async () => { await UploadFolder.run(['--folder=/valid/folder/path']); - expect(getAuthDetailsSpy).toHaveBeenCalledOnce(); + expect(configReadUserSpy).toHaveBeenCalledTimes(2); expect(validateDirectoryExistsSpy).toHaveBeenCalledWith('/valid/folder/path'); expect(getDestinationFolderUuidSpy).toHaveBeenCalledOnce(); expect(UploadFacadeSpy).toHaveBeenCalledWith( @@ -64,7 +63,7 @@ describe('Upload Folder Command', () => { await UploadFolder.run(['--folder=/valid/folder/path', `--destination=${customDestinationId}`]); - expect(getAuthDetailsSpy).toHaveBeenCalledOnce(); + expect(configReadUserSpy).toHaveBeenCalledOnce(); expect(validateDirectoryExistsSpy).toHaveBeenCalledWith('/valid/folder/path'); expect(getDestinationFolderUuidSpy).toHaveBeenCalledOnce(); expect(UploadFacadeSpy).toHaveBeenCalledWith( @@ -114,23 +113,21 @@ describe('Upload Folder Command', () => { oclif: { exit: 1 }, }); - expect(getAuthDetailsSpy).toHaveBeenCalledOnce(); + expect(configReadUserSpy).toHaveBeenCalledOnce(); expect(validateDirectoryExistsSpy).toHaveBeenCalledWith(invalidPath); expect(UploadFacadeSpy).not.toHaveBeenCalled(); }); it('should throw an error when user does not have credentials', async () => { - const getAuthDetailsSpy = vi - .spyOn(AuthService.instance, 'getAuthDetails') - .mockRejectedValue(new MissingCredentialsError()); + const readUserSpy = vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(undefined); const result = UploadFolder.run(['--folder=/some/folder/path']); + await expect(result).rejects.toMatchObject({ message: expect.stringContaining('EEXIT: 1'), oclif: { exit: 1 }, }); - - expect(getAuthDetailsSpy).toHaveBeenCalledOnce(); + expect(readUserSpy).toHaveBeenCalledOnce(); expect(validateDirectoryExistsSpy).not.toHaveBeenCalled(); expect(UploadFacadeSpy).not.toHaveBeenCalled(); }); diff --git a/test/fixtures/webdav.fixture.ts b/test/fixtures/webdav.fixture.ts index 06ecbffb..694b1299 100644 --- a/test/fixtures/webdav.fixture.ts +++ b/test/fixtures/webdav.fixture.ts @@ -2,6 +2,12 @@ import { getMockReq, getMockRes } from 'vitest-mock-express'; import { Request, Response } from 'express'; import { WebDavRequestedResource } from '../../src/types/webdav.types'; import path from 'node:path'; +import { SdkManager } from '../../src/services/sdk-manager.service'; +import { Environment } from '@internxt/inxt-js'; +import { ConfigService } from '../../src/services/config.service'; +import { UserFixture } from './auth.fixture'; +import { NetworkFacade } from '../../src/services/network/network-facade.service'; +import { NetworkOptions } from '../../src/types/network.types'; export const createWebDavRequestFixture = (request: T): T & Request => { return getMockReq({ @@ -56,3 +62,33 @@ export const getRequestedFolderResource = ({ url: completeURL, }; }; + +export const getNetworkMock = () => { + return SdkManager.instance.getNetwork({ + user: 'user', + pass: 'pass', + }); +}; + +export const getEnvironmentMock = () => { + return new Environment({ + bridgeUser: 'user', + bridgePass: 'pass', + bridgeUrl: ConfigService.instance.get('NETWORK_URL'), + encryptionKey: UserFixture.mnemonic, + appDetails: SdkManager.getAppDetails(), + }); +}; + +export const getNetworkFacadeMock = () => { + return new NetworkFacade(getNetworkMock(), getEnvironmentMock()); +}; + +export const getNetworkOptionsMock = (attributes?: Partial): NetworkOptions => { + const options: NetworkOptions = { + networkFacade: new NetworkFacade(getNetworkMock(), getEnvironmentMock()), + bucket: UserFixture.bucket, + mnemonic: UserFixture.mnemonic, + }; + return { ...options, ...attributes }; +}; diff --git a/test/services/auth.service.test.ts b/test/services/auth.service.test.ts index c9012ab7..e6f320ef 100644 --- a/test/services/auth.service.test.ts +++ b/test/services/auth.service.test.ts @@ -20,6 +20,9 @@ import { paths } from '@internxt/sdk/dist/schema'; describe('Auth service', () => { beforeEach(() => { vi.restoreAllMocks(); + + vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture); + vi.spyOn(ConfigService.instance, 'saveUser').mockResolvedValue(undefined); }); it('When user logs in, then login user credentials are generated', async () => { diff --git a/test/services/drive/drive-file.service.test.ts b/test/services/drive/drive-file.service.test.ts index 6616f310..557e4a51 100644 --- a/test/services/drive/drive-file.service.test.ts +++ b/test/services/drive/drive-file.service.test.ts @@ -5,12 +5,17 @@ import Storage, { DriveFileData, EncryptionVersion } from '@internxt/sdk/dist/dr import { Drive } from '@internxt/sdk'; import { randomUUID } from 'node:crypto'; import { CommonFixture } from '../../fixtures/common.fixture'; +import { ConfigService } from '../../../src/services/config.service'; +import { UserCredentialsFixture } from '../../fixtures/login.fixture'; describe('Drive file Service', () => { const sut = DriveFileService.instance; beforeEach(() => { vi.restoreAllMocks(); + + vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture); + vi.spyOn(ConfigService.instance, 'saveUser').mockResolvedValue(undefined); }); it('When a file is created, should be created successfully', async () => { diff --git a/test/services/drive/drive-folder.service.test.ts b/test/services/drive/drive-folder.service.test.ts index fccd12ae..78d57110 100644 --- a/test/services/drive/drive-folder.service.test.ts +++ b/test/services/drive/drive-folder.service.test.ts @@ -6,12 +6,17 @@ import { SdkManager } from '../../../src/services/sdk-manager.service'; import { DriveUtils } from '../../../src/utils/drive.utils'; import { generateSubcontent, newCreateFolderResponse, newFolderMeta } from '../../fixtures/drive.fixture'; import { CreateFolderResponse, FetchPaginatedFile, FetchPaginatedFolder } from '@internxt/sdk/dist/drive/storage/types'; +import { ConfigService } from '../../../src/services/config.service'; +import { UserCredentialsFixture } from '../../fixtures/login.fixture'; describe('Drive folder Service', () => { const sut = DriveFolderService.instance; beforeEach(() => { vi.restoreAllMocks(); + + vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture); + vi.spyOn(ConfigService.instance, 'saveUser').mockResolvedValue(undefined); }); it('When folder metadata is requested by UUID, it is aquired successfully', async () => { @@ -87,7 +92,7 @@ describe('Drive folder Service', () => { ]); vi.spyOn(SdkManager.instance, 'getStorage').mockReturnValue(Storage.prototype); - const [createFolder] = sut.createFolder({ + const [createFolder] = await sut.createFolder({ plainName: newFolderResponse.plainName, parentFolderUuid: newFolderResponse.parentUuid, }); diff --git a/test/services/local-filesystem/local-filesystem.service.test.ts b/test/services/local-filesystem/local-filesystem.service.test.ts index 8f1bab17..9e03cd3f 100644 --- a/test/services/local-filesystem/local-filesystem.service.test.ts +++ b/test/services/local-filesystem/local-filesystem.service.test.ts @@ -1,24 +1,16 @@ import { beforeEach, describe, expect, it, vi, MockedFunction } from 'vitest'; import { LocalFilesystemService } from '../../../src/services/local-filesystem/local-filesystem.service'; -import { Dirent, promises, Stats } from 'fs'; +import { Dirent, promises, Stats } from 'node:fs'; import { logger } from '../../../src/utils/logger.utils'; import { FileSystemNode } from '../../../src/services/local-filesystem/local-filesystem.types'; -vi.mock('fs', () => ({ +vi.mock('node:fs', () => ({ promises: { stat: vi.fn(), readdir: vi.fn(), }, })); -vi.mock('../../../src/utils/logger.utils', () => ({ - logger: { - warn: vi.fn(), - error: vi.fn(), - info: vi.fn(), - }, -})); - describe('Local Filesystem Service', () => { let service: LocalFilesystemService; const mockStat = vi.mocked(promises.stat); @@ -40,8 +32,8 @@ describe('Local Filesystem Service', () => { }) as unknown as Dirent; beforeEach(() => { - service = LocalFilesystemService.instance; vi.clearAllMocks(); + service = LocalFilesystemService.instance; mockReaddir.mockResolvedValue([]); }); diff --git a/test/services/network/network-facade.service.test.ts b/test/services/network/network-facade.service.test.ts index 599d69b5..1aef275f 100644 --- a/test/services/network/network-facade.service.test.ts +++ b/test/services/network/network-facade.service.test.ts @@ -3,7 +3,6 @@ import { NetworkFacade } from '../../../src/services/network/network-facade.serv import { SdkManager } from '../../../src/services/sdk-manager.service'; import path from 'node:path'; import { createReadStream } from 'node:fs'; -import { CryptoService } from '../../../src/services/crypto.service'; import { DownloadService } from '../../../src/services/network/download.service'; import { Readable } from 'node:stream'; import axios from 'axios'; @@ -43,8 +42,6 @@ describe('Network Facade Service', () => { getNetworkMock(), // @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); @@ -68,8 +65,6 @@ describe('Network Facade Service', () => { getNetworkMock(), // @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); @@ -109,9 +104,8 @@ describe('Network Facade Service', () => { ], version: 2, }); - const downloadServiceStub = DownloadService.instance; - vi.spyOn(downloadServiceStub, 'downloadFile').mockResolvedValue(readableContent); - const sut = new NetworkFacade(networkMock, getEnvironmentMock(), downloadServiceStub, CryptoService.instance); + vi.spyOn(DownloadService.instance, 'downloadFile').mockResolvedValue(readableContent); + const sut = new NetworkFacade(networkMock, getEnvironmentMock()); const chunks: Uint8Array[] = []; @@ -162,9 +156,8 @@ describe('Network Facade Service', () => { ], version: 2, }); - const downloadServiceStub = DownloadService.instance; - vi.spyOn(downloadServiceStub, 'downloadFile').mockResolvedValue(readableContent); - const sut = new NetworkFacade(networkMock, getEnvironmentMock(), downloadServiceStub, CryptoService.instance); + vi.spyOn(DownloadService.instance, 'downloadFile').mockResolvedValue(readableContent); + const sut = new NetworkFacade(networkMock, getEnvironmentMock()); const writable = new WritableStream(); @@ -213,9 +206,8 @@ describe('Network Facade Service', () => { ], version: 2, }); - const downloadServiceStub = DownloadService.instance; - const sut = new NetworkFacade(networkMock, getEnvironmentMock(), downloadServiceStub, CryptoService.instance); + const sut = new NetworkFacade(networkMock, getEnvironmentMock()); const writable = new WritableStream(); diff --git a/test/services/network/upload/upload-facade.service.test.ts b/test/services/network/upload/upload-facade.service.test.ts index b2d1ed32..76a86d0b 100644 --- a/test/services/network/upload/upload-facade.service.test.ts +++ b/test/services/network/upload/upload-facade.service.test.ts @@ -5,72 +5,25 @@ import { logger } from '../../../../src/utils/logger.utils'; import { LocalFilesystemService } from '../../../../src/services/local-filesystem/local-filesystem.service'; import { UploadFolderService } from '../../../../src/services/network/upload/upload-folder.service'; import { UploadFileService } from '../../../../src/services/network/upload/upload-file.service'; -import { NetworkFacade } from '../../../../src/services/network/network-facade.service'; import { LoginUserDetails } from '../../../../src/types/command.types'; import { createFileSystemNodeFixture } from './upload.service.helpers'; import { AsyncUtils } from '../../../../src/utils/async.utils'; - -vi.mock('../../../../src/utils/cli.utils', () => ({ - CLIUtils: { - timer: vi.fn(), - prepareNetwork: vi.fn(), - }, -})); - -vi.mock('../../../../src/utils/logger.utils', () => ({ - logger: { - info: vi.fn(), - }, -})); - -vi.mock('../../../../src/services/local-filesystem/local-filesystem.service', () => ({ - LocalFilesystemService: { - instance: { - scanLocalDirectory: vi.fn(), - }, - }, -})); - -vi.mock('../../../../src/services/network/upload/upload-folder.service', () => ({ - UploadFolderService: { - instance: { - createFolders: vi.fn(), - }, - }, -})); - -vi.mock('../../../../src/services/network/upload/upload-file.service', () => ({ - UploadFileService: { - instance: { - uploadFilesConcurrently: vi.fn(), - }, - }, -})); - -vi.mock('../../../../src/utils/async.utils', () => ({ - AsyncUtils: { - sleep: vi.fn().mockResolvedValue(undefined), - }, -})); +import { UserFixture } from '../../../fixtures/auth.fixture'; +import { getNetworkOptionsMock } from '../../../fixtures/webdav.fixture'; describe('UploadFacade', () => { let sut: UploadFacade; - const mockNetworkFacade = {} as NetworkFacade; + const mockNetworkOptions = getNetworkOptionsMock(); - const mockLoginUserDetails = { - bridgeUser: 'test-bridge-user', - userId: 'test-user-id', - mnemonic: 'test-mnemonic', - bucket: 'test-bucket', - } as LoginUserDetails; + const mockLoginUserDetails: LoginUserDetails = UserFixture; const folderName = 'test-folder'; const folderMap = new Map([[folderName, 'folder-uuid-123']]); + beforeEach(() => { vi.clearAllMocks(); sut = UploadFacade.instance; - vi.mocked(CLIUtils.prepareNetwork).mockReturnValue(mockNetworkFacade); - vi.mocked(LocalFilesystemService.instance.scanLocalDirectory).mockResolvedValue({ + vi.spyOn(LocalFilesystemService.instance, 'scanLocalDirectory').mockResolvedValue({ folders: [createFileSystemNodeFixture({ type: 'folder', name: folderName, relativePath: folderName })], files: [ createFileSystemNodeFixture({ @@ -83,11 +36,14 @@ describe('UploadFacade', () => { totalItems: 2, totalBytes: 500, }); - vi.mocked(UploadFolderService.instance.createFolders).mockResolvedValue(folderMap); - vi.mocked(UploadFileService.instance.uploadFilesConcurrently).mockResolvedValue(500); - vi.mocked(CLIUtils.timer).mockReturnValue({ + vi.spyOn(UploadFolderService.instance, 'createFolders').mockResolvedValue(folderMap); + vi.spyOn(UploadFileService.instance, 'uploadFilesConcurrently').mockResolvedValue(500); + vi.spyOn(CLIUtils, 'prepareNetwork').mockResolvedValue(mockNetworkOptions); + vi.spyOn(CLIUtils, 'timer').mockReturnValue({ stop: vi.fn().mockReturnValue(1000), }); + vi.spyOn(AsyncUtils, 'sleep').mockResolvedValue(); + vi.spyOn(logger, 'info').mockImplementation(vi.fn()); }); describe('uploadFolder', () => { @@ -96,14 +52,14 @@ describe('UploadFacade', () => { const onProgress = vi.fn(); it('should throw an error if createFolders returns an empty map', async () => { - vi.mocked(LocalFilesystemService.instance.scanLocalDirectory).mockResolvedValue({ + vi.spyOn(LocalFilesystemService.instance, 'scanLocalDirectory').mockResolvedValue({ folders: [createFileSystemNodeFixture({ type: 'folder', name: 'test', relativePath: 'test' })], files: [], totalItems: 1, totalBytes: 0, }); - vi.mocked(UploadFolderService.instance.createFolders).mockResolvedValue(new Map()); + vi.spyOn(UploadFolderService.instance, 'createFolders').mockResolvedValue(new Map()); await expect( sut.uploadFolder({ @@ -140,7 +96,7 @@ describe('UploadFacade', () => { it('should report progress correctly during upload', async () => { const folderMap = new Map([[folderName, 'folder-uuid-123']]); - vi.mocked(UploadFolderService.instance.createFolders).mockImplementation( + vi.spyOn(UploadFolderService.instance, 'createFolders').mockImplementation( async ({ currentProgress, emitProgress }) => { currentProgress.itemsUploaded = 1; emitProgress(); @@ -148,7 +104,7 @@ describe('UploadFacade', () => { }, ); - vi.mocked(UploadFileService.instance.uploadFilesConcurrently).mockImplementation( + vi.spyOn(UploadFileService.instance, 'uploadFilesConcurrently').mockImplementation( async ({ currentProgress, emitProgress }) => { currentProgress.itemsUploaded = 2; currentProgress.bytesUploaded = 500; @@ -173,8 +129,8 @@ describe('UploadFacade', () => { it('should wait 500ms between folder creation and file upload to prevent backend indexing issues', async () => { vi.useFakeTimers(); - vi.mocked(UploadFolderService.instance.createFolders).mockResolvedValue(folderMap); - vi.mocked(UploadFileService.instance.uploadFilesConcurrently).mockResolvedValue(100); + vi.spyOn(UploadFolderService.instance, 'createFolders').mockResolvedValue(folderMap); + vi.spyOn(UploadFileService.instance, 'uploadFilesConcurrently').mockResolvedValue(100); const uploadPromise = sut.uploadFolder({ localPath, diff --git a/test/services/network/upload/upload-file.service.test.ts b/test/services/network/upload/upload-file.service.test.ts index 00fb002d..adda9f37 100644 --- a/test/services/network/upload/upload-file.service.test.ts +++ b/test/services/network/upload/upload-file.service.test.ts @@ -3,14 +3,9 @@ import { UploadFileService } from '../../../../src/services/network/upload/uploa import { NetworkFacade } from '../../../../src/services/network/network-facade.service'; import { DriveFileService } from '../../../../src/services/drive/drive-file.service'; import { logger } from '../../../../src/utils/logger.utils'; -import { isAlreadyExistsError } from '../../../../src/utils/errors.utils'; +import { ErrorUtils } from '../../../../src/utils/errors.utils'; import { stat } from 'fs/promises'; import { createReadStream } from 'fs'; -import { - createFileStreamWithBuffer, - isFileThumbnailable, - tryUploadThumbnail, -} from '../../../../src/utils/thumbnail.utils'; import { createFileSystemNodeFixture, createMockReadStream, @@ -18,6 +13,8 @@ import { createProgressFixtures, } from './upload.service.helpers'; import { newFileItem } from '../../../fixtures/drive.fixture'; +import { ThumbnailUtils } from '../../../../src/utils/thumbnail.utils'; +import { ThumbnailService } from '../../../../src/services/thumbnail.service'; vi.mock('fs', () => ({ createReadStream: vi.fn(), @@ -27,42 +24,6 @@ vi.mock('fs/promises', () => ({ stat: vi.fn(), })); -vi.mock('../../../../src/services/drive/drive-file.service', () => ({ - DriveFileService: { - instance: { - createFile: vi.fn(), - }, - }, -})); - -vi.mock('../../../../src/utils/thumbnail.utils', () => ({ - isFileThumbnailable: vi.fn(), - tryUploadThumbnail: vi.fn(), - createFileStreamWithBuffer: vi.fn(), -})); - -vi.mock('../../../../src/utils/stream.utils', () => ({ - StreamUtils: { - createFileStreamWithBuffer: vi.fn(), - }, -})); - -vi.mock('../../../../src/utils/logger.utils', () => ({ - logger: { - warn: vi.fn(), - info: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock('../../../../src/utils/errors.utils', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - isAlreadyExistsError: vi.fn(), - }; -}); - describe('UploadFileService', () => { let sut: UploadFileService; const mockFile = newFileItem(); @@ -77,16 +38,16 @@ describe('UploadFileService', () => { beforeEach(() => { vi.clearAllMocks(); sut = UploadFileService.instance; - vi.mocked(isAlreadyExistsError).mockReturnValue(false); vi.mocked(stat).mockResolvedValue(createMockStats(1024) as Awaited>); vi.mocked(createReadStream).mockReturnValue(createMockReadStream() as ReturnType); - vi.mocked(isFileThumbnailable).mockReturnValue(false); - vi.mocked(createFileStreamWithBuffer).mockReturnValue({ + vi.spyOn(ErrorUtils, 'isAlreadyExistsError').mockReturnValue(false); + vi.spyOn(ThumbnailUtils, 'isFileThumbnailable').mockReturnValue(false); + vi.spyOn(ThumbnailService.instance, 'tryUploadThumbnail').mockResolvedValue(undefined); + vi.spyOn(ThumbnailService.instance, 'createFileStreamWithBuffer').mockReturnValue({ fileStream: createMockReadStream() as ReturnType, bufferStream: undefined, }); - vi.mocked(tryUploadThumbnail).mockResolvedValue(undefined); - vi.mocked(DriveFileService.instance.createFile).mockResolvedValue(mockFile); + vi.spyOn(DriveFileService.instance, 'createFile').mockResolvedValue(mockFile); }); describe('uploadFilesConcurrently', () => { @@ -334,9 +295,11 @@ describe('UploadFileService', () => { it('should call tryUploadThumbnail when bufferStream is present', async () => { const mockBufferStream = { getBuffer: vi.fn() }; - vi.mocked(createFileStreamWithBuffer).mockReturnValue({ + vi.spyOn(ThumbnailService.instance, 'createFileStreamWithBuffer').mockReturnValue({ fileStream: createMockReadStream() as ReturnType, - bufferStream: mockBufferStream as unknown as ReturnType['bufferStream'], + bufferStream: mockBufferStream as unknown as ReturnType< + typeof ThumbnailService.instance.createFileStreamWithBuffer + >['bufferStream'], }); const file = createFileSystemNodeFixture({ @@ -353,17 +316,17 @@ describe('UploadFileService', () => { parentFolderUuid: destinationFolderUuid, }); - expect(tryUploadThumbnail).toHaveBeenCalledWith({ + expect(ThumbnailService.instance.tryUploadThumbnail).toHaveBeenCalledWith({ bufferStream: mockBufferStream, fileType: 'png', - userBucket: bucket, + bucket, fileUuid: mockFile.uuid, networkFacade: mockNetworkFacade, }); }); it('should return null when file already exists', async () => { - vi.mocked(isAlreadyExistsError).mockReturnValue(true); + vi.spyOn(ErrorUtils, 'isAlreadyExistsError').mockReturnValue(true); vi.mocked(mockNetworkFacade.uploadFile).mockImplementation((_stream, _size, _bucket, callback) => { callback(new Error('File already exists'), null); return { stop: vi.fn() } as unknown as ReturnType; diff --git a/test/services/network/upload/upload-folder.service.test.ts b/test/services/network/upload/upload-folder.service.test.ts index 6f12dfcf..b070c6c8 100644 --- a/test/services/network/upload/upload-folder.service.test.ts +++ b/test/services/network/upload/upload-folder.service.test.ts @@ -2,52 +2,28 @@ import { beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'; import { DriveFolderService } from '../../../../src/services/drive/drive-folder.service'; import { UploadFolderService } from '../../../../src/services/network/upload/upload-folder.service'; import { logger } from '../../../../src/utils/logger.utils'; -import { isAlreadyExistsError } from '../../../../src/utils/errors.utils'; +import { ErrorUtils } from '../../../../src/utils/errors.utils'; import { createFileSystemNodeFixture, createProgressFixtures } from './upload.service.helpers'; import { DELAYS_MS } from '../../../../src/services/network/upload/upload.types'; -vi.mock('../../../../src/services/drive/drive-folder.service', () => ({ - DriveFolderService: { - instance: { - createFolder: vi.fn(), - }, - }, -})); - -vi.mock('../../../../src/utils/logger.utils', () => ({ - logger: { - warn: vi.fn(), - info: vi.fn(), - error: vi.fn(), - }, -})); - -vi.mock('../../../../src/utils/errors.utils', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - isAlreadyExistsError: vi.fn(), - ErrorUtils: { - report: vi.fn(), - }, - }; -}); describe('UploadFolderService', () => { let sut: UploadFolderService; + beforeEach(() => { vi.clearAllMocks(); sut = UploadFolderService.instance; - vi.mocked(DriveFolderService.instance.createFolder).mockReturnValue([ + vi.spyOn(DriveFolderService.instance, 'createFolder').mockReturnValue([ Promise.resolve({ uuid: 'mock-folder-uuid' }), ] as unknown as ReturnType); - vi.mocked(isAlreadyExistsError).mockReturnValue(false); + vi.spyOn(ErrorUtils, 'isAlreadyExistsError').mockReturnValue(false); }); + describe('createFolders', () => { const destinationFolderUuid = 'dest-uuid'; it('should properly return a map of created folders where key is relativePath and value is uuid', async () => { const { currentProgress, emitProgress } = createProgressFixtures(); - vi.mocked(DriveFolderService.instance.createFolder) + vi.spyOn(DriveFolderService.instance, 'createFolder') .mockReturnValueOnce([Promise.resolve({ uuid: 'root-uuid' })] as unknown as ReturnType< typeof DriveFolderService.instance.createFolder >) @@ -122,7 +98,7 @@ describe('UploadFolderService', () => { const parentFolderUuid = 'parent-uuid'; it('should properly create a folder and return the created folder uuid', async () => { - vi.mocked(DriveFolderService.instance.createFolder).mockReturnValueOnce([ + vi.spyOn(DriveFolderService.instance, 'createFolder').mockReturnValueOnce([ Promise.resolve({ uuid: 'created-folder-uuid' }), ] as unknown as ReturnType); @@ -137,8 +113,8 @@ describe('UploadFolderService', () => { it('should properly return null if the folder already exists', async () => { const alreadyExistsError = new Error('Folder already exists'); - vi.mocked(isAlreadyExistsError).mockReturnValue(true); - vi.mocked(DriveFolderService.instance.createFolder).mockReturnValueOnce([ + vi.spyOn(ErrorUtils, 'isAlreadyExistsError').mockReturnValue(true); + vi.spyOn(DriveFolderService.instance, 'createFolder').mockReturnValueOnce([ Promise.reject(alreadyExistsError), ] as unknown as ReturnType); @@ -156,7 +132,7 @@ describe('UploadFolderService', () => { const rejection1 = Promise.reject(transientError).catch(() => {}); const rejection2 = Promise.reject(transientError).catch(() => {}); - vi.mocked(DriveFolderService.instance.createFolder) + vi.spyOn(DriveFolderService.instance, 'createFolder') .mockReturnValueOnce([rejection1] as unknown as ReturnType) .mockReturnValueOnce([rejection2] as unknown as ReturnType) .mockReturnValueOnce([Promise.resolve({ uuid: 'success-uuid' })] as unknown as ReturnType< @@ -184,7 +160,7 @@ describe('UploadFolderService', () => { process.off('unhandledRejection', unhandledRejectionListener); }); vi.useFakeTimers(); - vi.mocked(DriveFolderService.instance.createFolder).mockImplementation(() => { + vi.spyOn(DriveFolderService.instance, 'createFolder').mockImplementation(() => { return [Promise.reject(new Error('Persistent network error'))] as unknown as ReturnType< typeof DriveFolderService.instance.createFolder >; diff --git a/test/services/thumbnail.service.test.ts b/test/services/thumbnail.service.test.ts new file mode 100644 index 00000000..d3be973a --- /dev/null +++ b/test/services/thumbnail.service.test.ts @@ -0,0 +1,32 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { BufferStream } from '../../src/utils/stream.utils'; +import { ThumbnailService } from '../../src/services/thumbnail.service'; +import path from 'node:path'; +import { Readable } from 'node:stream'; + +describe('Thumbnail Service tests', () => { + const testFilePath = path.join(process.cwd(), 'test/fixtures/test-content.fixture.txt'); + + beforeEach(() => { + vi.restoreAllMocks(); + }); + + describe('createFileStreamWithBuffer', () => { + it('should create BufferStream and pipe stream when file type is thumbnailable', () => { + const result = ThumbnailService.instance.createFileStreamWithBuffer({ path: testFilePath, fileType: 'png' }); + + expect(result.bufferStream).toBeDefined(); + expect(result.bufferStream).toBeInstanceOf(BufferStream); + expect(result.fileStream).toBeDefined(); + expect(result.fileStream).toBeInstanceOf(Readable); + }); + + it('should not create BufferStream when file type is not thumbnailable', () => { + const result = ThumbnailService.instance.createFileStreamWithBuffer({ path: testFilePath, fileType: 'txt' }); + + expect(result.bufferStream).toBeUndefined(); + expect(result.fileStream).toBeDefined(); + expect(result.fileStream).toBeInstanceOf(Readable); + }); + }); +}); diff --git a/test/services/usage.service.test.ts b/test/services/usage.service.test.ts index f612d462..4a306fe9 100644 --- a/test/services/usage.service.test.ts +++ b/test/services/usage.service.test.ts @@ -20,7 +20,7 @@ describe('Usage Service', () => { const result = await UsageService.instance.fetchUsage(); - expect(result).to.be.deep.equal(driveSpaceUsage); + expect(result).to.be.deep.equal(driveSpaceUsage.total); }); it('When getting user space limit, it should return the total usage', async () => { diff --git a/test/utils/cli.utils.test.ts b/test/utils/cli.utils.test.ts index 5d787a17..ff344f62 100644 --- a/test/utils/cli.utils.test.ts +++ b/test/utils/cli.utils.test.ts @@ -8,6 +8,9 @@ import { SdkManager } from '../../src/services/sdk-manager.service'; import { ConfigService } from '../../src/services/config.service'; import { NetworkFacade } from '../../src/services/network/network-facade.service'; import { Environment } from '@internxt/inxt-js'; +import { UserFixture } from '../fixtures/auth.fixture'; +import { getNetworkOptionsMock } from '../fixtures/webdav.fixture'; +import { NetworkOptions } from '../../src/types/network.types'; vi.mock('ux', () => { return { @@ -20,23 +23,6 @@ vi.mock('ux', () => { }; }); -vi.mock('../../src/services/sdk-manager.service', () => ({ - SdkManager: { - instance: { - getNetwork: vi.fn(), - }, - getAppDetails: vi.fn(), - }, -})); - -vi.mock('../../src/services/config.service', () => ({ - ConfigService: { - instance: { - get: vi.fn(), - }, - }, -})); - vi.mock('@internxt/inxt-js', () => ({ Environment: vi.fn(), })); @@ -51,15 +37,12 @@ describe('CliUtils', () => { (str: Uint8Array | string, encoding?: BufferEncoding, cb?: (err?: Error) => void): boolean; }>; let stdoutClear: MockInstance<(dir: Direction, callback?: () => void) => boolean>; - let reporter: (message: string) => void; + const reporter: (message: string) => void = vi.fn(); const BRIDGE_URL = 'https://test.com'; - let mockNetworkFacade: NetworkFacade; - const mockLoginUserDetails = { - bridgeUser: 'test-bridge-user', - userId: 'test-user-id', - mnemonic: 'test-mnemonic', - } as LoginUserDetails; + const mockNetworkFacade: NetworkFacade = {} as NetworkFacade; + const mockNetworkOptions: NetworkOptions = getNetworkOptionsMock(); + const mockLoginUserDetails: LoginUserDetails = UserFixture; const mockNetworkModule = {} as ReturnType; const mockAppDetails = {} as ReturnType; @@ -71,18 +54,15 @@ describe('CliUtils', () => { stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); stdoutClear = vi.spyOn(process.stdout, 'clearLine').mockImplementation(() => true); - reporter = vi.fn(); - - mockNetworkFacade = {} as NetworkFacade; vi.mocked(NetworkFacade).mockImplementation(function (this: NetworkFacade) { return mockNetworkFacade; }); vi.mocked(Environment).mockImplementation(function (this: Environment) { return {} as Environment; }); - vi.mocked(SdkManager.instance.getNetwork).mockReturnValue(mockNetworkModule); - vi.mocked(SdkManager.getAppDetails).mockReturnValue(mockAppDetails); - vi.mocked(ConfigService.instance.get).mockReturnValue(BRIDGE_URL); + vi.spyOn(SdkManager.instance, 'getNetwork').mockReturnValue(mockNetworkModule); + vi.spyOn(SdkManager, 'getAppDetails').mockReturnValue(mockAppDetails); + vi.spyOn(ConfigService.instance, 'get').mockReturnValue(BRIDGE_URL); }); afterEach(() => { @@ -199,10 +179,18 @@ describe('CliUtils', () => { }); describe('prepareNetwork', () => { - it('should properly create a networkFacade instance and return it', () => { - const result = CLIUtils.prepareNetwork({ loginUserDetails: mockLoginUserDetails }); + it('should properly create a networkFacade instance and return it', async () => { + vi.spyOn(CLIUtils, 'getNetworkCreds').mockResolvedValue({ + bucket: mockLoginUserDetails.bucket, + credentials: { + user: mockLoginUserDetails.bridgeUser, + pass: mockLoginUserDetails.userId, + }, + mnemonic: mockLoginUserDetails.mnemonic, + }); + const result = await CLIUtils.prepareNetwork(mockLoginUserDetails); - expect(result).toBe(mockNetworkFacade); + expect(result).toEqual(mockNetworkOptions); expect(SdkManager.instance.getNetwork).toHaveBeenCalledWith({ user: mockLoginUserDetails.bridgeUser, pass: mockLoginUserDetails.userId, @@ -216,17 +204,6 @@ describe('CliUtils', () => { }); expect(NetworkFacade).toHaveBeenCalledTimes(1); }); - - it('should properly output to the terminal the prepare network step', () => { - const jsonFlag = true; - const doingSpy = vi.spyOn(CLIUtils, 'doing'); - const doneSpy = vi.spyOn(CLIUtils, 'done'); - - CLIUtils.prepareNetwork({ jsonFlag, loginUserDetails: mockLoginUserDetails }); - - expect(doingSpy).toHaveBeenCalledWith('Preparing Network', jsonFlag); - expect(doneSpy).toHaveBeenCalledWith(jsonFlag); - }); }); describe('timer', () => { diff --git a/test/utils/errors.utils.test.ts b/test/utils/errors.utils.test.ts index 5a5728a5..2fa5140c 100644 --- a/test/utils/errors.utils.test.ts +++ b/test/utils/errors.utils.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ErrorUtils, isAlreadyExistsError, isFileNotFoundError } from '../../src/utils/errors.utils'; +import { ErrorUtils } from '../../src/utils/errors.utils'; import { logger } from '../../src/utils/logger.utils'; describe('Errors Utils', () => { @@ -34,20 +34,20 @@ describe('Errors Utils', () => { it('should properly detect an error object that has an already exists as message', () => { const error = new Error('File already exists'); - expect(isAlreadyExistsError(error)).toBe(true); + expect(ErrorUtils.isAlreadyExistsError(error)).toBe(true); }); it('should properly detect an error object that has 409 as status', () => { const error = { status: 409, message: 'Conflict' }; - expect(isAlreadyExistsError(error)).toBe(true); + expect(ErrorUtils.isAlreadyExistsError(error)).toBe(true); }); it('should return false if the passed error is not an object', () => { - expect(isAlreadyExistsError('string error')).toBe(false); - expect(isAlreadyExistsError(123)).toBe(false); - expect(isAlreadyExistsError(null)).toBe(false); - expect(isAlreadyExistsError(undefined)).toBe(false); + expect(ErrorUtils.isAlreadyExistsError('string error')).toBe(false); + expect(ErrorUtils.isAlreadyExistsError(123)).toBe(false); + expect(ErrorUtils.isAlreadyExistsError(null)).toBe(false); + expect(ErrorUtils.isAlreadyExistsError(undefined)).toBe(false); }); }); @@ -56,7 +56,7 @@ describe('Errors Utils', () => { const error = new Error('File not found'); Object.assign(error, { code: 'ENOENT' }); - expect(isFileNotFoundError(error)).toBe(true); + expect(ErrorUtils.isFileNotFoundError(error)).toBe(true); }); it('should return true when error is a real ENOENT error from fs operations', () => { @@ -67,28 +67,28 @@ describe('Errors Utils', () => { path: '/nonexistent/file.txt', }); - expect(isFileNotFoundError(error)).toBe(true); + expect(ErrorUtils.isFileNotFoundError(error)).toBe(true); }); it('should return false when error has a different error code', () => { const error = new Error('Permission denied'); Object.assign(error, { code: 'EACCES' }); - expect(isFileNotFoundError(error)).toBe(false); + expect(ErrorUtils.isFileNotFoundError(error)).toBe(false); }); it('should return false when error has no code property', () => { const error = new Error('Some error'); - expect(isFileNotFoundError(error)).toBe(false); + expect(ErrorUtils.isFileNotFoundError(error)).toBe(false); }); it('should return false when error is not an Error object', () => { - expect(isFileNotFoundError({ code: 'ENOENT' })).toBe(false); - expect(isFileNotFoundError('ENOENT')).toBe(false); - expect(isFileNotFoundError(null)).toBe(false); - expect(isFileNotFoundError(undefined)).toBe(false); - expect(isFileNotFoundError(123)).toBe(false); + expect(ErrorUtils.isFileNotFoundError({ code: 'ENOENT' })).toBe(false); + expect(ErrorUtils.isFileNotFoundError('ENOENT')).toBe(false); + expect(ErrorUtils.isFileNotFoundError(null)).toBe(false); + expect(ErrorUtils.isFileNotFoundError(undefined)).toBe(false); + expect(ErrorUtils.isFileNotFoundError(123)).toBe(false); }); }); }); diff --git a/test/utils/network.utils.test.ts b/test/utils/network.utils.test.ts index d43b86c8..5e0d8da4 100644 --- a/test/utils/network.utils.test.ts +++ b/test/utils/network.utils.test.ts @@ -65,7 +65,7 @@ describe('Network utils', () => { mockStat.mockImplementation(async () => { return Promise.reject(); }); - const selfsignedSpy = vi.spyOn(selfsigned, 'generate').mockImplementation(() => sslSelfSigned); + const selfsignedSpy = vi.spyOn(selfsigned, 'generate').mockResolvedValue(sslSelfSigned); const result = await NetworkUtils.getWebdavSSLCerts(webdavConfig); @@ -155,7 +155,7 @@ describe('Network utils', () => { validTo: past.toDateString(), })); - const selfsignedSpy = vi.spyOn(selfsigned, 'generate').mockImplementation(() => sslSelfSigned); + const selfsignedSpy = vi.spyOn(selfsigned, 'generate').mockResolvedValue(sslSelfSigned); const result = await NetworkUtils.getWebdavSSLCerts(webdavConfig); diff --git a/test/utils/thumbnail.utils.test.ts b/test/utils/thumbnail.utils.test.ts index 156ee2ac..d8a21311 100644 --- a/test/utils/thumbnail.utils.test.ts +++ b/test/utils/thumbnail.utils.test.ts @@ -1,173 +1,144 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createFileStreamWithBuffer, - isFileThumbnailable, - isImageThumbnailable, - isPDFThumbnailable, -} from '../../src/utils/thumbnail.utils'; -import { BufferStream } from '../../src/utils/stream.utils'; -import path from 'node:path'; -import { Readable } from 'node:stream'; +import { ThumbnailUtils } from '../../src/utils/thumbnail.utils'; describe('Thumbnail Utils tests', () => { - const testFilePath = path.join(process.cwd(), 'test/fixtures/test-content.fixture.txt'); - beforeEach(() => { vi.restoreAllMocks(); }); - describe('createFileStreamWithBuffer', () => { - it('should create BufferStream and pipe stream when file type is thumbnailable', () => { - const result = createFileStreamWithBuffer({ path: testFilePath, fileType: 'png' }); - - expect(result.bufferStream).toBeDefined(); - expect(result.bufferStream).toBeInstanceOf(BufferStream); - expect(result.fileStream).toBeDefined(); - expect(result.fileStream).toBeInstanceOf(Readable); - }); - - it('should not create BufferStream when file type is not thumbnailable', () => { - const result = createFileStreamWithBuffer({ path: testFilePath, fileType: 'txt' }); - - expect(result.bufferStream).toBeUndefined(); - expect(result.fileStream).toBeDefined(); - expect(result.fileStream).toBeInstanceOf(Readable); - }); - }); - describe('isFileThumbnailable', () => { it('should return true for valid image extensions', () => { - expect(isFileThumbnailable('jpg')).toBe(true); - expect(isFileThumbnailable('jpeg')).toBe(true); - expect(isFileThumbnailable('png')).toBe(true); - expect(isFileThumbnailable('webp')).toBe(true); - expect(isFileThumbnailable('gif')).toBe(true); - expect(isFileThumbnailable('tif')).toBe(true); - expect(isFileThumbnailable('tiff')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('jpg')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('jpeg')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('png')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('webp')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('gif')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('tif')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('tiff')).toBe(true); }); it('should return true regardless of case', () => { - expect(isFileThumbnailable('JPG')).toBe(true); - expect(isFileThumbnailable('PNG')).toBe(true); - expect(isFileThumbnailable('Webp')).toBe(true); - expect(isFileThumbnailable('GIF')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('JPG')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('PNG')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('Webp')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('GIF')).toBe(true); }); it('should handle whitespace correctly', () => { - expect(isFileThumbnailable(' jpg ')).toBe(true); - expect(isFileThumbnailable(' png ')).toBe(true); - expect(isFileThumbnailable('\tgif\t')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable(' jpg ')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable(' png ')).toBe(true); + expect(ThumbnailUtils.isFileThumbnailable('\tgif\t')).toBe(true); }); it('should return false for non-thumbnailable extensions', () => { - expect(isFileThumbnailable('pdf')).toBe(false); - expect(isFileThumbnailable('doc')).toBe(false); - expect(isFileThumbnailable('txt')).toBe(false); - expect(isFileThumbnailable('mp4')).toBe(false); - expect(isFileThumbnailable('bmp')).toBe(false); - expect(isFileThumbnailable('raw')).toBe(false); - expect(isFileThumbnailable('heic')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('pdf')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('doc')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('txt')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('mp4')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('bmp')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('raw')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('heic')).toBe(false); }); it('should return false for empty strings', () => { - expect(isFileThumbnailable('')).toBe(false); - expect(isFileThumbnailable(' ')).toBe(false); - expect(isFileThumbnailable('\t\n')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable(' ')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('\t\n')).toBe(false); }); it('should return false for invalid input', () => { - expect(isFileThumbnailable('unknown')).toBe(false); - expect(isFileThumbnailable('jpgg')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('unknown')).toBe(false); + expect(ThumbnailUtils.isFileThumbnailable('jpgg')).toBe(false); }); }); describe('isPDFThumbnailable', () => { it('should return true for pdf extension', () => { - expect(isPDFThumbnailable('pdf')).toBe(true); + expect(ThumbnailUtils.isPDFThumbnailable('pdf')).toBe(true); }); it('should return true regardless of case', () => { - expect(isPDFThumbnailable('PDF')).toBe(true); - expect(isPDFThumbnailable('Pdf')).toBe(true); - expect(isPDFThumbnailable('pDf')).toBe(true); + expect(ThumbnailUtils.isPDFThumbnailable('PDF')).toBe(true); + expect(ThumbnailUtils.isPDFThumbnailable('Pdf')).toBe(true); + expect(ThumbnailUtils.isPDFThumbnailable('pDf')).toBe(true); }); it('should handle whitespace correctly', () => { - expect(isPDFThumbnailable(' pdf ')).toBe(true); - expect(isPDFThumbnailable(' PDF ')).toBe(true); - expect(isPDFThumbnailable('\tpdf\n')).toBe(true); + expect(ThumbnailUtils.isPDFThumbnailable(' pdf ')).toBe(true); + expect(ThumbnailUtils.isPDFThumbnailable(' PDF ')).toBe(true); + expect(ThumbnailUtils.isPDFThumbnailable('\tpdf\n')).toBe(true); }); it('should return false for non-pdf extensions', () => { - expect(isPDFThumbnailable('jpg')).toBe(false); - expect(isPDFThumbnailable('png')).toBe(false); - expect(isPDFThumbnailable('doc')).toBe(false); - expect(isPDFThumbnailable('docx')).toBe(false); - expect(isPDFThumbnailable('txt')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('jpg')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('png')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('doc')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('docx')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('txt')).toBe(false); }); it('should return false for empty strings', () => { - expect(isPDFThumbnailable('')).toBe(false); - expect(isPDFThumbnailable(' ')).toBe(false); - expect(isPDFThumbnailable('\t\n')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable(' ')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('\t\n')).toBe(false); }); it('should return false for invalid input', () => { - expect(isPDFThumbnailable('pdff')).toBe(false); - expect(isPDFThumbnailable('pd')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('pdff')).toBe(false); + expect(ThumbnailUtils.isPDFThumbnailable('pd')).toBe(false); }); }); describe('isImageThumbnailable', () => { it('should return true for all thumbnailable image extensions', () => { - expect(isImageThumbnailable('jpg')).toBe(true); - expect(isImageThumbnailable('jpeg')).toBe(true); - expect(isImageThumbnailable('png')).toBe(true); - expect(isImageThumbnailable('webp')).toBe(true); - expect(isImageThumbnailable('gif')).toBe(true); - expect(isImageThumbnailable('tif')).toBe(true); - expect(isImageThumbnailable('tiff')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('jpg')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('jpeg')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('png')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('webp')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('gif')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('tif')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('tiff')).toBe(true); }); it('should return true regardless of case', () => { - expect(isImageThumbnailable('JPG')).toBe(true); - expect(isImageThumbnailable('PNG')).toBe(true); - expect(isImageThumbnailable('GIF')).toBe(true); - expect(isImageThumbnailable('Jpeg')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('JPG')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('PNG')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('GIF')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('Jpeg')).toBe(true); }); it('should handle whitespace correctly', () => { - expect(isImageThumbnailable(' jpg ')).toBe(true); - expect(isImageThumbnailable(' png ')).toBe(true); - expect(isImageThumbnailable('\twebp\n')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable(' jpg ')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable(' png ')).toBe(true); + expect(ThumbnailUtils.isImageThumbnailable('\twebp\n')).toBe(true); }); it('should return false for non-thumbnailable image formats', () => { - expect(isImageThumbnailable('bmp')).toBe(false); - expect(isImageThumbnailable('heic')).toBe(false); - expect(isImageThumbnailable('raw')).toBe(false); - expect(isImageThumbnailable('cr2')).toBe(false); - expect(isImageThumbnailable('nef')).toBe(false); - expect(isImageThumbnailable('eps')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('bmp')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('heic')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('raw')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('cr2')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('nef')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('eps')).toBe(false); }); it('should return false for non-image extensions', () => { - expect(isImageThumbnailable('pdf')).toBe(false); - expect(isImageThumbnailable('doc')).toBe(false); - expect(isImageThumbnailable('txt')).toBe(false); - expect(isImageThumbnailable('mp4')).toBe(false); - expect(isImageThumbnailable('mp3')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('pdf')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('doc')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('txt')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('mp4')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('mp3')).toBe(false); }); it('should return false for empty strings', () => { - expect(isImageThumbnailable('')).toBe(false); - expect(isImageThumbnailable(' ')).toBe(false); - expect(isImageThumbnailable('\t\n')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable(' ')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('\t\n')).toBe(false); }); it('should return false for invalid input', () => { - expect(isImageThumbnailable('jpgg')).toBe(false); - expect(isImageThumbnailable('unknown')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('jpgg')).toBe(false); + expect(ThumbnailUtils.isImageThumbnailable('unknown')).toBe(false); }); }); }); diff --git a/test/utils/webdav.utils.test.ts b/test/utils/webdav.utils.test.ts index 94ab51a1..66ae31ec 100644 --- a/test/utils/webdav.utils.test.ts +++ b/test/utils/webdav.utils.test.ts @@ -104,11 +104,7 @@ describe('Webdav utils', () => { .mockResolvedValue(expectedFolder); const findFileStub = vi.spyOn(DriveFileService.instance, 'getFileMetadataByPath').mockRejectedValue(new Error()); - const driveFolderItem = await WebDavUtils.getDriveItemFromResource({ - resource: requestFolderFixture, - driveFolderService: DriveFolderService.instance, - driveFileService: DriveFileService.instance, - }); + const driveFolderItem = await WebDavUtils.getDriveItemFromResource(requestFolderFixture); expect(driveFolderItem).to.be.deep.equal(expectedFolder); expect(findFolderStub).toHaveBeenCalledOnce(); expect(findFileStub).not.toHaveBeenCalled(); @@ -120,11 +116,7 @@ describe('Webdav utils', () => { .mockRejectedValue(new AppError('Folder not found', 404)); const findFileStub = vi.spyOn(DriveFileService.instance, 'getFileMetadataByPath').mockRejectedValue(new Error()); - const item = await WebDavUtils.getDriveItemFromResource({ - resource: requestFolderFixture, - driveFolderService: DriveFolderService.instance, - driveFileService: DriveFileService.instance, - }); + const item = await WebDavUtils.getDriveItemFromResource(requestFolderFixture); expect(findFolderStub).toHaveBeenCalledOnce(); expect(findFileStub).not.toHaveBeenCalled(); expect(item).toBeUndefined(); @@ -137,11 +129,7 @@ describe('Webdav utils', () => { .spyOn(DriveFolderService.instance, 'getFolderMetadataByPath') .mockRejectedValue(new Error()); - const driveFileItem = await WebDavUtils.getDriveItemFromResource({ - resource: requestFileFixture, - driveFolderService: DriveFolderService.instance, - driveFileService: DriveFileService.instance, - }); + const driveFileItem = await WebDavUtils.getDriveItemFromResource(requestFileFixture); expect(driveFileItem).to.be.deep.equal(expectedFile); expect(findFileStub).toHaveBeenCalledOnce(); expect(findFolderStub).not.toHaveBeenCalled(); diff --git a/test/webdav/handlers/DELETE.handler.test.ts b/test/webdav/handlers/DELETE.handler.test.ts index d45198a1..7b4d207d 100644 --- a/test/webdav/handlers/DELETE.handler.test.ts +++ b/test/webdav/handlers/DELETE.handler.test.ts @@ -9,23 +9,21 @@ import { } from '../../fixtures/webdav.fixture'; import { TrashService } from '../../../src/services/drive/trash.service'; import { NotFoundError } from '../../../src/utils/errors.utils'; -import { DriveFileService } from '../../../src/services/drive/drive-file.service'; -import { DriveFolderService } from '../../../src/services/drive/drive-folder.service'; import { WebDavUtils } from '../../../src/utils/webdav.utils'; import { newFileItem, newFolderItem } from '../../fixtures/drive.fixture'; import { WebDavRequestedResource } from '../../../src/types/webdav.types'; +import { AuthService } from '../../../src/services/auth.service'; +import { UserCredentialsFixture } from '../../fixtures/login.fixture'; describe('DELETE request handler', () => { beforeEach(() => { vi.restoreAllMocks(); + + vi.spyOn(AuthService.instance, 'getAuthDetails').mockResolvedValue(UserCredentialsFixture); }); it('When the item does not exist, it should reply with a 404 error', async () => { - const requestHandler = new DELETERequestHandler({ - trashService: TrashService.instance, - driveFileService: DriveFileService.instance, - driveFolderService: DriveFolderService.instance, - }); + const requestHandler = new DELETERequestHandler(); const requestedFileResource: WebDavRequestedResource = getRequestedFileResource(); @@ -56,11 +54,7 @@ describe('DELETE request handler', () => { it('When the file exists, then it should reply with a 204 response', async () => { const trashService = TrashService.instance; - const requestHandler = new DELETERequestHandler({ - trashService, - driveFileService: DriveFileService.instance, - driveFolderService: DriveFolderService.instance, - }); + const requestHandler = new DELETERequestHandler(); const requestedFileResource: WebDavRequestedResource = getRequestedFileResource(); const request = createWebDavRequestFixture({ method: 'DELETE', @@ -89,11 +83,7 @@ describe('DELETE request handler', () => { it('When folder exists, then it should reply with a 204 response', async () => { const trashService = TrashService.instance; - const requestHandler = new DELETERequestHandler({ - trashService, - driveFileService: DriveFileService.instance, - driveFolderService: DriveFolderService.instance, - }); + const requestHandler = new DELETERequestHandler(); const requestedFolderResource: WebDavRequestedResource = getRequestedFolderResource(); const request = createWebDavRequestFixture({ diff --git a/test/webdav/handlers/GET.handler.test.ts b/test/webdav/handlers/GET.handler.test.ts index cd7d9288..8f9a1ec8 100644 --- a/test/webdav/handlers/GET.handler.test.ts +++ b/test/webdav/handlers/GET.handler.test.ts @@ -3,15 +3,14 @@ import { fail } from 'node:assert'; import { createWebDavRequestFixture, createWebDavResponseFixture, + getNetworkFacadeMock, + getNetworkOptionsMock, getRequestedFileResource, } from '../../fixtures/webdav.fixture'; import { GETRequestHandler } from '../../../src/webdav/handlers/GET.handler'; 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 { AuthService } from '../../../src/services/auth.service'; import { NotFoundError } from '../../../src/utils/errors.utils'; -import { SdkManager } from '../../../src/services/sdk-manager.service'; import { NetworkFacade } from '../../../src/services/network/network-facade.service'; import { WebDavUtils } from '../../../src/utils/webdav.utils'; import { WebDavRequestedResource } from '../../../src/types/webdav.types'; @@ -20,46 +19,20 @@ 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'; +import { CLIUtils } from '../../../src/utils/cli.utils'; +import { NetworkOptions } from '../../../src/types/network.types'; describe('GET request handler', () => { - let networkFacade: NetworkFacade; let sut: GETRequestHandler; - const getNetworkMock = () => { - return SdkManager.instance.getNetwork({ - user: 'user', - pass: 'pass', - }); - }; - - const getEnvironmentMock = () => { - return new Environment({ - bridgeUser: 'user', - bridgePass: 'pass', - bridgeUrl: ConfigService.instance.get('NETWORK_URL'), - encryptionKey: UserFixture.mnemonic, - appDetails: SdkManager.getAppDetails(), - }); - }; + const networkFacade: NetworkFacade = getNetworkFacadeMock(); + const networkOptions: NetworkOptions = getNetworkOptionsMock({ networkFacade }); beforeEach(() => { - networkFacade = new NetworkFacade( - getNetworkMock(), - getEnvironmentMock(), - DownloadService.instance, - CryptoService.instance, - ); - sut = new GETRequestHandler({ - driveFileService: DriveFileService.instance, - downloadService: DownloadService.instance, - authService: AuthService.instance, - cryptoService: CryptoService.instance, - networkFacade, - }); - vi.restoreAllMocks(); + + vi.spyOn(CLIUtils, 'prepareNetwork').mockResolvedValue(networkOptions); + + sut = new GETRequestHandler(); }); it('should throw a NotFoundError when the Drive file is not found', async () => { @@ -127,7 +100,7 @@ describe('GET request handler', () => { expect(getFileMetadataStub).toHaveBeenCalledOnce(); expect(authDetailsStub).toHaveBeenCalledOnce(); expect(downloadStreamStub).toHaveBeenCalledWith( - mockFile.bucket, + networkOptions.bucket, mockAuthDetails.user.mnemonic, mockFile.fileId, mockFile.size, @@ -184,7 +157,7 @@ describe('GET request handler', () => { expect(getFileMetadataStub).toHaveBeenCalledOnce(); expect(authDetailsStub).toHaveBeenCalledOnce(); expect(downloadStreamStub).toHaveBeenCalledWith( - mockFile.bucket, + networkOptions.bucket, mockAuthDetails.user.mnemonic, mockFile.fileId, mockSize - rangeStart, diff --git a/test/webdav/handlers/HEAD.handler.test.ts b/test/webdav/handlers/HEAD.handler.test.ts index 747c9a7c..471d9af8 100644 --- a/test/webdav/handlers/HEAD.handler.test.ts +++ b/test/webdav/handlers/HEAD.handler.test.ts @@ -15,9 +15,7 @@ import { randomInt } from 'crypto'; describe('HEAD request handler', () => { let sut: HEADRequestHandler; beforeEach(() => { - sut = new HEADRequestHandler({ - driveFileService: DriveFileService.instance, - }); + sut = new HEADRequestHandler(); vi.restoreAllMocks(); }); diff --git a/test/webdav/handlers/MKCOL.handler.test.ts b/test/webdav/handlers/MKCOL.handler.test.ts index 60196d24..cf5e1613 100644 --- a/test/webdav/handlers/MKCOL.handler.test.ts +++ b/test/webdav/handlers/MKCOL.handler.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { MKCOLRequestHandler } from '../../../src/webdav/handlers/MKCOL.handler'; -import { DriveFolderService } from '../../../src/services/drive/drive-folder.service'; import { createWebDavRequestFixture, createWebDavResponseFixture, @@ -10,23 +9,18 @@ import { UserSettingsFixture } from '../../fixtures/auth.fixture'; import { newFolderItem } from '../../fixtures/drive.fixture'; import { WebDavRequestedResource } from '../../../src/types/webdav.types'; import { WebDavUtils } from '../../../src/utils/webdav.utils'; +import { AuthService } from '../../../src/services/auth.service'; +import { UserCredentialsFixture } from '../../fixtures/login.fixture'; import { WebDavFolderService } from '../../../src/webdav/services/webdav-folder.service'; -import { ConfigService } from '../../../src/services/config.service'; describe('MKCOL request handler', () => { - let webDavFolderService: WebDavFolderService; let sut: MKCOLRequestHandler; beforeEach(() => { - webDavFolderService = new WebDavFolderService({ - driveFolderService: DriveFolderService.instance, - configService: ConfigService.instance, - }); - sut = new MKCOLRequestHandler({ - driveFolderService: DriveFolderService.instance, - webDavFolderService, - }); vi.restoreAllMocks(); + sut = new MKCOLRequestHandler(); + + vi.spyOn(AuthService.instance, 'getAuthDetails').mockResolvedValue(UserCredentialsFixture); }); it('When a WebDav client sends a MKCOL request, it should reply with a 201 if success', async () => { @@ -49,23 +43,20 @@ describe('MKCOL request handler', () => { .spyOn(WebDavUtils, 'getRequestedResource') .mockResolvedValue(requestedFolderResource); const getDriveFolderItemFromPathStub = vi - .spyOn(webDavFolderService, 'getDriveFolderItemFromPath') + .spyOn(WebDavFolderService.instance, 'getDriveFolderItemFromPath') .mockResolvedValue(parentFolder); const getDriveFolderFromResourceStub = vi .spyOn(WebDavUtils, 'getDriveFolderFromResource') .mockResolvedValue(undefined); const createFolderStub = vi - .spyOn(webDavFolderService, 'createFolder') + .spyOn(WebDavFolderService.instance, 'createFolder') .mockResolvedValue(newFolderItem({ name: 'FolderA', uuid: 'new-folder-uuid' })); await sut.handle(request, response); expect(response.status).toHaveBeenCalledWith(201); expect(getRequestedResourceStub).toHaveBeenCalledWith(request.url); expect(getDriveFolderItemFromPathStub).toHaveBeenCalledWith(requestedFolderResource.parentPath); - expect(getDriveFolderFromResourceStub).toHaveBeenCalledWith({ - url: requestedFolderResource.url, - driveFolderService: DriveFolderService.instance, - }); + expect(getDriveFolderFromResourceStub).toHaveBeenCalledWith(requestedFolderResource.url); expect(createFolderStub).toHaveBeenCalledWith({ folderName: requestedFolderResource.path.base, parentFolderUuid: parentFolder.uuid, diff --git a/test/webdav/handlers/PROPFIND.handler.test.ts b/test/webdav/handlers/PROPFIND.handler.test.ts index 69d60eb4..6be0ce82 100644 --- a/test/webdav/handlers/PROPFIND.handler.test.ts +++ b/test/webdav/handlers/PROPFIND.handler.test.ts @@ -10,7 +10,6 @@ import { getRequestedFolderResource, } from '../../fixtures/webdav.fixture'; import { FormatUtils } from '../../../src/utils/format.utils'; -import { DriveFileService } from '../../../src/services/drive/drive-file.service'; import { WebDavRequestedResource } from '../../../src/types/webdav.types'; import { WebDavUtils } from '../../../src/utils/webdav.utils'; import mime from 'mime-types'; @@ -30,11 +29,9 @@ const randomUUIDStub = vi.mocked(randomUUID); describe('PROPFIND request handler', () => { let sut: PROPFINDRequestHandler; + beforeEach(() => { - sut = new PROPFINDRequestHandler({ - driveFileService: DriveFileService.instance, - driveFolderService: DriveFolderService.instance, - }); + sut = new PROPFINDRequestHandler(); vi.restoreAllMocks(); }); @@ -55,12 +52,9 @@ describe('PROPFIND request handler', () => { }); const folderFixture = newFolderItem({ - id: parseInt(UserSettingsFixture.rootFolderId), + id: Number.parseInt(UserSettingsFixture.rootFolderId), }); - const drive = crypto.randomInt(2000000000); - const backups = crypto.randomInt(2000000000); - const total = drive + backups; - const usageFixture = { _id: UserSettingsFixture.email, total, drive, backups }; + const usageFixture = crypto.randomInt(2000000000); const spaceLimitFixture = crypto.randomInt(2000000000); const getRequestedResourceStub = vi @@ -79,7 +73,7 @@ describe('PROPFIND request handler', () => { expect(response.status).toHaveBeenCalledWith(207); expect(response.send).toHaveBeenCalledWith( // eslint-disable-next-line max-len - `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKapplication/octet-stream${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture.total}${usageFixture.total}`, + `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKapplication/octet-stream${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture}${usageFixture}`, ); expect(getRequestedResourceStub).toHaveBeenCalledOnce(); expect(getFolderMetadataStub).toHaveBeenCalledOnce(); @@ -104,17 +98,14 @@ describe('PROPFIND request handler', () => { }); const folderFixture = newFolderItem({ - id: parseInt(UserSettingsFixture.rootFolderId), + id: Number.parseInt(UserSettingsFixture.rootFolderId), }); const paginatedFolder1 = newPaginatedFolder({ plainName: 'folder_1', updatedAt: new Date('2024-03-04T15:11:01.000Z').toString(), uuid: 'FOLDER_UUID_1', }); - const drive = crypto.randomInt(2000000000); - const backups = crypto.randomInt(2000000000); - const total = drive + backups; - const usageFixture = { _id: UserSettingsFixture.email, total, drive, backups }; + const usageFixture = crypto.randomInt(2000000000); const spaceLimitFixture = crypto.randomInt(2000000000); const getRequestedResourceStub = vi @@ -134,7 +125,7 @@ describe('PROPFIND request handler', () => { expect(response.status).toHaveBeenCalledWith(207); expect(response.send).toHaveBeenCalledWith( // eslint-disable-next-line max-len - `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKapplication/octet-stream${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture.total}${usageFixture.total}${XMLUtils.encodeWebDavUri(`/${paginatedFolder1.plainName}/`)}HTTP/1.1 200 OK${paginatedFolder1.plainName}${FormatUtils.formatDateForWebDav(paginatedFolder1.updatedAt)}0`, + `${XMLUtils.encodeWebDavUri('/')}HTTP/1.1 200 OKapplication/octet-stream${FormatUtils.formatDateForWebDav(folderFixture.updatedAt)}F00000030${spaceLimitFixture - usageFixture}${usageFixture}${XMLUtils.encodeWebDavUri(`/${paginatedFolder1.plainName}/`)}HTTP/1.1 200 OK${paginatedFolder1.plainName}${FormatUtils.formatDateForWebDav(paginatedFolder1.updatedAt)}0`, ); expect(getRequestedResourceStub).toHaveBeenCalledOnce(); expect(getAndSearchItemFromResourceStub).toHaveBeenCalledOnce(); diff --git a/test/webdav/handlers/PUT.handler.test.ts b/test/webdav/handlers/PUT.handler.test.ts index 1643924e..160e1e2b 100644 --- a/test/webdav/handlers/PUT.handler.test.ts +++ b/test/webdav/handlers/PUT.handler.test.ts @@ -2,67 +2,33 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createWebDavRequestFixture, createWebDavResponseFixture, + getNetworkFacadeMock, + getNetworkOptionsMock, getRequestedFileResource, getRequestedFolderResource, } from '../../fixtures/webdav.fixture'; import { DriveFileService } from '../../../src/services/drive/drive-file.service'; -import { DriveFolderService } from '../../../src/services/drive/drive-folder.service'; -import { CryptoService } from '../../../src/services/crypto.service'; -import { DownloadService } from '../../../src/services/network/download.service'; import { AuthService } from '../../../src/services/auth.service'; -import { SdkManager } from '../../../src/services/sdk-manager.service'; import { NetworkFacade } from '../../../src/services/network/network-facade.service'; import { PUTRequestHandler } from '../../../src/webdav/handlers/PUT.handler'; -import { WebDavFolderService } from '../../../src/webdav/services/webdav-folder.service'; import { TrashService } from '../../../src/services/drive/trash.service'; 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'; +import { CLIUtils } from '../../../src/utils/cli.utils'; describe('PUT request handler', () => { let networkFacade: NetworkFacade; let sut: PUTRequestHandler; - const getNetworkMock = () => { - return SdkManager.instance.getNetwork({ - user: 'user', - pass: 'pass', - }); - }; - - const getEnvironmentMock = () => { - return new Environment({ - bridgeUser: 'user', - bridgePass: 'pass', - bridgeUrl: ConfigService.instance.get('NETWORK_URL'), - encryptionKey: UserFixture.mnemonic, - appDetails: SdkManager.getAppDetails(), - }); - }; beforeEach(() => { vi.restoreAllMocks(); - networkFacade = new NetworkFacade( - getNetworkMock(), - getEnvironmentMock(), - DownloadService.instance, - CryptoService.instance, - ); - const webDavFolderService = new WebDavFolderService({ - driveFolderService: DriveFolderService.instance, - configService: ConfigService.instance, - }); - sut = new PUTRequestHandler({ - driveFileService: DriveFileService.instance, - driveFolderService: DriveFolderService.instance, - webDavFolderService, - authService: AuthService.instance, - trashService: TrashService.instance, - networkFacade, - }); + + networkFacade = getNetworkFacadeMock(); + vi.spyOn(CLIUtils, 'prepareNetwork').mockResolvedValue(getNetworkOptionsMock({ networkFacade })); + + sut = new PUTRequestHandler(); }); it('should upload an empty file when the content-length request is 0', async () => { diff --git a/test/webdav/middlewares/auth.middleware.test.ts b/test/webdav/middlewares/auth.middleware.test.ts index a9ac0b3f..613eff86 100644 --- a/test/webdav/middlewares/auth.middleware.test.ts +++ b/test/webdav/middlewares/auth.middleware.test.ts @@ -22,7 +22,7 @@ describe('Auth middleware', () => { .spyOn(AuthService.instance, 'getAuthDetails') .mockRejectedValue(new MissingCredentialsError()); - await AuthMiddleware(AuthService.instance)(req, res, next); + await AuthMiddleware()(req, res, next); expect(authServiceStub).toHaveBeenCalledOnce(); expect(next).not.toHaveBeenCalled(); @@ -44,7 +44,7 @@ describe('Auth middleware', () => { const next = vi.fn(); const authServiceStub = vi.spyOn(AuthService.instance, 'getAuthDetails').mockResolvedValue(UserCredentialsFixture); - await AuthMiddleware(AuthService.instance)(req, res, next); + await AuthMiddleware()(req, res, next); expect(authServiceStub).toHaveBeenCalledOnce(); expect(next).toHaveBeenCalledOnce(); diff --git a/test/webdav/services/webdav-folder.service.test.ts b/test/webdav/services/webdav-folder.service.test.ts index dbbd097c..648a14d4 100644 --- a/test/webdav/services/webdav-folder.service.test.ts +++ b/test/webdav/services/webdav-folder.service.test.ts @@ -13,6 +13,7 @@ describe('WebDavFolderService', () => { let driveFolderService: DriveFolderService; let configService: ConfigService; const rootFolderId = 'root-uuid-123'; + const mockWebdavConfig = (createFullPath: boolean) => { vi.spyOn(configService, 'readWebdavConfig').mockResolvedValue({ createFullPath, @@ -34,10 +35,7 @@ describe('WebDavFolderService', () => { vi.restoreAllMocks(); driveFolderService = DriveFolderService.instance; configService = ConfigService.instance; - sut = new WebDavFolderService({ - driveFolderService, - configService, - }); + sut = WebDavFolderService.instance; }); describe('createParentPathOrThrow', () => { @@ -137,7 +135,7 @@ describe('WebDavFolderService', () => { uuid: 'test-uuid', }); - vi.spyOn(driveFolderService, 'createFolder').mockReturnValue([ + vi.spyOn(driveFolderService, 'createFolder').mockResolvedValue([ Promise.resolve(folderResponse), { cancel: () => {} }, ]); diff --git a/yarn.lock b/yarn.lock index 15b940d7..c1c579ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -836,6 +836,11 @@ enabled "2.0.x" kuler "^2.0.0" +"@dashlane/pqc-kem-kyber512-node@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@dashlane/pqc-kem-kyber512-node/-/pqc-kem-kyber512-node-1.0.0.tgz#0305f8a6c86595a1dc3b0d16184237c71e912d8c" + integrity sha512-gVzQwP/1OqKLyYZ/oRI9uECSnYIcLUcZbnAA34Q2l8X1eXq5JWf304tDp1UTdYdJ+ZE58SmQ68VCa/WvpCviGw== + "@emnapi/runtime@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.0.tgz#d7ef3832df8564fe5903bf0567aedbd19538ecbe" @@ -4766,6 +4771,11 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" +hash-wasm@4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.12.0.tgz#f9f1a9f9121e027a9acbf6db5d59452ace1ef9bb" + integrity sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ== + hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"