diff --git a/src/main/analytics/service.ts b/src/main/analytics/service.ts index 7ddc77619..d36c7f1e8 100644 --- a/src/main/analytics/service.ts +++ b/src/main/analytics/service.ts @@ -8,7 +8,7 @@ import { TrackedWebdavServerEvents, WebdavErrorContext, } from '../../shared/IPC/events/webdav'; -import uuid from 'uuid'; + function platformShortName(platform: string) { switch (platform) { case 'darwin': diff --git a/src/main/analytics/webdav-handlers.ts b/src/main/analytics/webdav-handlers.ts index bae97d887..498c3c5aa 100644 --- a/src/main/analytics/webdav-handlers.ts +++ b/src/main/analytics/webdav-handlers.ts @@ -1,38 +1,92 @@ -import { ipcMain, IpcMainEvent } from 'electron'; import Logger from 'electron-log'; -import { FileCreatedDomainEvent } from '../../workers/webdav/modules/files/domain/FileCreatedDomainEvent'; -import { ipcWebdav } from '../ipcs/webdav'; -import { trackWebdavError, trackWebdavEvent } from './service'; -import { WebdavDomainEvent } from '../../workers/webdav/modules/shared/domain/WebdavDomainEvent'; -import { FileDownloadedDomainEvent } from '../../workers/webdav/modules/files/domain/FileDownloadedDomainEvent'; -import { FileDeletedDomainEvent } from '../../workers/webdav/modules/files/domain/FileDeletedDomainEvent'; +import { ipcWebdav, IpcWebdavFlow, IpcWebdavFlowErrors } from '../ipcs/webdav'; +import { + trackHandledWebdavError, + trackWebdavError, + trackWebdavEvent, +} from './service'; import { WebdavErrorContext } from '../../shared/IPC/events/webdav'; -function subscribeToDomainEvents() { - function subscribeTo( - eventName: Event['eventName'], - listener: ( - event: IpcMainEvent, - domainEvent: ReturnType - ) => void - ) { - ipcMain.on(`webdav.${eventName}`, listener); - } +function subscribeToFlowEvents(ipc: IpcWebdavFlow) { + ipc.on('WEBDAV_FILE_DELETED', (_, payload) => { + const { name, type, size } = payload; - subscribeTo( - FileCreatedDomainEvent.EVENT_NAME, - (_, attributes) => { - trackWebdavEvent('Upload', attributes); - } - ); + trackWebdavEvent('Delete', { + name, + type, + size, + }); + }); + + ipc.on('WEBDAV_FILE_DOWNLOADED', (_, payload) => { + const { name, type, size, uploadInfo } = payload; + + trackWebdavEvent('Upload', { + name, + type, + size, + elapsedTime: uploadInfo.elapsedTime, + }); + }); + + ipc.on('WEBDAV_FILE_MOVED', (_, payload) => { + const { name, folderName } = payload; - // For some reason FileDownloadedDomainEvent.EVENT_NAME does not work ( ̄(エ) ̄)ゞ - subscribeTo('file.downloaded', (_, attributes) => { - trackWebdavEvent('Preview', attributes); + trackWebdavEvent('Move', { + name, + folderName, + }); }); - subscribeTo(FileDeletedDomainEvent.EVENT_NAME, () => { - trackWebdavEvent('Delete', {}); + ipc.on('WEBDAV_FILE_OVERWRITED', (_, payload) => { + const { name } = payload; + + trackWebdavEvent('Move', { + name, + }); + }); + + ipc.on('WEBDAV_FILE_RENAMED', (_, payload) => { + const { name } = payload; + + trackWebdavEvent('Rename', { + name, + }); + }); + + ipc.on('WEBDAV_FILE_CLONNED', (_, payload) => { + const { name, type, size, uploadInfo } = payload; + + trackWebdavEvent('Upload', { + name, + type, + size, + clonned: true, + elapsedTime: uploadInfo.elapsedTime, + }); + }); + + ipc.on('WEBDAV_FILE_UPLOADED', (_, payload) => { + const { name, type, size, uploadInfo } = payload; + + trackWebdavEvent('Upload', { + name, + type, + size, + elapsedTime: uploadInfo.elapsedTime, + }); + }); +} + +function subscribeToFlowErrors(ipc: IpcWebdavFlowErrors) { + ipc.on('WEBDAV_FILE_UPLOADED_ERROR', (_, payload) => { + const { name, error } = payload; + trackWebdavError('Upload Error', { name, error }); + }); + + ipc.on('WEBDAV_ACTION_ERROR', (_, error: Error, ctx: WebdavErrorContext) => { + const errorName = `${ctx.action} Error` as const; + trackHandledWebdavError(errorName, error, ctx); }); } @@ -44,15 +98,8 @@ function subscribeToServerEvents() { ipcWebdav.on('WEBDAV_VIRTUAL_DRIVE_MOUNT_ERROR', (_, err: Error) => { Logger.info('WEBDAV_VIRTUAL_DRIVE_MOUNT_ERROR', err.message); }); - - ipcWebdav.on( - 'WEBDAV_ACTION_ERROR', - (_, error: Error, ctx: WebdavErrorContext) => { - const errorName = `${ctx.action} Error` as const; - trackWebdavError(errorName, error, ctx); - } - ); } -subscribeToDomainEvents(); +subscribeToFlowEvents(ipcWebdav); +subscribeToFlowErrors(ipcWebdav); subscribeToServerEvents(); diff --git a/src/main/ipcs/webdav.ts b/src/main/ipcs/webdav.ts index 2c773cde8..32b731681 100644 --- a/src/main/ipcs/webdav.ts +++ b/src/main/ipcs/webdav.ts @@ -1,5 +1,7 @@ import { ipcMain } from 'electron'; import { + WebdavFlowEvents, + WebdavFlowEventsErrors, WebdavMainEvents, WebDavProcessEvents, } from '../../shared/IPC/events/webdav'; @@ -9,3 +11,9 @@ export const ipcWebdav = ipcMain as unknown as CustomIpc< WebdavMainEvents, WebDavProcessEvents >; + +type NoEvents = Record) => any>; + +export type IpcWebdavFlow = CustomIpc; + +export type IpcWebdavFlowErrors = CustomIpc; diff --git a/src/shared/IPC/IPCs.ts b/src/shared/IPC/IPCs.ts index 233d6f6d1..b8e3b5ed8 100644 --- a/src/shared/IPC/IPCs.ts +++ b/src/shared/IPC/IPCs.ts @@ -2,37 +2,63 @@ import { IpcMainEvent } from 'electron'; type EventHandler = (...args: any) => any; -export type CustomIPCEvents<> = Record; +export type CustomIPCEvents = Record; + +type NonVoidReturnHandler = { + [Property in keyof T as ReturnType extends void + ? never + : Property]: T[Property]; +}; + +type VoidReturnHandler = { + [Property in keyof T as ReturnType extends void + ? Property + : never]: T[Property]; +}; + +type VoidParamsHandler = { + [Property in keyof T as Parameters extends never[] + ? Property + : never]: T[Property]; +}; export interface CustomIpc< EmitedEvents extends CustomIPCEvents, - ListenedEvents extends CustomIPCEvents, - InvokableFunctions + ListenedEvents extends CustomIPCEvents > { - send( + emit(event: keyof VoidParamsHandler): void; + + send>( event: Event, - ...args: Parameters + ...args: Parameters[Event]> ): void; - emit(event: keyof EmitedEvents): void; - - invoke( - event: Event - ): InvokableFunctions[Event]; + invoke>( + event: Event, + ...args: Parameters[Event]> + ): Promise>; - on( + on>( event: Event, listener: ( event: IpcMainEvent, - ...args: Parameters + ...args: Parameters[Event]> ) => void ): void; - once( + once>( event: Event, listener: ( event: IpcMainEvent, - ...args: Parameters + ...args: Parameters[Event]> ) => void ): void; + + handle>( + event: Event, + listener: ( + event: IpcMainEvent, + ...args: Parameters[Event]> + ) => void + ): Promise>; } diff --git a/src/shared/IPC/events/webdav.ts b/src/shared/IPC/events/webdav.ts index d5e6f0478..69d6c3317 100644 --- a/src/shared/IPC/events/webdav.ts +++ b/src/shared/IPC/events/webdav.ts @@ -7,6 +7,7 @@ const trackedEvents = [ 'download', 'preview', 'move', + 'rename', ] as const; export type TrackedWebdavServerEvents = Capitalize< (typeof trackedEvents)[number] @@ -22,25 +23,79 @@ export type WebdavErrorContext = { root: string; }; -export type WebDavProcessEvents = { +type WebdavServerEvents = { WEBDAV_SERVER_START_SUCCESS: () => void; WEBDAV_SERVER_START_ERROR: (err: Error) => void; WEBDAV_SERVER_STOP_SUCCESS: () => void; WEBDAV_SERVER_STOP_ERROR: (err: Error) => void; WEBDAV_SERVER_ADDING_ROOT_FOLDER_ERROR: (err: Error) => void; +}; + +type WebdavVirtualDriveEvents = { WEBDAV_VIRTUAL_DRIVE_MOUNTED_SUCCESSFULLY: () => void; WEBDAV_VIRTUAL_DRIVE_MOUNT_ERROR: (err: Error) => void; +}; + +type UploadInfo = { + elapsedTime: number | undefined; +}; + +export type WebdavFlowEvents = { + WEBDAV_FILE_UPLOADED: (payload: { + name: string; + size: number; + type: string; + uploadInfo: UploadInfo; + }) => void; + WEBDAV_FILE_UPLOAD_PROGRESS: (payload: { + name: string; + progess: number; + uploadInfo: UploadInfo; + }) => void; + WEBDAV_FILE_DOWNLOADED: (payload: { + name: string; + size: number; + type: string; + uploadInfo: UploadInfo; + }) => void; + WEBDAV_FILE_DELETED: (payload: { + name: string; + size: number; + type: string; + }) => void; + WEBDAV_FILE_RENAMED: (payload: { name: string; oldName: string }) => void; + WEBDAV_FILE_MOVED: (payload: { name: string; folderName: string }) => void; + WEBDAV_FILE_OVERWRITED: (payload: { name: string }) => void; + WEBDAV_FILE_CLONNED: (payload: { + name: string; + size: number; + type: string; + uploadInfo: UploadInfo; + }) => void; +}; + +export type WebdavFlowEventsErrors = { + WEBDAV_FILE_UPLOADED_ERROR: (payload: { + name: string; + error: string; + }) => void; WEBDAV_ACTION_ERROR: (err: Error, ctx: WebdavErrorContext) => void; }; export type WebdavInvokableFunctions = { - GET_UPDATED_REMOTE_ITEMS: Promise<{ + GET_UPDATED_REMOTE_ITEMS: () => Promise<{ files: DriveFile[]; folders: DriveFolder[]; }>; - START_REMOTE_SYNC: Promise; + START_REMOTE_SYNC: () => Promise; }; +export type WebDavProcessEvents = WebdavServerEvents & + WebdavVirtualDriveEvents & + WebdavFlowEvents & + WebdavFlowEventsErrors & + WebdavInvokableFunctions; + export type WebdavMainEvents = { STOP_WEBDAV_SERVER_PROCESS: () => void; }; diff --git a/src/shared/types/Stopwatch.ts b/src/shared/types/Stopwatch.ts new file mode 100644 index 000000000..085778187 --- /dev/null +++ b/src/shared/types/Stopwatch.ts @@ -0,0 +1,33 @@ +import { performance } from 'perf_hooks'; + +export class Stopwatch { + private _start: number | undefined; + private _finish: number | undefined; + + start() { + if (this._start) { + this._finish = undefined; + } + + this._start = performance.now(); + } + + finish() { + if (!this._start) { + throw new Error('Cannot finish a stopwatch that have not started'); + } + this._finish = performance.now(); + } + + elapsedTime(): number | undefined { + if (!this._start || !this._finish) { + return undefined; + } + + return this._start - this._finish; + } + + reset() { + this._start = this._finish = undefined; + } +} diff --git a/src/workers/webdav/dependencyInjection/DependencyContainer.ts b/src/workers/webdav/dependencyInjection/DependencyContainer.ts index 990b5a616..152c70673 100644 --- a/src/workers/webdav/dependencyInjection/DependencyContainer.ts +++ b/src/workers/webdav/dependencyInjection/DependencyContainer.ts @@ -37,7 +37,7 @@ export interface DependencyContainer { fileDeleter: WebdavFileDeleter; fileMover: WebdavFileMover; fileCreator: WebdavFileCreator; - fileDonwloader: WebdavFileDownloader; + fileDownloader: WebdavFileDownloader; fileMimeTypeResolver: WebdavFileMimeTypeResolver; fileRenamer: WebdavFileRenamer; diff --git a/src/workers/webdav/dependencyInjection/DependencyContainerFactory.ts b/src/workers/webdav/dependencyInjection/DependencyContainerFactory.ts index fbdc53cf7..467b373f5 100644 --- a/src/workers/webdav/dependencyInjection/DependencyContainerFactory.ts +++ b/src/workers/webdav/dependencyInjection/DependencyContainerFactory.ts @@ -23,7 +23,7 @@ import { Traverser } from '../modules/items/application/Traverser'; import { AllWebdavItemsNameLister } from '../modules/shared/application/AllWebdavItemsSearcher'; import { WebdavUnknownItemTypeSearcher } from '../modules/shared/application/WebdavUnknownItemTypeSearcher'; import { WebdavUnkownItemMetadataDealer } from '../modules/shared/application/WebdavUnkownItemMetadataDealer'; -import { DuplexEventBus } from '../modules/shared/infrastructure/DuplexEventBus'; +import { NodeJsEventBus } from '../modules/shared/infrastructure/DuplexEventBus'; import { FreeSpacePerEnvironmentCalculator } from '../modules/userUsage/application/FreeSpacePerEnvironmentCalculator'; import { IncrementDriveUsageOnFileCreated } from '../modules/userUsage/application/IncrementDriveUsageOnFileCreated'; import { UsedSpaceCalculator } from '../modules/userUsage/application/UsedSpaceCalculator'; @@ -112,7 +112,7 @@ export class DependencyContainerFactory { user.bucket ); - const eventBus = new DuplexEventBus(); + const eventBus = new NodeJsEventBus(); const fileRenamer = new WebdavFileRenamer(fileRepository, eventBus); @@ -148,26 +148,30 @@ export class DependencyContainerFactory { fileRepository, folderFinder, fileContentRepository, - eventBus + eventBus, + ipc ), - fileDeleter: new WebdavFileDeleter(fileRepository, eventBus), + fileDeleter: new WebdavFileDeleter(fileRepository, eventBus, ipc), fileMover: new WebdavFileMover( fileRepository, folderFinder, fileRenamer, - eventBus + eventBus, + ipc ), fileCreator: new WebdavFileCreator( fileRepository, folderFinder, fileContentRepository, temporalFileCollection, - eventBus + eventBus, + ipc ), - fileDonwloader: new WebdavFileDownloader( + fileDownloader: new WebdavFileDownloader( fileRepository, fileContentRepository, - eventBus + eventBus, + ipc ), fileRenamer, fileMimeTypeResolver: new WebdavFileMimeTypeResolver(), diff --git a/src/workers/webdav/ipc.ts b/src/workers/webdav/ipc.ts index 84751972f..69ccbd5e8 100644 --- a/src/workers/webdav/ipc.ts +++ b/src/workers/webdav/ipc.ts @@ -1,15 +1,10 @@ import { ipcRenderer } from 'electron'; import { - WebdavInvokableFunctions, WebdavMainEvents, WebDavProcessEvents, } from '../../shared/IPC/events/webdav'; import { CustomIpc } from '../../shared/IPC/IPCs'; -export type WebdavCustomIpc = CustomIpc< - WebDavProcessEvents, - WebdavMainEvents, - WebdavInvokableFunctions ->; +export type WebdavIpc = CustomIpc; -export const ipc = ipcRenderer as unknown as WebdavCustomIpc; +export const ipc = ipcRenderer as unknown as WebdavIpc; diff --git a/src/workers/webdav/modules/files/application/WebdavFileClonner.ts b/src/workers/webdav/modules/files/application/WebdavFileClonner.ts index d27e614cb..cb78eeb45 100644 --- a/src/workers/webdav/modules/files/application/WebdavFileClonner.ts +++ b/src/workers/webdav/modules/files/application/WebdavFileClonner.ts @@ -5,6 +5,9 @@ import { WebdavFile } from '../domain/WebdavFile'; import { WebdavFileRepository } from '../domain/WebdavFileRepository'; import { WebdavServerEventBus } from '../../shared/domain/WebdavServerEventBus'; import { FileAlreadyExistsError } from '../domain/errors/FileAlreadyExistsError'; +import { Stopwatch } from '../../../../../shared/types/Stopwatch'; +import { WebdavIpc } from '../../../ipc'; +import { ContentFileClonner } from '../domain/ContentFileClonner'; export class WebdavFileClonner { private static FILE_OVERRIDED = true; @@ -14,9 +17,29 @@ export class WebdavFileClonner { private readonly repository: WebdavFileRepository, private readonly folderFinder: WebdavFolderFinder, private readonly contentRepository: RemoteFileContentsRepository, - private readonly eventBus: WebdavServerEventBus + private readonly eventBus: WebdavServerEventBus, + private readonly ipc: WebdavIpc ) {} + private registerEvents(clonner: ContentFileClonner, file: WebdavFile) { + const stopwatch = new Stopwatch(); + + clonner.on('start', () => { + stopwatch.start(); + }); + + clonner.on('finish', () => { + stopwatch.finish(); + + this.ipc.send('WEBDAV_FILE_CLONNED', { + name: file.name, + type: file.type, + size: file.size, + uploadInfo: { elapsedTime: stopwatch.elapsedTime() }, + }); + }); + } + private async overwrite( file: WebdavFile, fileOverwritted: WebdavFile, @@ -24,7 +47,12 @@ export class WebdavFileClonner { ) { const destinationFolder = this.folderFinder.run(fileOverwritted.dirname); - const clonnedFileId = await this.contentRepository.clone(file); + const clonner = this.contentRepository.clonner(file); + + this.registerEvents(clonner, file); + + const clonnedFileId = await clonner.clone(); + const newFile = file.overwrite( clonnedFileId, destinationFolder.id, @@ -33,17 +61,27 @@ export class WebdavFileClonner { fileOverwritted.trash(); - await this.repository.delete(fileOverwritted); - await this.repository.add(newFile); + try { + await this.repository.delete(fileOverwritted); + await this.repository.add(newFile); - await this.eventBus.publish(newFile.pullDomainEvents()); - await this.eventBus.publish(fileOverwritted.pullDomainEvents()); + await this.eventBus.publish(newFile.pullDomainEvents()); + await this.eventBus.publish(fileOverwritted.pullDomainEvents()); + } catch (err: unknown) { + if (!(err instanceof Error)) { + throw new Error(`${err} was thrown`); + } + } } private async copy(file: WebdavFile, path: FilePath) { const destinationFolder = this.folderFinder.run(path.dirname()); - const clonnedFileId = await this.contentRepository.clone(file); + const clonner = this.contentRepository.clonner(file); + + this.registerEvents(clonner, file); + + const clonnedFileId = await clonner.clone(); const clonned = file.clone(clonnedFileId, destinationFolder.id, path); diff --git a/src/workers/webdav/modules/files/application/WebdavFileCreator.ts b/src/workers/webdav/modules/files/application/WebdavFileCreator.ts index c66bf9598..c0343e4ef 100644 --- a/src/workers/webdav/modules/files/application/WebdavFileCreator.ts +++ b/src/workers/webdav/modules/files/application/WebdavFileCreator.ts @@ -9,6 +9,9 @@ import { WebdavFileRepository } from '../domain/WebdavFileRepository'; import { WebdavFolder } from '../../folders/domain/WebdavFolder'; import { FileSize } from '../domain/FileSize'; import { WebdavServerEventBus } from '../../shared/domain/WebdavServerEventBus'; +import { ContentFileUploader } from '../domain/ContentFileUploader'; +import { WebdavIpc } from '../../../ipc'; +import { Stopwatch } from '../../../../../shared/types/Stopwatch'; export class WebdavFileCreator { constructor( @@ -16,9 +19,46 @@ export class WebdavFileCreator { private readonly folderFinder: WebdavFolderFinder, private readonly contentsRepository: RemoteFileContentsRepository, private readonly temporalFileCollection: FileMetadataCollection, - private readonly eventBus: WebdavServerEventBus + private readonly eventBus: WebdavServerEventBus, + private readonly ipc: WebdavIpc ) {} + private registerEvents( + uploader: ContentFileUploader, + metadata: ItemMetadata + ) { + const stopwatch = new Stopwatch(); + + uploader.on('start', () => { + stopwatch.start(); + + this.ipc.send('WEBDAV_FILE_UPLOAD_PROGRESS', { + name: metadata.name, + progess: 0, + uploadInfo: { elapsedTime: stopwatch.elapsedTime() }, + }); + }); + + uploader.on('progress', (progess: number) => { + this.ipc.send('WEBDAV_FILE_UPLOAD_PROGRESS', { + name: metadata.name, + progess, + uploadInfo: { elapsedTime: stopwatch.elapsedTime() }, + }); + }); + + uploader.on('finish', () => { + stopwatch.finish(); + + this.ipc.send('WEBDAV_FILE_UPLOADED', { + name: metadata.name, + type: metadata.type, + size: metadata.size, + uploadInfo: { elapsedTime: stopwatch.elapsedTime() }, + }); + }); + } + private async createFileEntry( fileId: string, folder: WebdavFolder, @@ -46,30 +86,36 @@ export class WebdavFileCreator { const fileSize = new FileSize(size); const filePath = new FilePath(path); - this.temporalFileCollection.add( - filePath.value, - ItemMetadata.from({ - createdAt: Date.now(), - updatedAt: Date.now(), - name: filePath.name(), - size, - extension: filePath.extension(), - type: 'FILE', - }) - ); + const metadata = ItemMetadata.from({ + createdAt: Date.now(), + updatedAt: Date.now(), + name: filePath.name(), + size, + extension: filePath.extension(), + type: 'FILE', + }); + + this.temporalFileCollection.add(filePath.value, metadata); const folder = this.folderFinder.run(filePath.dirname()); const stream = new PassThrough(); - const upload = this.contentsRepository.upload(fileSize, stream); + const uploader = this.contentsRepository.uploader(fileSize, stream); + + this.registerEvents(uploader, metadata); + + const upload = uploader.upload(); upload .then(async (fileId) => { return this.createFileEntry(fileId, folder, size, filePath); }) - .catch(() => { - // TODO: comunicate somehow this error happened + .catch((error: Error) => { + this.ipc.send('WEBDAV_FILE_UPLOADED_ERROR', { + name: metadata.name, + error: error.message, + }); }); return { diff --git a/src/workers/webdav/modules/files/application/WebdavFileDeleter.ts b/src/workers/webdav/modules/files/application/WebdavFileDeleter.ts index 5dddf7865..dc73b5997 100644 --- a/src/workers/webdav/modules/files/application/WebdavFileDeleter.ts +++ b/src/workers/webdav/modules/files/application/WebdavFileDeleter.ts @@ -1,3 +1,4 @@ +import { WebdavIpc } from '../../../ipc'; import { WebdavServerEventBus } from '../../shared/domain/WebdavServerEventBus'; import { WebdavFile } from '../domain/WebdavFile'; import { WebdavFileRepository } from '../domain/WebdavFileRepository'; @@ -5,7 +6,8 @@ import { WebdavFileRepository } from '../domain/WebdavFileRepository'; export class WebdavFileDeleter { constructor( private readonly repository: WebdavFileRepository, - private readonly eventBus: WebdavServerEventBus + private readonly eventBus: WebdavServerEventBus, + private readonly ipc: WebdavIpc ) {} async run(file: WebdavFile): Promise { @@ -14,5 +16,11 @@ export class WebdavFileDeleter { await this.repository.delete(file); await this.eventBus.publish(file.pullDomainEvents()); + + this.ipc.send('WEBDAV_FILE_DELETED', { + name: file.name, + type: file.type, + size: file.size, + }); } } diff --git a/src/workers/webdav/modules/files/application/WebdavFileDownloader.ts b/src/workers/webdav/modules/files/application/WebdavFileDownloader.ts index 69baefd75..6d8983217 100644 --- a/src/workers/webdav/modules/files/application/WebdavFileDownloader.ts +++ b/src/workers/webdav/modules/files/application/WebdavFileDownloader.ts @@ -4,23 +4,52 @@ import { RemoteFileContentsRepository } from '../domain/RemoteFileContentsReposi import { RemoteFileContents } from '../domain/RemoteFileContent'; import { WebdavFileRepository } from '../domain/WebdavFileRepository'; import { FilePath } from '../domain/FilePath'; +import { WebdavIpc } from 'workers/webdav/ipc'; +import { Stopwatch } from '../../../../../shared/types/Stopwatch'; +import { ContentFileDownloader } from '../domain/ContentFileDownloader'; +import { WebdavFile } from '../domain/WebdavFile'; export class WebdavFileDownloader { constructor( private readonly repository: WebdavFileRepository, private readonly contents: RemoteFileContentsRepository, - private readonly eventBus: WebdavServerEventBus + private readonly eventBus: WebdavServerEventBus, + private readonly ipc: WebdavIpc ) {} + private registerEvents(downloader: ContentFileDownloader, file: WebdavFile) { + const stopwatch = new Stopwatch(); + + downloader.on('start', () => { + stopwatch.start(); + }); + + downloader.on('finish', () => { + stopwatch.finish(); + + this.ipc.send('WEBDAV_FILE_DOWNLOADED', { + name: file.name, + size: file.size, + type: file.type, + uploadInfo: { elapsedTime: stopwatch.elapsedTime() }, + }); + }); + } + async run(path: string): Promise { const filePath = new FilePath(path); + const file = this.repository.search(filePath); if (!file) { throw new FileNotFoundError(path); } - const readable = await this.contents.download(file); + const downloader = this.contents.downloader(file); + + this.registerEvents(downloader, file); + + const readable = await downloader.download(); const remoteContents = RemoteFileContents.preview(file, readable); diff --git a/src/workers/webdav/modules/files/application/WebdavFileMover.ts b/src/workers/webdav/modules/files/application/WebdavFileMover.ts index 5127914d1..366d4804e 100644 --- a/src/workers/webdav/modules/files/application/WebdavFileMover.ts +++ b/src/workers/webdav/modules/files/application/WebdavFileMover.ts @@ -1,11 +1,13 @@ -import { WebdavFolder } from '../../folders/domain/WebdavFolder'; +import { WebdavIpc } from 'workers/webdav/ipc'; import { WebdavFolderFinder } from '../../folders/application/WebdavFolderFinder'; +import { WebdavFolder } from '../../folders/domain/WebdavFolder'; +import { WebdavServerEventBus } from '../../shared/domain/WebdavServerEventBus'; +import { FileAlreadyExistsError } from '../domain/errors/FileAlreadyExistsError'; +import { UnknownFileActionError } from '../domain/errors/UnknownFileActionError'; import { FilePath } from '../domain/FilePath'; import { WebdavFile } from '../domain/WebdavFile'; import { WebdavFileRepository } from '../domain/WebdavFileRepository'; -import { FileAlreadyExistsError } from '../domain/errors/FileAlreadyExistsError'; -import { UnknownFileActionError } from '../domain/errors/UnknownFileActionError'; -import { WebdavServerEventBus } from '../../shared/domain/WebdavServerEventBus'; + import { WebdavFileRenamer } from './WebdavFileRenamer'; export class WebdavFileMover { @@ -13,7 +15,8 @@ export class WebdavFileMover { private readonly repository: WebdavFileRepository, private readonly folderFinder: WebdavFolderFinder, private readonly fileRenamer: WebdavFileRenamer, - private readonly eventBus: WebdavServerEventBus + private readonly eventBus: WebdavServerEventBus, + private readonly ipc: WebdavIpc ) {} private async move(file: WebdavFile, folder: WebdavFolder) { @@ -22,6 +25,11 @@ export class WebdavFileMover { await this.repository.updateParentDir(file); await this.eventBus.publish(file.pullDomainEvents()); + + this.ipc.send('WEBDAV_FILE_MOVED', { + name: file.nameWithExtension, + folderName: file.dirname, + }); } private async overwite( @@ -37,6 +45,10 @@ export class WebdavFileMover { await this.eventBus.publish(file.pullDomainEvents()); await this.eventBus.publish(destinationFile.pullDomainEvents()); + + this.ipc.send('WEBDAV_FILE_OVERWRITED', { + name: file.nameWithExtension, + }); } private noMoreActionsLeft(action: never) { diff --git a/src/workers/webdav/modules/files/domain/ContentFileClonner.ts b/src/workers/webdav/modules/files/domain/ContentFileClonner.ts new file mode 100644 index 000000000..966fff474 --- /dev/null +++ b/src/workers/webdav/modules/files/domain/ContentFileClonner.ts @@ -0,0 +1,22 @@ +import { WebdavFileAtributes } from './WebdavFile'; + +export type FileCloneEvents = { + start: () => void; + 'start-download': () => void; + 'start-upload': () => void; + 'download-progress': (progress: number) => void; + 'upload-progress': (progress: number) => void; + 'download-finished': (fileId: WebdavFileAtributes['fileId']) => void; + 'upload-finished': (fileId: WebdavFileAtributes['fileId']) => void; + finish: (fileId: WebdavFileAtributes['fileId']) => void; + error: (error: Error) => void; +}; + +export interface ContentFileClonner { + clone(): Promise; + + on( + event: keyof FileCloneEvents, + handler: FileCloneEvents[keyof FileCloneEvents] + ): void; +} diff --git a/src/workers/webdav/modules/files/domain/ContentFileDownloader.ts b/src/workers/webdav/modules/files/domain/ContentFileDownloader.ts new file mode 100644 index 000000000..ddfa93c91 --- /dev/null +++ b/src/workers/webdav/modules/files/domain/ContentFileDownloader.ts @@ -0,0 +1,18 @@ +import { Readable } from 'stream'; +import { WebdavFileAtributes } from './WebdavFile'; + +export type FileDownloadEvents = { + start: () => void; + progress: (progress: number) => void; + finish: (fileId: WebdavFileAtributes['fileId']) => void; + error: (error: Error) => void; +}; + +export interface ContentFileDownloader { + download(): Promise; + + on( + event: keyof FileDownloadEvents, + handler: FileDownloadEvents[keyof FileDownloadEvents] + ): void; +} diff --git a/src/workers/webdav/modules/files/domain/ContentFileUploader.ts b/src/workers/webdav/modules/files/domain/ContentFileUploader.ts new file mode 100644 index 000000000..ffdcdf289 --- /dev/null +++ b/src/workers/webdav/modules/files/domain/ContentFileUploader.ts @@ -0,0 +1,17 @@ +type FileId = string; + +export type FileUploadEvents = { + start: () => void; + progress: (progress: number) => void; + finish: (fileId: FileId) => void; + error: (error: Error) => void; +}; + +export interface ContentFileUploader { + upload(): Promise; + + on( + event: keyof FileUploadEvents, + fn: FileUploadEvents[keyof FileUploadEvents] + ): void; +} diff --git a/src/workers/webdav/modules/files/domain/RemoteFileContent.ts b/src/workers/webdav/modules/files/domain/RemoteFileContent.ts index 59b3bada0..6c81705ed 100644 --- a/src/workers/webdav/modules/files/domain/RemoteFileContent.ts +++ b/src/workers/webdav/modules/files/domain/RemoteFileContent.ts @@ -16,7 +16,7 @@ export class RemoteFileContents extends AggregateRoot { static preview(file: WebdavFile, contents: Readable): RemoteFileContents { const remoteContents = new RemoteFileContents( file.fileId, - file.size.value, + file.size, file.type, contents ); diff --git a/src/workers/webdav/modules/files/domain/RemoteFileContentsRepository.ts b/src/workers/webdav/modules/files/domain/RemoteFileContentsRepository.ts index ecaf6cfed..da10681eb 100644 --- a/src/workers/webdav/modules/files/domain/RemoteFileContentsRepository.ts +++ b/src/workers/webdav/modules/files/domain/RemoteFileContentsRepository.ts @@ -1,11 +1,14 @@ import { Readable } from 'stream'; +import { ContentFileClonner } from './ContentFileClonner'; +import { ContentFileDownloader } from './ContentFileDownloader'; +import { ContentFileUploader } from './ContentFileUploader'; import { FileSize } from './FileSize'; import { WebdavFile } from './WebdavFile'; export interface RemoteFileContentsRepository { - clone(file: WebdavFile): Promise; + downloader(file: WebdavFile): ContentFileDownloader; - download(file: WebdavFile): Promise; + uploader(size: FileSize, contents: Readable): ContentFileUploader; - upload(size: FileSize, contents: Readable): Promise; + clonner(file: WebdavFile): ContentFileClonner; } diff --git a/src/workers/webdav/modules/files/domain/WebdavFile.ts b/src/workers/webdav/modules/files/domain/WebdavFile.ts index 20664ad3c..eb0700aa7 100644 --- a/src/workers/webdav/modules/files/domain/WebdavFile.ts +++ b/src/workers/webdav/modules/files/domain/WebdavFile.ts @@ -26,7 +26,7 @@ export class WebdavFile extends AggregateRoot { public readonly fileId: string, private _folderId: number, private _path: FilePath, - public readonly size: FileSize, + private readonly _size: FileSize, public readonly createdAt: Date, public readonly updatedAt: Date, private _status: FileStatus @@ -58,6 +58,10 @@ export class WebdavFile extends AggregateRoot { return this._path.dirname(); } + public get size(): number { + return this._size.value; + } + public get status() { return this._status; } @@ -107,7 +111,7 @@ export class WebdavFile extends AggregateRoot { this.record( new FileDeletedDomainEvent({ aggregateId: this.fileId, - size: this.size.value, + size: this._size.value, }) ); } @@ -136,7 +140,7 @@ export class WebdavFile extends AggregateRoot { fileId, folderId, newPath, - this.size, + this._size, this.createdAt, new Date(), FileStatus.Exists @@ -145,7 +149,7 @@ export class WebdavFile extends AggregateRoot { file.record( new FileCreatedDomainEvent({ aggregateId: fileId, - size: this.size.value, + size: this._size.value, type: this._path.extension(), }) ); @@ -158,7 +162,7 @@ export class WebdavFile extends AggregateRoot { fileId, folderId, newPath, - this.size, + this._size, this.createdAt, new Date(), FileStatus.Exists @@ -167,7 +171,7 @@ export class WebdavFile extends AggregateRoot { file.record( new FileCreatedDomainEvent({ aggregateId: fileId, - size: this.size.value, + size: this._size.value, type: this._path.extension(), }) ); @@ -215,7 +219,7 @@ export class WebdavFile extends AggregateRoot { folderId: this.folderId, createdAt: this.createdAt.getDate(), path: this._path.value, - size: this.size.value, + size: this._size.value, updatedAt: this.updatedAt.getDate(), }; } diff --git a/src/workers/webdav/modules/files/infrastructure/persistance/HttpWebdavFileRepository.ts b/src/workers/webdav/modules/files/infrastructure/persistance/HttpWebdavFileRepository.ts index a048c247e..4de235d0d 100644 --- a/src/workers/webdav/modules/files/infrastructure/persistance/HttpWebdavFileRepository.ts +++ b/src/workers/webdav/modules/files/infrastructure/persistance/HttpWebdavFileRepository.ts @@ -12,7 +12,7 @@ import { AddFileDTO } from './dtos/AddFileDTO'; import { UpdateFileParentDirDTO } from './dtos/UpdateFileParentDirDTO'; import { UpdateFileNameDTO } from './dtos/UpdateFileNameDTO'; import { FilePath } from '../../domain/FilePath'; -import { WebdavCustomIpc } from '../../../../ipc'; +import { WebdavIpc } from '../../../../ipc'; import { RemoteItemsGenerator } from 'workers/webdav/modules/items/application/RemoteItemsGenerator'; import { FileStatuses } from '../../domain/FileStatus'; @@ -24,7 +24,7 @@ export class HttpWebdavFileRepository implements WebdavFileRepository { private readonly trashHttpClient: Axios, private readonly traverser: Traverser, private readonly bucket: string, - private readonly ipc: WebdavCustomIpc + private readonly ipc: WebdavIpc ) {} private async getTree(): Promise<{ @@ -95,7 +95,7 @@ export class HttpWebdavFileRepository implements WebdavFileRepository { folder_id: file.folderId, name: encryptedName, plain_name: file.name, - size: file.size.value, + size: file.size, type: file.type, modificationTime: Date.now(), }, diff --git a/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContentFileClonner.ts b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContentFileClonner.ts new file mode 100644 index 000000000..f00670d53 --- /dev/null +++ b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContentFileClonner.ts @@ -0,0 +1,91 @@ +import { DownloadStrategyFunction } from '@internxt/inxt-js/build/lib/core'; +import { UploadStrategyFunction } from '@internxt/inxt-js/build/lib/core/upload/strategy'; +import EventEmitter from 'events'; +import { Readable } from 'stream'; +import { + ContentFileClonner, + FileCloneEvents, +} from '../../domain/ContentFileClonner'; +import { WebdavFile } from '../../domain/WebdavFile'; + +export class EnvironmentContentFileClonner implements ContentFileClonner { + private readonly eventEmitter: EventEmitter; + + constructor( + private readonly upload: UploadStrategyFunction, + private readonly download: DownloadStrategyFunction, + private readonly bucket: string, + private readonly file: WebdavFile + ) { + this.eventEmitter = new EventEmitter(); + } + + private downloadFile(): Promise { + this.eventEmitter.emit('start-download'); + return new Promise((resolve, reject) => { + this.download( + this.bucket, + this.file.fileId, + { + progressCallback: (progress: number) => { + this.eventEmitter.emit('download-progress', progress); + }, + finishedCallback: async (err: Error, stream: Readable) => { + if (err) { + this.eventEmitter.emit('error', err); + return reject(err); + } + this.eventEmitter.emit('download-finished'); + resolve(stream); + }, + }, + { + label: 'Dynamic', + params: { + useProxy: false, + concurrency: 10, + }, + } + ); + }); + } + + private uploadFile(source: Readable, file: WebdavFile): Promise { + this.eventEmitter.emit('start-upload'); + return new Promise((resolve, reject) => { + this.upload(this.bucket, { + source, + fileSize: file.size, + finishedCallback: (err: Error | null, fileId: string) => { + if (err) { + this.eventEmitter.emit('error', err); + return reject(err); + } + this.eventEmitter.emit('upload-finished', fileId); + resolve(fileId); + }, + progressCallback: (progress: number) => { + this.eventEmitter.emit('upload-progress', progress); + }, + }); + }); + } + + async clone() { + this.eventEmitter.emit('start'); + + const file = await this.downloadFile(); + const fileId = await this.uploadFile(file, this.file); + + this.eventEmitter.emit('finish'); + + return fileId; + } + + on( + event: keyof FileCloneEvents, + handler: FileCloneEvents[keyof FileCloneEvents] + ): void { + this.eventEmitter.on(event, handler); + } +} diff --git a/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContentFileUpoader.ts b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContentFileUpoader.ts new file mode 100644 index 000000000..cd70c9639 --- /dev/null +++ b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContentFileUpoader.ts @@ -0,0 +1,50 @@ +import { UploadStrategyFunction } from '@internxt/inxt-js/build/lib/core/upload/strategy'; +import { EventEmitter, Readable } from 'stream'; +import { + ContentFileUploader, + FileUploadEvents, +} from '../../domain/ContentFileUploader'; + +export class EnvironmentContentFileUpoader implements ContentFileUploader { + private eventEmitter: EventEmitter; + + constructor( + private readonly fn: UploadStrategyFunction, + private readonly bucket: string, + private readonly size: number, + private readonly file: Promise + ) { + this.eventEmitter = new EventEmitter(); + } + + async upload(): Promise { + const source = await this.file; + + this.eventEmitter.emit('start'); + + return new Promise((resolve, reject) => { + this.fn(this.bucket, { + source, + fileSize: this.size, + finishedCallback: (err: Error | null, fileId: string) => { + if (err) { + this.eventEmitter.emit('error', err); + return reject(err); + } + this.eventEmitter.emit('finish', fileId); + resolve(fileId); + }, + progressCallback: (progress: number) => { + this.eventEmitter.emit('progress', progress); + }, + }); + }); + } + + on( + event: keyof FileUploadEvents, + handler: FileUploadEvents[keyof FileUploadEvents] + ): void { + this.eventEmitter.on(event, handler); + } +} diff --git a/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContnetFileDownloader.ts b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContnetFileDownloader.ts new file mode 100644 index 000000000..5ff8745dc --- /dev/null +++ b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentContnetFileDownloader.ts @@ -0,0 +1,61 @@ +import { DownloadStrategyFunction } from '@internxt/inxt-js/build/lib/core/download/strategy'; +import { EventEmitter, Readable } from 'stream'; +import { + ContentFileDownloader, + FileDownloadEvents, +} from '../../domain/ContentFileDownloader'; +import { RemoteFileContents } from '../../domain/RemoteFileContent'; +import { WebdavFile } from '../../domain/WebdavFile'; + +export class EnvironmentContentFileDownloader implements ContentFileDownloader { + private eventEmitter: EventEmitter; + + constructor( + private readonly fn: DownloadStrategyFunction, + private readonly bucket: string, + private readonly file: WebdavFile + ) { + this.eventEmitter = new EventEmitter(); + } + + download(): Promise { + this.eventEmitter.emit('start'); + return new Promise((resolve, reject) => { + this.fn( + this.bucket, + this.file.fileId, + { + progressCallback: (progress: number) => { + this.eventEmitter.emit('progress', progress); + }, + finishedCallback: async (err: Error, stream: Readable) => { + if (err) { + this.eventEmitter.emit('error', err); + return reject(err); + } + this.eventEmitter.emit('finish'); + const remoteContents = RemoteFileContents.preview( + this.file, + stream + ); + resolve(remoteContents.stream); + }, + }, + { + label: 'Dynamic', + params: { + useProxy: false, + concurrency: 10, + }, + } + ); + }); + } + + on( + event: keyof FileDownloadEvents, + handler: FileDownloadEvents[keyof FileDownloadEvents] + ): void { + this.eventEmitter.on(event, handler); + } +} diff --git a/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentFileContentRepository.ts b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentFileContentRepository.ts index 81fa16b41..a4680dd77 100644 --- a/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentFileContentRepository.ts +++ b/src/workers/webdav/modules/files/infrastructure/storage/EnvironmentFileContentRepository.ts @@ -3,7 +3,13 @@ import { Readable } from 'stream'; import { FileSize } from '../../domain/FileSize'; import { RemoteFileContentsRepository } from '../../domain/RemoteFileContentsRepository'; import { WebdavFile } from '../../domain/WebdavFile'; -import { RemoteFileContents } from '../../domain/RemoteFileContent'; +import Logger from 'electron-log'; +import { EnvironmentContentFileUpoader } from './EnvironmentContentFileUpoader'; +import { EnvironmentContentFileDownloader } from './EnvironmentContnetFileDownloader'; +import { ContentFileDownloader } from '../../domain/ContentFileDownloader'; +import { ContentFileUploader } from '../../domain/ContentFileUploader'; +import { EnvironmentContentFileClonner } from './EnvironmentContentFileClonner'; +import { ContentFileClonner } from '../../domain/ContentFileClonner'; export class EnvironmentFileContentRepository implements RemoteFileContentsRepository @@ -15,89 +21,45 @@ export class EnvironmentFileContentRepository private readonly bucket: string ) {} - private simpleUpload(size: FileSize, contents: Readable): Promise { - return new Promise((resolve, reject) => { - this.environment.upload(this.bucket, { - progressCallback: () => { - // Noop - }, - finishedCallback: async (err: unknown, fileId: string) => { - if (!err) { - resolve(fileId); - } else { - reject(); - } - }, - fileSize: size.value, - source: contents, - }); - }); - } - - private multipartUpload(size: FileSize, contents: Readable): Promise { - return new Promise((resolve, reject) => { - this.environment.uploadMultipartFile(this.bucket, { - progressCallback: (_progress: number) => { - // Noop - }, - finishedCallback: async (err: unknown, fileId: string | null) => { - if (err) { - contents.destroy(new Error('MULTIPART UPLOAD FAILED')); - reject(); - } + clonner(file: WebdavFile): ContentFileClonner { + const uploadFunciton = + file.size > + EnvironmentFileContentRepository.MULTIPART_UPLOADE_SIZE_THRESHOLD + ? this.environment.uploadMultipartFile + : this.environment.upload; - if (!fileId) { - reject(); - return; - } + const clonner = new EnvironmentContentFileClonner( + uploadFunciton, + this.environment.download, + this.bucket, + file + ); - resolve(fileId); - }, - fileSize: size.value, - source: contents, - }); - }); + return clonner; } - async clone(file: WebdavFile): Promise { - const remoteFileContents = await this.download(file); + downloader(file: WebdavFile): ContentFileDownloader { + Logger.log('download!!', { name: file.nameWithExtension }); - return this.upload(file.size, remoteFileContents); + return new EnvironmentContentFileDownloader( + this.environment.download, + this.bucket, + file + ); } - download(file: WebdavFile): Promise { - return new Promise((resolve, reject) => { - this.environment.download( - this.bucket, - file.fileId, - { - progressCallback: () => { - // Noop - }, - finishedCallback: async (err: unknown, stream: Readable) => { - if (err) { - reject(err); - } else { - const remoteContents = RemoteFileContents.preview(file, stream); - resolve(remoteContents.stream); - } - }, - }, - { - label: 'Dynamic', - params: { - useProxy: false, - concurrency: 10, - }, - } - ); - }); - } - - upload(size: FileSize, contents: Readable): Promise { - return size.value > + uploader(size: FileSize, contents: Readable): ContentFileUploader { + const fn = + size.value > EnvironmentFileContentRepository.MULTIPART_UPLOADE_SIZE_THRESHOLD - ? this.multipartUpload(size, contents) - : this.simpleUpload(size, contents); + ? this.environment.uploadMultipartFile + : this.environment.upload; + + return new EnvironmentContentFileUpoader( + fn, + this.bucket, + size.value, + Promise.resolve(contents) + ); } } diff --git a/src/workers/webdav/modules/files/test/__mocks__/ContentFileClonnerMock.ts b/src/workers/webdav/modules/files/test/__mocks__/ContentFileClonnerMock.ts new file mode 100644 index 000000000..17a1eb729 --- /dev/null +++ b/src/workers/webdav/modules/files/test/__mocks__/ContentFileClonnerMock.ts @@ -0,0 +1,24 @@ +import { + ContentFileClonner, + FileCloneEvents, +} from '../../domain/ContentFileClonner'; + +export class ContentFileClonnerMock implements ContentFileClonner { + mock = jest.fn(); + onMock = jest.fn(); + + clone(): Promise { + return this.mock(); + } + + on( + event: keyof FileCloneEvents, + fn: + | (() => void) + | ((progress: number) => void) + | ((fileId: string) => void) + | ((error: Error) => void) + ): void { + return this.onMock(event, fn); + } +} diff --git a/src/workers/webdav/modules/files/test/__mocks__/ContentFileDownloaderMock.ts b/src/workers/webdav/modules/files/test/__mocks__/ContentFileDownloaderMock.ts new file mode 100644 index 000000000..dbaf94633 --- /dev/null +++ b/src/workers/webdav/modules/files/test/__mocks__/ContentFileDownloaderMock.ts @@ -0,0 +1,25 @@ +import { Readable } from 'stream'; +import { + ContentFileDownloader, + FileDownloadEvents, +} from '../../domain/ContentFileDownloader'; + +export class ContentFileDownloaderMock implements ContentFileDownloader { + mock = jest.fn(); + onMock = jest.fn(); + + download(): Promise { + return this.mock(); + } + + on( + event: keyof FileDownloadEvents, + fn: + | (() => void) + | ((progress: number) => void) + | ((fileId: string) => void) + | ((error: Error) => void) + ): void { + return this.onMock(event, fn); + } +} diff --git a/src/workers/webdav/modules/files/test/__mocks__/ContentFileUploaderMock.ts b/src/workers/webdav/modules/files/test/__mocks__/ContentFileUploaderMock.ts new file mode 100644 index 000000000..2feea13bf --- /dev/null +++ b/src/workers/webdav/modules/files/test/__mocks__/ContentFileUploaderMock.ts @@ -0,0 +1,23 @@ +import { + ContentFileUploader, + FileUploadEvents, +} from '../../domain/ContentFileUploader'; + +export class ContentFileUploaderMock implements ContentFileUploader { + uploadMock = jest.fn(); + onMock = jest.fn(); + + upload(): Promise { + return this.uploadMock(); + } + on( + event: keyof FileUploadEvents, + fn: + | (() => void) + | ((progress: number) => void) + | ((fileId: string) => void) + | ((error: Error) => void) + ): void { + return this.onMock(event, fn); + } +} diff --git a/src/workers/webdav/modules/files/test/__mocks__/FileContentRepositoryMock.ts b/src/workers/webdav/modules/files/test/__mocks__/FileContentRepositoryMock.ts index 73201e57f..7c1c6340e 100644 --- a/src/workers/webdav/modules/files/test/__mocks__/FileContentRepositoryMock.ts +++ b/src/workers/webdav/modules/files/test/__mocks__/FileContentRepositoryMock.ts @@ -2,21 +2,27 @@ import { Readable } from 'stream'; import { RemoteFileContentsRepository } from '../../domain/RemoteFileContentsRepository'; import { FileSize } from '../../domain/FileSize'; import { WebdavFile } from '../../domain/WebdavFile'; +import { ContentFileUploader } from '../../domain/ContentFileUploader'; +import { ContentFileClonner } from '../../domain/ContentFileClonner'; +import { ContentFileUploaderMock } from './ContentFileUploaderMock'; +import { ContentFileClonnerMock } from './ContentFileClonnerMock'; +import { ContentFileDownloaderMock } from './ContentFileDownloaderMock'; +import { ContentFileDownloader } from '../../domain/ContentFileDownloader'; export class FileContentRepositoryMock implements RemoteFileContentsRepository { - public mockClone = jest.fn(); - public mockDownload = jest.fn(); - public mockUpload = jest.fn(); + public mockClone = new ContentFileClonnerMock(); + public mockDownload = new ContentFileDownloaderMock(); + public mockUpload = new ContentFileUploaderMock(); - clone(file: WebdavFile): Promise { - return this.mockClone(file); + clonner(_file: WebdavFile): ContentFileClonner { + return this.mockClone; } - download(file: WebdavFile): Promise { - return this.mockDownload(file); + downloader(_file: WebdavFile): ContentFileDownloader { + return this.mockDownload; } - upload(size: FileSize, contents: Readable): Promise { - return this.mockUpload(size, contents); + uploader(_size: FileSize, _contents: Readable): ContentFileUploader { + return this.mockUpload; } } diff --git a/src/workers/webdav/modules/files/test/application/WebdavFileClonner.test.ts b/src/workers/webdav/modules/files/test/application/WebdavFileClonner.test.ts index b42d2abf3..e5250660b 100644 --- a/src/workers/webdav/modules/files/test/application/WebdavFileClonner.test.ts +++ b/src/workers/webdav/modules/files/test/application/WebdavFileClonner.test.ts @@ -1,3 +1,4 @@ +import { WebdavIpcMock } from '../../../shared/infrastructure/__mock__/WebdavIPC'; import { WebdavFolderFinder } from '../../../folders/application/WebdavFolderFinder'; import { WebdavFolderMother } from '../../../folders/test/domain/WebdavFolderMother'; import { WebdavFolderRepositoryMock } from '../../../folders/test/__mocks__/WebdavFolderRepositoryMock'; @@ -16,6 +17,7 @@ describe('Webdav File Clonner', () => { let folderRepository: WebdavFolderRepositoryMock; let contentsRepository: FileContentRepositoryMock; let eventBus: EventBusMock; + let ipc: WebdavIpcMock; let SUT: WebdavFileClonner; @@ -25,12 +27,14 @@ describe('Webdav File Clonner', () => { const folderFinder = new WebdavFolderFinder(folderRepository); contentsRepository = new FileContentRepositoryMock(); eventBus = new EventBusMock(); + ipc = new WebdavIpcMock(); SUT = new WebdavFileClonner( fileReposiotry, folderFinder, contentsRepository, - eventBus + eventBus, + ipc ); }); @@ -44,7 +48,7 @@ describe('Webdav File Clonner', () => { fileReposiotry.mockSearch.mockReturnValueOnce(fileToOverride); folderRepository.mockSearch.mockReturnValueOnce(folder); - contentsRepository.mockClone.mockReturnValueOnce(clonnedFileId); + contentsRepository.mockClone.mock.mockReturnValueOnce(clonnedFileId); fileReposiotry.mockAdd.mockImplementationOnce(() => { // }); @@ -82,7 +86,7 @@ describe('Webdav File Clonner', () => { fileReposiotry.mockSearch.mockReturnValueOnce(undefined); folderRepository.mockSearch.mockReturnValueOnce(folder); - contentsRepository.mockClone.mockReturnValueOnce(clonnedFileId); + contentsRepository.mockClone.mock.mockReturnValueOnce(clonnedFileId); fileReposiotry.mockAdd.mockImplementationOnce(() => { // }); diff --git a/src/workers/webdav/modules/files/test/application/WebdavFileCreator.test.ts b/src/workers/webdav/modules/files/test/application/WebdavFileCreator.test.ts index 642f08329..a0e3e6548 100644 --- a/src/workers/webdav/modules/files/test/application/WebdavFileCreator.test.ts +++ b/src/workers/webdav/modules/files/test/application/WebdavFileCreator.test.ts @@ -6,9 +6,8 @@ import { FileMetadataCollection } from '../../domain/FileMetadataCollection'; import { InMemoryTemporalFileMetadataCollection } from '../../infrastructure/persistance/InMemoryTemporalFileMetadataCollection'; import { FileContentRepositoryMock } from '../__mocks__/FileContentRepositoryMock'; import { WebdavFileRepositoryMock } from '../__mocks__/WebdavFileRepositoyMock'; -import { WebdavServerEventBus } from '../../../shared/domain/WebdavServerEventBus'; import { EventBusMock } from '../../../shared/test/__mock__/EventBusMock'; -import { FileSize } from '../../domain/FileSize'; +import { WebdavIpcMock } from '../../../shared/infrastructure/__mock__/WebdavIPC'; describe('Webdav File Creator', () => { let fileReposiotry: WebdavFileRepositoryMock; @@ -16,6 +15,7 @@ describe('Webdav File Creator', () => { let contentsRepository: FileContentRepositoryMock; let temporalFileCollection: FileMetadataCollection; let eventBus: EventBusMock; + let ipc: WebdavIpcMock; let SUT: WebdavFileCreator; @@ -26,13 +26,15 @@ describe('Webdav File Creator', () => { contentsRepository = new FileContentRepositoryMock(); temporalFileCollection = new InMemoryTemporalFileMetadataCollection(); eventBus = new EventBusMock(); + ipc = new WebdavIpcMock(); SUT = new WebdavFileCreator( fileReposiotry, folderFinder, contentsRepository, temporalFileCollection, - eventBus + eventBus, + ipc ); }); @@ -44,18 +46,18 @@ describe('Webdav File Creator', () => { const folder = WebdavFolderMother.any(); folderRepository.mockSearch.mockReturnValueOnce(folder); - contentsRepository.mockUpload.mockResolvedValueOnce(createdFileId); + contentsRepository.mockUpload.uploadMock.mockResolvedValueOnce( + createdFileId + ); fileReposiotry.mockAdd.mockImplementationOnce(() => { // returns Promise }); await SUT.run(path, size); - expect(contentsRepository.mockUpload).toBeCalledTimes(1); + expect(contentsRepository.mockUpload.uploadMock).toBeCalledTimes(1); expect(fileReposiotry.mockAdd.mock.calls[0][0].fileId).toBe(createdFileId); - expect(fileReposiotry.mockAdd.mock.calls[0][0].size).toStrictEqual( - new FileSize(size) - ); + expect(fileReposiotry.mockAdd.mock.calls[0][0].size).toStrictEqual(size); expect(fileReposiotry.mockAdd.mock.calls[0][0].folderId).toBe(folder.id); }); @@ -67,7 +69,9 @@ describe('Webdav File Creator', () => { const folder = WebdavFolderMother.any(); folderRepository.mockSearch.mockReturnValueOnce(folder); - contentsRepository.mockUpload.mockResolvedValueOnce(createdFileId); + contentsRepository.mockUpload.uploadMock.mockResolvedValueOnce( + createdFileId + ); fileReposiotry.mockAdd.mockImplementationOnce(() => { // returns Promise }); @@ -91,7 +95,7 @@ describe('Webdav File Creator', () => { const folder = WebdavFolderMother.any(); folderRepository.mockSearch.mockReturnValueOnce(folder); - contentsRepository.mockUpload.mockResolvedValueOnce(''); + contentsRepository.mockUpload.uploadMock.mockResolvedValueOnce(''); const { stream } = await SUT.run(path, size); @@ -105,7 +109,9 @@ describe('Webdav File Creator', () => { const folder = WebdavFolderMother.any(); folderRepository.mockSearch.mockReturnValueOnce(folder); - contentsRepository.mockUpload.mockRejectedValueOnce('TEST ERROR'); + contentsRepository.mockUpload.uploadMock.mockRejectedValueOnce( + 'TEST ERROR' + ); try { await SUT.run(path, size); diff --git a/src/workers/webdav/modules/files/test/application/WebdavFileDownloader.test.ts b/src/workers/webdav/modules/files/test/application/WebdavFileDownloader.test.ts index 95995887d..4112ce6bd 100644 --- a/src/workers/webdav/modules/files/test/application/WebdavFileDownloader.test.ts +++ b/src/workers/webdav/modules/files/test/application/WebdavFileDownloader.test.ts @@ -6,18 +6,26 @@ import { WebdavFileMother } from '../domain/WebdavFileMother'; import { FileContentRepositoryMock } from '../__mocks__/FileContentRepositoryMock'; import { WebdavFileRepositoryMock } from '../__mocks__/WebdavFileRepositoyMock'; import { FilePath } from '../../domain/FilePath'; - +import { WebdavIpcMock } from '../../../shared/infrastructure/__mock__/WebdavIPC'; describe('Webdav File Downloader', () => { let repository: WebdavFileRepositoryMock; let contentsRepository: FileContentRepositoryMock; let eventBus: WebdavServerEventBus; let SUT: WebdavFileDownloader; + let ipc: WebdavIpcMock; beforeEach(() => { repository = new WebdavFileRepositoryMock(); contentsRepository = new FileContentRepositoryMock(); eventBus = new EventBusMock(); - SUT = new WebdavFileDownloader(repository, contentsRepository, eventBus); + ipc = new WebdavIpcMock(); + + SUT = new WebdavFileDownloader( + repository, + contentsRepository, + eventBus, + ipc + ); }); it('Gets the a readable stream when the path is founded', async () => { @@ -25,14 +33,14 @@ describe('Webdav File Downloader', () => { const file = WebdavFileMother.onFolderName(folderPath); repository.mockSearch.mockReturnValueOnce(file); - contentsRepository.mockDownload.mockResolvedValueOnce(new Readable()); + contentsRepository.mockDownload.mock.mockResolvedValueOnce(new Readable()); const readable = await SUT.run(file.path); expect(readable).toBeDefined(); expect(repository.mockSearch).toHaveBeenCalledWith(new FilePath(file.path)); - expect(contentsRepository.mockDownload).toHaveBeenCalledWith(file); + expect(contentsRepository.mockDownload.mock).toHaveBeenCalled(); }); it('Throws an exception if the path is not founded', async () => { diff --git a/src/workers/webdav/modules/files/test/application/WebdavFileMover.test.ts b/src/workers/webdav/modules/files/test/application/WebdavFileMover.test.ts index 0191ab16e..02c9e4679 100644 --- a/src/workers/webdav/modules/files/test/application/WebdavFileMover.test.ts +++ b/src/workers/webdav/modules/files/test/application/WebdavFileMover.test.ts @@ -7,6 +7,7 @@ import { FileAlreadyExistsError } from '../../domain/errors/FileAlreadyExistsErr import { WebdavFileMother } from '../domain/WebdavFileMother'; import { WebdavFileRepositoryMock } from '../__mocks__/WebdavFileRepositoyMock'; import { FilePath } from '../../domain/FilePath'; +import { WebdavIpcMock } from '../../../shared/infrastructure/__mock__/WebdavIPC'; import { WebdavFileRenamer } from '../../application/WebdavFileRenamer'; describe('Webdav File Mover', () => { @@ -15,6 +16,7 @@ describe('Webdav File Mover', () => { let folderFinder: WebdavFolderFinder; let fileRenamer: WebdavFileRenamer; let eventBus: EventBusMock; + let ipc: WebdavIpcMock; let SUT: WebdavFileMover; @@ -24,7 +26,15 @@ describe('Webdav File Mover', () => { folderFinder = new WebdavFolderFinder(folderRepository); fileRenamer = new WebdavFileRenamer(repository, eventBus); eventBus = new EventBusMock(); - SUT = new WebdavFileMover(repository, folderFinder, fileRenamer, eventBus); + ipc = new WebdavIpcMock(); + + SUT = new WebdavFileMover( + repository, + folderFinder, + fileRenamer, + eventBus, + ipc + ); }); describe('Move', () => { diff --git a/src/workers/webdav/modules/files/test/infrastructure/storage/EnvironmentFileContentRepository.test.ts b/src/workers/webdav/modules/files/test/infrastructure/storage/EnvironmentFileContentRepository.test.ts index 38fe95b03..8e323336c 100644 --- a/src/workers/webdav/modules/files/test/infrastructure/storage/EnvironmentFileContentRepository.test.ts +++ b/src/workers/webdav/modules/files/test/infrastructure/storage/EnvironmentFileContentRepository.test.ts @@ -13,12 +13,12 @@ describe.skip('Environment File Content Repository', () => { }); const bucket = '34a3023d-aaed-5b85-9abc-3c6b22076670'; - let reposiotry: EnvironmentFileContentRepository; + let repository: EnvironmentFileContentRepository; let mockDownload: jest.Mock; let mockUpload: jest.Mock; beforeEach(() => { - reposiotry = new EnvironmentFileContentRepository(environment, bucket); + repository = new EnvironmentFileContentRepository(environment, bucket); mockDownload = jest.fn(); mockUpload = jest.fn(); @@ -40,7 +40,7 @@ describe.skip('Environment File Content Repository', () => { cbs.finishedCallback(undefined, new Readable()); }); - const stream = await reposiotry.download(file); + const stream = await repository.downloader(file).download(); expect(stream.readable).toBe(true); }); diff --git a/src/workers/webdav/modules/folders/infrastructure/HttpWebdavFolderRepository.ts b/src/workers/webdav/modules/folders/infrastructure/HttpWebdavFolderRepository.ts index c42e854fc..8aaa06fbf 100644 --- a/src/workers/webdav/modules/folders/infrastructure/HttpWebdavFolderRepository.ts +++ b/src/workers/webdav/modules/folders/infrastructure/HttpWebdavFolderRepository.ts @@ -9,7 +9,7 @@ import { WebdavFolderRepository } from '../domain/WebdavFolderRepository'; import Logger from 'electron-log'; import * as uuid from 'uuid'; import { UpdateFolderNameDTO } from './dtos/UpdateFolderNameDTO'; -import { WebdavCustomIpc } from '../../../ipc'; +import { WebdavIpc } from '../../../ipc'; import { RemoteItemsGenerator } from '../../items/application/RemoteItemsGenerator'; import { FolderStatuses } from '../domain/FolderStatus'; @@ -20,7 +20,7 @@ export class HttpWebdavFolderRepository implements WebdavFolderRepository { private readonly driveClient: Axios, private readonly trashClient: Axios, private readonly traverser: Traverser, - private readonly ipc: WebdavCustomIpc + private readonly ipc: WebdavIpc ) {} private async getTree(): Promise<{ diff --git a/src/workers/webdav/modules/items/application/RemoteItemsGenerator.ts b/src/workers/webdav/modules/items/application/RemoteItemsGenerator.ts index 85dadc6c4..e5ab1957f 100644 --- a/src/workers/webdav/modules/items/application/RemoteItemsGenerator.ts +++ b/src/workers/webdav/modules/items/application/RemoteItemsGenerator.ts @@ -6,10 +6,10 @@ import { ServerFolder, ServerFolderStatus, } from '../../../../filesystems/domain/ServerFolder'; -import { WebdavCustomIpc } from 'workers/webdav/ipc'; +import { WebdavIpc } from 'workers/webdav/ipc'; export class RemoteItemsGenerator { - constructor(private readonly ipc: WebdavCustomIpc) {} + constructor(private readonly ipc: WebdavIpc) {} async getAll(): Promise<{ files: ServerFile[]; folders: ServerFolder[] }> { const updatedRemoteItems = await this.ipc.invoke( 'GET_UPDATED_REMOTE_ITEMS' diff --git a/src/workers/webdav/modules/shared/domain/ItemMetadata.ts b/src/workers/webdav/modules/shared/domain/ItemMetadata.ts index 0e50b80c3..96df8bacf 100644 --- a/src/workers/webdav/modules/shared/domain/ItemMetadata.ts +++ b/src/workers/webdav/modules/shared/domain/ItemMetadata.ts @@ -36,7 +36,7 @@ export class ItemMetadata { file.createdAt.getTime(), file.updatedAt.getTime(), file.nameWithExtension, - file.size.value, + file.size, file.type, 'FILE' ); diff --git a/src/workers/webdav/modules/shared/infrastructure/DuplexEventBus.ts b/src/workers/webdav/modules/shared/infrastructure/DuplexEventBus.ts index 178bf76d3..3897455a9 100644 --- a/src/workers/webdav/modules/shared/infrastructure/DuplexEventBus.ts +++ b/src/workers/webdav/modules/shared/infrastructure/DuplexEventBus.ts @@ -1,18 +1,14 @@ -import { ipcRenderer } from 'electron'; import EventEmitter from 'events'; import { WebdavDomainEvent } from '../domain/WebdavDomainEvent'; import { WebdavServerEventBus } from '../domain/WebdavServerEventBus'; import { DomainEventSubscribers } from './DomainEventSubscribers'; -// Emits domain events from the webdav server to itself -// and to the main process -export class DuplexEventBus +export class NodeJsEventBus extends EventEmitter implements WebdavServerEventBus { async publish(events: Array): Promise { events.forEach((event) => { - ipcRenderer.send(event.eventName, event.toPrimitives()); this.emit(event.eventName, event); }); } diff --git a/src/workers/webdav/modules/shared/infrastructure/__mock__/WebdavIPC.ts b/src/workers/webdav/modules/shared/infrastructure/__mock__/WebdavIPC.ts new file mode 100644 index 000000000..5e7218f85 --- /dev/null +++ b/src/workers/webdav/modules/shared/infrastructure/__mock__/WebdavIPC.ts @@ -0,0 +1,44 @@ +import { + WebdavMainEvents, + WebDavProcessEvents, +} from 'shared/IPC/events/webdav'; +import { WebdavIpc } from '../../../../ipc'; + +export class WebdavIpcMock implements WebdavIpc { + sendMock = jest.fn(); + emitMock = jest.fn(); + onMock = jest.fn(); + onceMock = jest.fn(); + handleMock = jest.fn(); + + send(event: string, ...args: Array) { + return this.sendMock(event, ...args); + } + + emit(event: keyof WebDavProcessEvents): void { + return this.emitMock(event); + } + on(event: Event): void { + this.onMock(event); + } + once(event: Event): void { + this.onceMock(event); + } + + invoke( + event: Event, + ...args: never + ): Promise> { + throw new Error('Method not implemented.'); + } + + handle( + event: Event, + listener: ( + event: Electron.IpcMainEvent, + ...args: Parameters + ) => void + ): Promise> { + return this.handleMock(event, listener); + } +} diff --git a/src/workers/webdav/worker/InternxtFileSystem/InternxtFileSystem.ts b/src/workers/webdav/worker/InternxtFileSystem/InternxtFileSystem.ts index 84c0174d6..ee1ca7ebf 100644 --- a/src/workers/webdav/worker/InternxtFileSystem/InternxtFileSystem.ts +++ b/src/workers/webdav/worker/InternxtFileSystem/InternxtFileSystem.ts @@ -198,11 +198,6 @@ export class InternxtFileSystem extends FileSystem { .run(path.toString(false), ctx.estimatedSize) .then(({ stream }: { stream: Writable; upload: Promise }) => { callback(undefined, stream); - ipcRenderer.send('SYNC_INFO_UPDATE', { - action: 'PULLED', - kind: 'REMOTE', - name: path.fileName(), - }); }) .catch((error: Error) => { ipcRenderer.send('SYNC_INFO_UPDATE', { @@ -229,48 +224,12 @@ export class InternxtFileSystem extends FileSystem { name: path.fileName(), }); - this.container.fileDonwloader + this.container.fileDownloader .run(path.toString(false)) .then((remoteFileContents: RemoteFileContents) => { - const totalLength = ctx.estimatedSize; - let uploadedSize = 0; - remoteFileContents.stream.on('data', (chunk) => { - uploadedSize += chunk.length || 0; - ipcRenderer.send('SYNC_INFO_UPDATE', { - action: 'PULL', - kind: 'LOCAL', - progress: uploadedSize / totalLength, - name: path.fileName(), - }); - }); - remoteFileContents.stream.on('end', () => { - ipcRenderer.send('SYNC_INFO_UPDATE', { - action: 'PULLED', - kind: 'LOCAL', - name: path.fileName(), - }); - }); - remoteFileContents.stream.on('error', (err) => { - ipcRenderer.send('SYNC_INFO_UPDATE', { - action: 'PULL_ERROR', - kind: 'LOCAL', - name: path.fileName(), - errorName: err.name, - errorDetails: err.message, - process: 'SYNC', - }); - }); callback(undefined, remoteFileContents.stream); }) .catch((error: Error) => { - ipcRenderer.send('SYNC_INFO_UPDATE', { - action: 'PULL_ERROR', - kind: 'LOCAL', - name: path.fileName(), - errorName: error.name, - errorDetails: error.message, - process: 'SYNC', - }); handleFileSystemError(error, 'Download', 'File', ctx); }); } diff --git a/src/workers/webdav/worker/error-handling.ts b/src/workers/webdav/worker/error-handling.ts index 9248ab714..0779f09f9 100644 --- a/src/workers/webdav/worker/error-handling.ts +++ b/src/workers/webdav/worker/error-handling.ts @@ -1,6 +1,5 @@ import { RequestContext } from 'webdav-server/lib/index.v2'; -// import * as Sentry from '@sentry/electron/renderer'; -import Logger from 'electron-log'; +import * as Sentry from '@sentry/electron/renderer'; import { ipc } from '../ipc'; import { TrackedWebdavServerEvents, @@ -8,9 +7,8 @@ import { } from '../../../shared/IPC/events/webdav'; function handleError(error: Error, context: WebdavErrorContext): void { - Logger.error('[FS] Error: ', error); ipc.send('WEBDAV_ACTION_ERROR', error, context); - // Sentry.captureException(error, context); + Sentry.captureException(error); } export function handleFileSystemError( diff --git a/tsconfig.json b/tsconfig.json index d4e326203..7a0b3d492 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,10 @@ "compilerOptions": { "target": "ESNext", "module": "commonjs", - "lib": ["dom", "esnext"], + "lib": [ + "dom", + "esnext" + ], "declaration": true, "declarationMap": true, "jsx": "react-jsx", @@ -27,5 +30,11 @@ "allowJs": true, "outDir": "release/app/dist" }, - "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] -} + "exclude": [ + "test", + "release/build", + "release/app/dist", + ".erb/dll", + "__mock__" + ] +} \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json index 7462eaac7..a0d847717 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -6,5 +6,8 @@ "noImplicitReturns": true, "noImplicitAny": true, "noImplicitThis": true - } -} + }, + "exclude": [ + "__mock__" + ] +} \ No newline at end of file