diff --git a/src/workers/sync-engine/BindingManager.ts b/src/workers/sync-engine/BindingManager.ts index 03f78869b..818383110 100644 --- a/src/workers/sync-engine/BindingManager.ts +++ b/src/workers/sync-engine/BindingManager.ts @@ -1,37 +1,38 @@ -import { VirtualDrive } from 'virtual-drive'; import Logger from 'electron-log'; import { Folder } from './modules/folders/domain/Folder'; import { File } from './modules/files/domain/File'; +import { DependencyContainer } from './dependency-injection/DependencyContainer'; import { buildControllers } from './callbacks-controllers/buildControllers'; export class BindingsManager { private static readonly PROVIDER_NAME = 'Internxt'; constructor( - private readonly drive: VirtualDrive, - private readonly controllers: ReturnType, + private readonly container: DependencyContainer, private readonly paths: { root: string; icon: string; } ) {} - private createFolderPlaceholder(folder: Folder) { + public createFolderPlaceholder(folder: Folder) { // In order to create a folder placeholder it's path must en with / const folderPath = `${folder.path.value}/`; - this.drive.createItemByPath(folderPath, folder.uuid); + this.container.virtualDrive.createItemByPath(folderPath, folder.uuid); + } + public createFilePlaceholder(file: File) { + this.container.virtualDrive.createItemByPath( + file.path.value, + file.contentsId, + file.size + ); } public createPlaceHolders(items: Array) { items.forEach((item) => { if (item.isFile()) { - this.drive.createItemByPath( - item.path.value, - item.contentsId, - item.size - ); - return; + return this.createFilePlaceholder(item); } this.createFolderPlaceholder(item); @@ -40,17 +41,22 @@ export class BindingsManager { async start(version: string, providerId: string) { await this.stop(); + + const controllers = buildControllers(this.container); + const callbacks = { notifyDeleteCallback: ( contentsId: string, callback: (response: boolean) => void ) => { - this.controllers.deleteFile + controllers.delete .execute(contentsId) .then(() => { + Logger.debug('DELETE RESPONSE SUCCESSFUL'); callback(true); }) .catch((error: Error) => { + Logger.debug('DELETE RESPONSE NOT SUCCESSFUL'); Logger.error(error); callback(false); }); @@ -63,7 +69,7 @@ export class BindingsManager { contentsId: string, callback: (response: boolean) => void ) => { - this.controllers.renameOrMoveFile.execute( + controllers.renameOrMoveFile.execute( absolutePath, contentsId, callback @@ -73,13 +79,13 @@ export class BindingsManager { absolutePath: string, callback: (acknowledge: boolean, id: string) => void ) => { - this.controllers.addFile.execute(absolutePath, callback); + controllers.addFile.execute(absolutePath, callback); }, fetchDataCallback: ( contentsId: string, callback: (success: boolean, path: string) => void ) => { - this.controllers.downloadFile + controllers.downloadFile .execute(contentsId) .then((path: string) => { callback(true, path); @@ -121,7 +127,7 @@ export class BindingsManager { }, }; - await this.drive.registerSyncRoot( + await this.container.virtualDrive.registerSyncRoot( BindingsManager.PROVIDER_NAME, version, providerId, @@ -129,14 +135,14 @@ export class BindingsManager { this.paths.icon ); - await this.drive.connectSyncRoot(); + await this.container.virtualDrive.connectSyncRoot(); } watch() { - this.drive.watchAndWait(this.paths.root); + this.container.virtualDrive.watchAndWait(this.paths.root); } async stop() { - await this.drive.disconnectSyncRoot(); + await this.container.virtualDrive.disconnectSyncRoot(); } } diff --git a/src/workers/sync-engine/callbacks-controllers/buildControllers.ts b/src/workers/sync-engine/callbacks-controllers/buildControllers.ts index 215b3c843..06dea44d8 100644 --- a/src/workers/sync-engine/callbacks-controllers/buildControllers.ts +++ b/src/workers/sync-engine/callbacks-controllers/buildControllers.ts @@ -1,6 +1,6 @@ import { DependencyContainer } from '../dependency-injection/DependencyContainer'; import { AddFileController } from './controllers/AddFileController'; -import { DeleteFileController } from './controllers/DeleteFileController'; +import { DeleteController } from './controllers/DeleteController'; import { DownloadFileController } from './controllers/DownloadFileController'; import { RenameOrMoveController } from './controllers/RenameOrMoveController'; @@ -10,17 +10,23 @@ export function buildControllers(container: DependencyContainer) { container.filePathFromAbsolutePathCreator, container.fileCreator, container.fileDeleter, - container.fileByPartialSearcher + container.fileByPartialSearcher, + container.folderCreator, + container.folderFinder + ); + + const deleteController = new DeleteController( + container.fileDeleter, + container.folderDeleter ); const renameOrMoveFileController = new RenameOrMoveController( container.filePathFromAbsolutePathCreator, container.filePathUpdater, - container.fileDeleter + container.folderPathUpdater, + deleteController ); - const deleteFileController = new DeleteFileController(container.fileDeleter); - const downloadFileController = new DownloadFileController( container.fileFinderByContentsId, container.contentsDownloader, @@ -30,7 +36,7 @@ export function buildControllers(container: DependencyContainer) { return { addFile: addFileController, renameOrMoveFile: renameOrMoveFileController, - deleteFile: deleteFileController, + delete: deleteController, downloadFile: downloadFileController, } as const; } diff --git a/src/workers/sync-engine/callbacks-controllers/controllers/AddFileController.ts b/src/workers/sync-engine/callbacks-controllers/controllers/AddFileController.ts index 4d1a85939..9d50d611a 100644 --- a/src/workers/sync-engine/callbacks-controllers/controllers/AddFileController.ts +++ b/src/workers/sync-engine/callbacks-controllers/controllers/AddFileController.ts @@ -6,34 +6,49 @@ import { RetryContentsUploader } from '../../modules/contents/application/RetryC import { FileDeleter } from '../../modules/files/application/FileDeleter'; import { FileByPartialSearcher } from '../../modules/files/application/FileByPartialSearcher'; import { PlatformPathConverter } from '../../modules/shared/test/helpers/PlatformPathConverter'; +import { rawPathIsFolder } from '../helpers/rawPathIsFolder'; +import { FolderCreator } from '../../modules/folders/application/FolderCreator'; +import { MapObserver } from 'workers/sync-engine/modules/shared/domain/MapObserver'; +import { FolderFinder } from 'workers/sync-engine/modules/folders/application/FolderFinder'; -export type DehydrateAndCreatePlaceholder = ( - id: string, - relativePath: string, - size: number -) => void; +type Queue = Map void>; export class AddFileController extends CallbackController { + private readonly filesQueue: Queue; + private readonly foldersQueue: Queue; + + private readonly observer: MapObserver; + constructor( private readonly contentsUploader: RetryContentsUploader, private readonly filePathFromAbsolutePathCreator: FilePathFromAbsolutePathCreator, private readonly fileCreator: FileCreator, private readonly fileDeleter: FileDeleter, - private readonly searchByPartial: FileByPartialSearcher + private readonly searchByPartial: FileByPartialSearcher, + private readonly folderCreator: FolderCreator, + private readonly folderFinder: FolderFinder ) { super(); + + this.filesQueue = new Map(); + this.foldersQueue = new Map(); + + this.observer = new MapObserver(this.foldersQueue, this.createFiles); + this.observer.startObserving(); } - async execute( + private createFile = async ( absolutePath: string, callback: (acknowledge: boolean, id: string) => void - ): Promise { + ) => { try { const path = this.filePathFromAbsolutePathCreator.run(absolutePath); const file = this.searchByPartial.run({ path: PlatformPathConverter.winToPosix(path.value), }); + this.folderFinder.findFromFilePath(path); + const fileContents = await this.contentsUploader.run(absolutePath); if (file) { @@ -51,6 +66,53 @@ export class AddFileController extends CallbackController { } catch (error: unknown) { Logger.error('Error when adding a file: ', error); callback(false, ''); + } finally { + this.filesQueue.delete(absolutePath); + } + }; + + private createFiles = async () => { + for (const [absolutePath, callback] of this.filesQueue) { + await this.createFile(absolutePath, callback); + } + }; + + private createFolder = async ( + absolutePath: string, + callback: (acknowledge: boolean, id: string) => void + ) => { + Logger.info('Creating folder', absolutePath); + try { + const folder = await this.folderCreator.run(absolutePath); + callback(true, folder.uuid); + } catch (error: unknown) { + Logger.error('Error creating a folder: ', error); + callback(false, ''); + } finally { + this.foldersQueue.delete(absolutePath); + } + }; + + private createFolders = async () => { + for (const [absolutePath, callback] of this.foldersQueue) { + await this.createFolder(absolutePath, callback); + } + }; + + async execute( + absolutePath: string, + callback: (acknowledge: boolean, id: string) => void + ): Promise { + if (rawPathIsFolder(absolutePath)) { + this.foldersQueue.set(absolutePath, callback); + await this.createFolders(); + return; + } + + Logger.debug('File is going to be queued: ', absolutePath); + this.filesQueue.set(absolutePath, callback); + if (this.foldersQueue.size === 0) { + this.createFiles(); } } } diff --git a/src/workers/sync-engine/callbacks-controllers/controllers/CallbackController.ts b/src/workers/sync-engine/callbacks-controllers/controllers/CallbackController.ts index b30842f5d..2c382b8f6 100644 --- a/src/workers/sync-engine/callbacks-controllers/controllers/CallbackController.ts +++ b/src/workers/sync-engine/callbacks-controllers/controllers/CallbackController.ts @@ -6,4 +6,12 @@ export abstract class CallbackController { '' ); } + protected isContentsId(id: string): boolean { + // make sure the id is trimmed before comparing + // if it was already trimmed should not change its length + const trimmed = this.trim(id); + + // TODO: need a better way to detect if its a file or a folder + return trimmed.length === 24; + } } diff --git a/src/workers/sync-engine/callbacks-controllers/controllers/DeleteController.ts b/src/workers/sync-engine/callbacks-controllers/controllers/DeleteController.ts new file mode 100644 index 000000000..796c89cf1 --- /dev/null +++ b/src/workers/sync-engine/callbacks-controllers/controllers/DeleteController.ts @@ -0,0 +1,53 @@ +import { FolderDeleter } from 'workers/sync-engine/modules/folders/application/FolderDeleter'; +import { FileDeleter } from '../../modules/files/application/FileDeleter'; +import { CallbackController } from './CallbackController'; +import { DelayQueue } from 'workers/sync-engine/modules/shared/domain/DelayQueue'; + +export class DeleteController extends CallbackController { + private readonly filesQueue: DelayQueue; + private readonly foldersQueue: DelayQueue; + + constructor( + private readonly fileDeleter: FileDeleter, + private readonly folderDeleter: FolderDeleter + ) { + super(); + + const deleteFile = async (file: string) => { + await this.fileDeleter.run(file); + }; + + const deleteFolder = async (folder: string) => { + await this.folderDeleter.run(folder); + }; + + const canDeleteFolders = () => { + // Folders can always be deleted + return true; + }; + + this.foldersQueue = new DelayQueue( + 'folders', + deleteFolder, + canDeleteFolders + ); + + const canDeleteFiles = () => { + // Files cannot be deleted if there are folders on the queue + return this.foldersQueue.size === 0; + }; + + this.filesQueue = new DelayQueue('files', deleteFile, canDeleteFiles); + } + + async execute(contentsId: string) { + const trimmedId = this.trim(contentsId); + + if (this.isContentsId(trimmedId)) { + this.filesQueue.push(trimmedId); + return; + } + + this.foldersQueue.push(trimmedId); + } +} diff --git a/src/workers/sync-engine/callbacks-controllers/controllers/DeleteFileController.ts b/src/workers/sync-engine/callbacks-controllers/controllers/DeleteFileController.ts deleted file mode 100644 index aed4dd58d..000000000 --- a/src/workers/sync-engine/callbacks-controllers/controllers/DeleteFileController.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FileDeleter } from '../../modules/files/application/FileDeleter'; -import { CallbackController } from './CallbackController'; - -export class DeleteFileController extends CallbackController { - constructor(private readonly deleter: FileDeleter) { - super(); - } - - async execute(contentsId: string) { - const trimmedId = this.trim(contentsId); - - await this.deleter.run(trimmedId); - } -} diff --git a/src/workers/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts b/src/workers/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts index 5bd83e9a6..19f4b873e 100644 --- a/src/workers/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts +++ b/src/workers/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts @@ -1,13 +1,16 @@ -import { FileDeleter } from 'workers/sync-engine/modules/files/application/FileDeleter'; +import { FolderPathUpdater } from '../../modules/folders/application/FolderPathUpdater'; import { FilePathFromAbsolutePathCreator } from '../../modules/files/application/FilePathFromAbsolutePathCreator'; import { FilePathUpdater } from '../../modules/files/application/FilePathUpdater'; import { CallbackController } from './CallbackController'; +import { DeleteController } from './DeleteController'; +import Logger from 'electron-log'; export class RenameOrMoveController extends CallbackController { constructor( private readonly filePathFromAbsolutePathCreator: FilePathFromAbsolutePathCreator, private readonly filePathUpdater: FilePathUpdater, - private readonly fileDeleter: FileDeleter + private readonly folderPathUpdater: FolderPathUpdater, + private readonly deleteController: DeleteController ) { super(); } @@ -21,16 +24,21 @@ export class RenameOrMoveController extends CallbackController { try { if (absolutePath.startsWith('\\$Recycle.Bin')) { - await this.fileDeleter.run(trimmedId); - callback(true); - return; + await this.deleteController.execute(trimmedId); + return callback(true); } const relative = this.filePathFromAbsolutePathCreator.run(absolutePath); - await this.filePathUpdater.run(trimmedId, relative); + if (this.isContentsId(trimmedId)) { + await this.filePathUpdater.run(trimmedId, relative); + return callback(true); + } + + await this.folderPathUpdater.run(trimmedId, absolutePath); callback(true); } catch (error: unknown) { + Logger.error(error); callback(false); } } diff --git a/src/workers/sync-engine/callbacks-controllers/helpers/rawPathIsFolder.ts b/src/workers/sync-engine/callbacks-controllers/helpers/rawPathIsFolder.ts new file mode 100644 index 000000000..be91952c9 --- /dev/null +++ b/src/workers/sync-engine/callbacks-controllers/helpers/rawPathIsFolder.ts @@ -0,0 +1,3 @@ +export function rawPathIsFolder(raw: string) { + return raw.endsWith('\\') || raw.endsWith('/'); +} diff --git a/src/workers/sync-engine/dependency-injection/DependencyContainer.ts b/src/workers/sync-engine/dependency-injection/DependencyContainer.ts index ea5a9198f..bb0b364d7 100644 --- a/src/workers/sync-engine/dependency-injection/DependencyContainer.ts +++ b/src/workers/sync-engine/dependency-injection/DependencyContainer.ts @@ -1,21 +1,26 @@ /* eslint-disable max-len */ -import { FileSearcher } from '../modules/files/application/FileSearcher'; +import { VirtualDrive } from 'virtual-drive/dist'; +import { FileCreator } from '../modules/files/application/FileCreator'; +import { FilePathFromAbsolutePathCreator } from '../modules/files/application/FilePathFromAbsolutePathCreator'; import { FilePathUpdater } from '../modules/files/application/FilePathUpdater'; +import { FileSearcher } from '../modules/files/application/FileSearcher'; import { FolderSearcher } from '../modules/folders/application/FolderSearcher'; -import { FilePathFromAbsolutePathCreator } from '../modules/files/application/FilePathFromAbsolutePathCreator'; -import { ItemsContainer } from './items/ItemsContainer'; import { ContentsContainer } from './contents/ContentsContainer'; -import { FileCreator } from '../modules/files/application/FileCreator'; import { FilesContainer } from './files/FilesContainer'; +import { FoldersContainer } from './folders/FoldersContainer'; +import { ItemsContainer } from './items/ItemsContainer'; export interface DependencyContainer extends ItemsContainer, ContentsContainer, - FilesContainer { + FilesContainer, + FoldersContainer { fileCreator: FileCreator; filePathUpdater: FilePathUpdater; fileSearcher: FileSearcher; filePathFromAbsolutePathCreator: FilePathFromAbsolutePathCreator; folderSearcher: FolderSearcher; + + virtualDrive: VirtualDrive; } diff --git a/src/workers/sync-engine/dependency-injection/DependencyContainerFactory.ts b/src/workers/sync-engine/dependency-injection/DependencyContainerFactory.ts index c9fb3ec57..b8acb9206 100644 --- a/src/workers/sync-engine/dependency-injection/DependencyContainerFactory.ts +++ b/src/workers/sync-engine/dependency-injection/DependencyContainerFactory.ts @@ -1,31 +1,23 @@ import { getUser } from 'main/auth/service'; -import configStore from 'main/config'; import { getClients } from '../../../shared/HttpClient/backgroud-process-clients'; -import crypt from '../../utils/crypt'; -import { ipcRendererSyncEngine } from '../ipcRendererSyncEngine'; -import { FileCreator } from '../modules/files/application/FileCreator'; -import { FileFinderByContentsId } from '../modules/files/application/FileFinderByContentsId'; -import { FilePathFromAbsolutePathCreator } from '../modules/files/application/FilePathFromAbsolutePathCreator'; -import { FileSearcher } from '../modules/files/application/FileSearcher'; -import { FilePathUpdater } from '../modules/files/application/FilePathUpdater'; -import { HttpFileRepository } from '../modules/files/infrastructure/HttpFileRepository'; -import { FolderSearcher } from '../modules/folders/application/FolderSearcher'; -import { WebdavFolderDeleter } from '../modules/folders/application/WebdavFolderDeleter'; -import { WebdavFolderFinder } from '../modules/folders/application/WebdavFolderFinder'; -import { HttpFolderRepository } from '../modules/folders/infrastructure/HttpFolderRepository'; -import { Traverser } from '../modules/items/application/Traverser'; -import { NodeJsEventBus } from '../modules/shared/infrastructure/DuplexEventBus'; +import { DomainEventSubscribers } from '../modules/shared/infrastructure/DomainEventSubscribers'; import { DependencyContainer } from './DependencyContainer'; +import { DependencyInjectionEventBus } from './common/eventBus'; import { buildContentsContainer } from './contents/builder'; -import { buildItemsContainer } from './items/builder'; import { buildFilesContainer } from './files/builder'; +import { buildFoldersContainer } from './folders/builder'; +import { buildItemsContainer } from './items/builder'; +import { DependencyInjectionVirtualDrive } from './common/virtualDrive'; +import { buildPlaceholdersContainer } from './placeholders/builder'; export class DependencyContainerFactory { private static _container: DependencyContainer | undefined; - static readonly subscriptors: Array = []; + static readonly subscribers: Array = [ + 'createFilePlaceholderOnDeletionFailed', + ]; - eventSubscriptors( + eventSubscribers( key: keyof DependencyContainer ): DependencyContainer[keyof DependencyContainer] | undefined { if (!DependencyContainerFactory._container) return undefined; @@ -33,10 +25,6 @@ export class DependencyContainerFactory { return DependencyContainerFactory._container[key]; } - public get containter() { - return DependencyContainerFactory._container; - } - async build(): Promise { if (DependencyContainerFactory._container !== undefined) { return DependencyContainerFactory._container; @@ -49,67 +37,30 @@ export class DependencyContainerFactory { const clients = getClients(); - const localRootFolderPath = configStore.get('syncRoot'); - - const traverser = new Traverser(crypt, user.root_folder_id); - - const fileRepository = new HttpFileRepository( - crypt, - clients.drive, - clients.newDrive, - traverser, - user.bucket, - ipcRendererSyncEngine - ); - - const folderRepository = new HttpFolderRepository( - clients.drive, - clients.newDrive, - traverser, - ipcRendererSyncEngine - ); - - await fileRepository.init(); - await folderRepository.init(); + const { bus } = DependencyInjectionEventBus; + const { virtualDrive } = DependencyInjectionVirtualDrive; + const PlaceholderContainer = buildPlaceholdersContainer(); const itemsContainer = buildItemsContainer(); const contentsContainer = await buildContentsContainer(); - const filesContainer = await buildFilesContainer(); - - const eventBus = new NodeJsEventBus(); - - const folderFinder = new WebdavFolderFinder(folderRepository); - - const fileFinder = new FileFinderByContentsId(fileRepository); - - const filePathUpdater = new FilePathUpdater( - fileRepository, - fileFinder, - folderFinder + const foldersContainer = await buildFoldersContainer(PlaceholderContainer); + const { container: filesContainer } = await buildFilesContainer( + foldersContainer ); const container = { drive: clients.drive, newDrive: clients.newDrive, - fileCreator: new FileCreator(fileRepository, folderFinder, eventBus), - - filePathUpdater, - fileSearcher: new FileSearcher(fileRepository), - filePathFromAbsolutePathCreator: new FilePathFromAbsolutePathCreator( - localRootFolderPath - ), - - folderSearcher: new FolderSearcher(folderRepository), - folderFinder, - - folderDeleter: new WebdavFolderDeleter(folderRepository), - ...itemsContainer, ...contentsContainer, ...filesContainer, + ...foldersContainer, + + virtualDrive, }; + bus.addSubscribers(DomainEventSubscribers.from(container)); DependencyContainerFactory._container = container; return container; diff --git a/src/workers/sync-engine/dependency-injection/common/eventBus.ts b/src/workers/sync-engine/dependency-injection/common/eventBus.ts new file mode 100644 index 000000000..be4939af9 --- /dev/null +++ b/src/workers/sync-engine/dependency-injection/common/eventBus.ts @@ -0,0 +1,15 @@ +import { NodeJsEventBus } from 'workers/sync-engine/modules/shared/infrastructure/NodeJsEventBus'; + +export class DependencyInjectionEventBus { + private static _bus: NodeJsEventBus; + + static get bus(): NodeJsEventBus { + if (DependencyInjectionEventBus._bus) { + return DependencyInjectionEventBus._bus; + } + + DependencyInjectionEventBus._bus = new NodeJsEventBus(); + + return DependencyInjectionEventBus._bus; + } +} diff --git a/src/workers/sync-engine/dependency-injection/common/localRootFolderPath.ts b/src/workers/sync-engine/dependency-injection/common/localRootFolderPath.ts new file mode 100644 index 000000000..ee8a1bb95 --- /dev/null +++ b/src/workers/sync-engine/dependency-injection/common/localRootFolderPath.ts @@ -0,0 +1,15 @@ +import configStore from 'main/config'; + +export class DependencyInjectionLocalRootFolderPath { + private static path: string; + + static get(): string { + if (DependencyInjectionLocalRootFolderPath.path) { + return DependencyInjectionLocalRootFolderPath.path; + } + + DependencyInjectionLocalRootFolderPath.path = configStore.get('syncRoot'); + + return DependencyInjectionLocalRootFolderPath.path; + } +} diff --git a/src/workers/sync-engine/dependency-injection/common/virtualDrive.ts b/src/workers/sync-engine/dependency-injection/common/virtualDrive.ts new file mode 100644 index 000000000..e74b97645 --- /dev/null +++ b/src/workers/sync-engine/dependency-injection/common/virtualDrive.ts @@ -0,0 +1,20 @@ +import { VirtualDrive } from 'virtual-drive/dist'; +import { DependencyInjectionLocalRootFolderPath } from './localRootFolderPath'; + +export class DependencyInjectionVirtualDrive { + private static _vd: VirtualDrive; + + static get virtualDrive(): VirtualDrive { + if (DependencyInjectionVirtualDrive._vd) { + return DependencyInjectionVirtualDrive._vd; + } + + const root = DependencyInjectionLocalRootFolderPath.get(); + + const vd = new VirtualDrive(root); + + DependencyInjectionVirtualDrive._vd = vd; + + return DependencyInjectionVirtualDrive._vd; + } +} diff --git a/src/workers/sync-engine/dependency-injection/files/FilesContainer.ts b/src/workers/sync-engine/dependency-injection/files/FilesContainer.ts index 633cf7a90..54e105acd 100644 --- a/src/workers/sync-engine/dependency-injection/files/FilesContainer.ts +++ b/src/workers/sync-engine/dependency-injection/files/FilesContainer.ts @@ -1,6 +1,13 @@ +import { CreateFilePlaceholderOnDeletionFailed } from '../../modules/files/application/CreateFilePlaceholderOnDeletionFailed'; +import { CreateFilePlaceholderEmitter } from '../../modules/files/application/CreateFilePlaceholderEmitter'; import { FileByPartialSearcher } from '../../modules/files/application/FileByPartialSearcher'; +import { FileCreator } from '../../modules/files/application/FileCreator'; import { FileDeleter } from '../../modules/files/application/FileDeleter'; import { FileFinderByContentsId } from '../../modules/files/application/FileFinderByContentsId'; +import { FilePathFromAbsolutePathCreator } from '../../modules/files/application/FilePathFromAbsolutePathCreator'; +import { FilePathUpdater } from '../../modules/files/application/FilePathUpdater'; +import { FilePlaceholderCreatorFromContentsId } from '../../modules/files/application/FilePlaceholderCreatorFromContentsId'; +import { FileSearcher } from '../../modules/files/application/FileSearcher'; import { LocalRepositoryRepositoryRefresher } from '../../modules/files/application/LocalRepositoryRepositoryRefresher'; export interface FilesContainer { @@ -8,4 +15,11 @@ export interface FilesContainer { localRepositoryRefresher: LocalRepositoryRepositoryRefresher; fileDeleter: FileDeleter; fileByPartialSearcher: FileByPartialSearcher; + filePathUpdater: FilePathUpdater; + fileCreator: FileCreator; + filePathFromAbsolutePathCreator: FilePathFromAbsolutePathCreator; + fileSearcher: FileSearcher; + createFilePlaceholderEmitter: CreateFilePlaceholderEmitter; + filePlaceholderCreatorFromContentsId: FilePlaceholderCreatorFromContentsId; + createFilePlaceholderOnDeletionFailed: CreateFilePlaceholderOnDeletionFailed; } diff --git a/src/workers/sync-engine/dependency-injection/files/builder.ts b/src/workers/sync-engine/dependency-injection/files/builder.ts index 948df7709..f0b27c2a1 100644 --- a/src/workers/sync-engine/dependency-injection/files/builder.ts +++ b/src/workers/sync-engine/dependency-injection/files/builder.ts @@ -1,19 +1,39 @@ +import { CreateFilePlaceholderEmitter } from 'workers/sync-engine/modules/files/application/CreateFilePlaceholderEmitter'; import crypt from '../../../utils/crypt'; import { ipcRendererSyncEngine } from '../../ipcRendererSyncEngine'; import { FileByPartialSearcher } from '../../modules/files/application/FileByPartialSearcher'; +import { FileCreator } from '../../modules/files/application/FileCreator'; import { FileDeleter } from '../../modules/files/application/FileDeleter'; import { FileFinderByContentsId } from '../../modules/files/application/FileFinderByContentsId'; +import { FilePathFromAbsolutePathCreator } from '../../modules/files/application/FilePathFromAbsolutePathCreator'; +import { FilePathUpdater } from '../../modules/files/application/FilePathUpdater'; +import { FileSearcher } from '../../modules/files/application/FileSearcher'; import { LocalRepositoryRepositoryRefresher } from '../../modules/files/application/LocalRepositoryRepositoryRefresher'; import { HttpFileRepository } from '../../modules/files/infrastructure/HttpFileRepository'; import { DependencyInjectionHttpClientsProvider } from '../common/clients'; +import { DependencyInjectionEventBus } from '../common/eventBus'; +import { DependencyInjectionLocalRootFolderPath } from '../common/localRootFolderPath'; import { DependencyInjectionTraverserProvider } from '../common/traverser'; import { DependencyInjectionUserProvider } from '../common/user'; +import { FoldersContainer } from '../folders/FoldersContainer'; import { FilesContainer } from './FilesContainer'; +import { FilePlaceholderCreatorFromContentsId } from 'workers/sync-engine/modules/files/application/FilePlaceholderCreatorFromContentsId'; +import { FilePlaceholderCreator } from 'workers/sync-engine/modules/files/infrastructure/FilePlaceholderCreator'; +import { CreateFilePlaceholderOnDeletionFailed } from 'workers/sync-engine/modules/files/application/CreateFilePlaceholderOnDeletionFailed'; +import { DependencyInjectionVirtualDrive } from '../common/virtualDrive'; -export async function buildFilesContainer(): Promise { +export async function buildFilesContainer( + folderContainer: FoldersContainer +): Promise<{ + container: FilesContainer; + subscribers: any; +}> { const clients = DependencyInjectionHttpClientsProvider.get(); const traverser = DependencyInjectionTraverserProvider.get(); const user = DependencyInjectionUserProvider.get(); + const localRootFolderPath = DependencyInjectionLocalRootFolderPath.get(); + const { bus: eventBus } = DependencyInjectionEventBus; + const { virtualDrive } = DependencyInjectionVirtualDrive; const fileRepository = new HttpFileRepository( crypt, @@ -33,20 +53,65 @@ export async function buildFilesContainer(): Promise { fileRepository ); + const placeholderCreator = new FilePlaceholderCreator(virtualDrive); + const fileDeleter = new FileDeleter( fileRepository, fileFinderByContentsId, + folderContainer.parentFoldersExistForDeletion, + placeholderCreator, ipcRendererSyncEngine ); const fileByPartialSearcher = new FileByPartialSearcher(fileRepository); + const filePathUpdater = new FilePathUpdater( + fileRepository, + fileFinderByContentsId, + folderContainer.folderFinder + ); + + const fileCreator = new FileCreator( + fileRepository, + folderContainer.folderFinder, + eventBus + ); + + const filePathFromAbsolutePathCreator = new FilePathFromAbsolutePathCreator( + localRootFolderPath + ); + + const fileSearcher = new FileSearcher(fileRepository); + + const createFilePlaceholderEmitter = new CreateFilePlaceholderEmitter( + eventBus + ); + + const filePlaceholderCreatorFromContentsId = + new FilePlaceholderCreatorFromContentsId( + fileFinderByContentsId, + placeholderCreator + ); + + const createFilePlaceholderOnDeletionFailed = + new CreateFilePlaceholderOnDeletionFailed( + filePlaceholderCreatorFromContentsId + ); + const container: FilesContainer = { fileFinderByContentsId, localRepositoryRefresher: localRepositoryRefresher, fileDeleter, fileByPartialSearcher, + filePathUpdater, + fileCreator, + filePathFromAbsolutePathCreator, + fileSearcher, + createFilePlaceholderEmitter: createFilePlaceholderEmitter, + filePlaceholderCreatorFromContentsId: filePlaceholderCreatorFromContentsId, + createFilePlaceholderOnDeletionFailed: + createFilePlaceholderOnDeletionFailed, }; - return container; + return { container, subscribers: [] }; } diff --git a/src/workers/sync-engine/dependency-injection/folders/FoldersContainer.ts b/src/workers/sync-engine/dependency-injection/folders/FoldersContainer.ts new file mode 100644 index 000000000..790f0c4f8 --- /dev/null +++ b/src/workers/sync-engine/dependency-injection/folders/FoldersContainer.ts @@ -0,0 +1,17 @@ +import { FolderCreator } from '../../modules/folders/application/FolderCreator'; +import { FolderDeleter } from '../../modules/folders/application/FolderDeleter'; +import { FolderFinder } from '../../modules/folders/application/FolderFinder'; +import { FolderPathCreator } from '../../modules/folders/application/FolderPathCreator'; +import { FolderPathUpdater } from '../../modules/folders/application/FolderPathUpdater'; +import { FolderSearcher } from '../../modules/folders/application/FolderSearcher'; +import { ParentFoldersExistForDeletion } from '../../modules/folders/application/ParentFoldersExistForDeletion'; + +export interface FoldersContainer { + folderCreator: FolderCreator; + folderFinder: FolderFinder; + folderPathFromAbsolutePathCreator: FolderPathCreator; + folderSearcher: FolderSearcher; + folderDeleter: FolderDeleter; + parentFoldersExistForDeletion: ParentFoldersExistForDeletion; + folderPathUpdater: FolderPathUpdater; +} diff --git a/src/workers/sync-engine/dependency-injection/folders/builder.ts b/src/workers/sync-engine/dependency-injection/folders/builder.ts new file mode 100644 index 000000000..3af60d3ab --- /dev/null +++ b/src/workers/sync-engine/dependency-injection/folders/builder.ts @@ -0,0 +1,77 @@ +import { FolderCreator } from 'workers/sync-engine/modules/folders/application/FolderCreator'; +import { FolderDeleter } from 'workers/sync-engine/modules/folders/application/FolderDeleter'; +import { FolderFinder } from 'workers/sync-engine/modules/folders/application/FolderFinder'; +import { FolderPathCreator } from 'workers/sync-engine/modules/folders/application/FolderPathCreator'; +import { FolderSearcher } from 'workers/sync-engine/modules/folders/application/FolderSearcher'; +import { ParentFoldersExistForDeletion } from 'workers/sync-engine/modules/folders/application/ParentFoldersExistForDeletion'; +import { HttpFolderRepository } from 'workers/sync-engine/modules/folders/infrastructure/HttpFolderRepository'; +import { ipcRendererSyncEngine } from '../../ipcRendererSyncEngine'; +import { DependencyInjectionHttpClientsProvider } from '../common/clients'; +import { DependencyInjectionLocalRootFolderPath } from '../common/localRootFolderPath'; +import { DependencyInjectionTraverserProvider } from '../common/traverser'; +import { FoldersContainer } from './FoldersContainer'; +import { FolderPathUpdater } from 'workers/sync-engine/modules/folders/application/FolderPathUpdater'; +import { FolderMover } from 'workers/sync-engine/modules/folders/application/FolderMover'; +import { FolderRenamer } from 'workers/sync-engine/modules/folders/application/FolderRenamer'; +import { PlaceholderContainer } from '../placeholders/PlaceholdersContainer'; + +export async function buildFoldersContainer( + placeholdersContainer: PlaceholderContainer +): Promise { + const clients = DependencyInjectionHttpClientsProvider.get(); + const traverser = DependencyInjectionTraverserProvider.get(); + const rootFolderPath = DependencyInjectionLocalRootFolderPath.get(); + + const repository = new HttpFolderRepository( + clients.drive, + clients.newDrive, + traverser, + ipcRendererSyncEngine + ); + + await repository.init(); + const folderPathFromAbsolutePathCreator = new FolderPathCreator( + rootFolderPath + ); + + const folderFinder = new FolderFinder(repository); + + const folderSearcher = new FolderSearcher(repository); + + const parentFoldersExistForDeletion = new ParentFoldersExistForDeletion( + repository + ); + + const folderDeleter = new FolderDeleter( + repository, + parentFoldersExistForDeletion, + placeholdersContainer.placeholderCreator + ); + + const folderCreator = new FolderCreator( + folderPathFromAbsolutePathCreator, + repository, + folderFinder, + ipcRendererSyncEngine + ); + + const folderMover = new FolderMover(repository, folderFinder); + const folderRenamer = new FolderRenamer(repository, ipcRendererSyncEngine); + + const folderPathUpdater = new FolderPathUpdater( + repository, + folderPathFromAbsolutePathCreator, + folderMover, + folderRenamer + ); + + return { + folderCreator, + folderFinder, + folderPathFromAbsolutePathCreator, + folderSearcher, + folderDeleter, + parentFoldersExistForDeletion, + folderPathUpdater, + }; +} diff --git a/src/workers/sync-engine/dependency-injection/placeholders/PlaceholdersContainer.ts b/src/workers/sync-engine/dependency-injection/placeholders/PlaceholdersContainer.ts new file mode 100644 index 000000000..4409d8121 --- /dev/null +++ b/src/workers/sync-engine/dependency-injection/placeholders/PlaceholdersContainer.ts @@ -0,0 +1,5 @@ +import { PlaceholderCreator } from '../../modules/placeholders/domain/PlaceholderCreator'; + +export interface PlaceholderContainer { + placeholderCreator: PlaceholderCreator; +} diff --git a/src/workers/sync-engine/dependency-injection/placeholders/builder.ts b/src/workers/sync-engine/dependency-injection/placeholders/builder.ts new file mode 100644 index 000000000..eb44e9aca --- /dev/null +++ b/src/workers/sync-engine/dependency-injection/placeholders/builder.ts @@ -0,0 +1,11 @@ +import { DependencyInjectionVirtualDrive } from '../common/virtualDrive'; +import { PlaceholderContainer } from './PlaceholdersContainer'; +import { VirtualDrivePlaceholderCreator } from 'workers/sync-engine/modules/placeholders/infrastructure/VirtualDrivePlaceholderCreator'; + +export function buildPlaceholdersContainer(): PlaceholderContainer { + const { virtualDrive } = DependencyInjectionVirtualDrive; + + const placeholderCreator = new VirtualDrivePlaceholderCreator(virtualDrive); + + return { placeholderCreator }; +} diff --git a/src/workers/sync-engine/index.ts b/src/workers/sync-engine/index.ts index 43b0a1eb0..1e015dc20 100644 --- a/src/workers/sync-engine/index.ts +++ b/src/workers/sync-engine/index.ts @@ -4,7 +4,6 @@ import { DependencyContainerFactory } from './dependency-injection/DependencyCon import packageJson from '../../../package.json'; import { BindingsManager } from './BindingManager'; import fs from 'fs/promises'; -import { buildControllers } from './callbacks-controllers/buildControllers'; import { iconPath } from 'workers/utils/icon'; async function ensureTheFolderExist(path: string) { @@ -19,26 +18,16 @@ async function ensureTheFolderExist(path: string) { async function setUp() { Logger.info('[SYNC ENGINE] Starting sync engine process'); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { VirtualDrive } = require('virtual-drive/dist'); - const virtualDrivePath = await ipcRenderer.invoke('get-virtual-drive-root'); - Logger.info( - '[SYNC ENGINE] Going to create root sync folder on: ', - virtualDrivePath - ); + Logger.info('[SYNC ENGINE] Going to use root folder: ', virtualDrivePath); await ensureTheFolderExist(virtualDrivePath); - const virtualDrive = new VirtualDrive(virtualDrivePath); - const factory = new DependencyContainerFactory(); const container = await factory.build(); - const controllers = buildControllers(container); - - const bindings = new BindingsManager(virtualDrive, controllers, { + const bindings = new BindingsManager(container, { root: virtualDrivePath, icon: iconPath, }); diff --git a/src/workers/sync-engine/modules/contents/application/ContentsDownloader.ts b/src/workers/sync-engine/modules/contents/application/ContentsDownloader.ts index 04c2cc27b..5c667a9ac 100644 --- a/src/workers/sync-engine/modules/contents/application/ContentsDownloader.ts +++ b/src/workers/sync-engine/modules/contents/application/ContentsDownloader.ts @@ -4,7 +4,7 @@ import { ContentFileDownloader } from '../domain/contentHandlers/ContentFileDown import { File } from '../../files/domain/File'; import { LocalFileContents } from '../domain/LocalFileContents'; import { LocalFileWriter } from '../domain/LocalFileWriter'; -import { Stopwatch } from 'shared/types/Stopwatch'; +import { Stopwatch } from '../../../../../shared/types/Stopwatch'; import Logger from 'electron-log'; export class ContentsDownloader { diff --git a/src/workers/sync-engine/modules/files/application/CreateFilePlaceholderEmitter.ts b/src/workers/sync-engine/modules/files/application/CreateFilePlaceholderEmitter.ts new file mode 100644 index 000000000..f9453f865 --- /dev/null +++ b/src/workers/sync-engine/modules/files/application/CreateFilePlaceholderEmitter.ts @@ -0,0 +1,12 @@ +import { NodeJsEventBus } from '../../shared/infrastructure/NodeJsEventBus'; +import { File } from '../domain/File'; + +export class CreateFilePlaceholderEmitter { + private static readonly EVENT_NAME = 'PLACEHOLDER:CREATE:FILE'; + + constructor(private readonly eventBus: NodeJsEventBus) {} + + emit(contentsId: File['contentsId']) { + this.eventBus.emit(CreateFilePlaceholderEmitter.EVENT_NAME, { contentsId }); + } +} diff --git a/src/workers/sync-engine/modules/files/application/CreateFilePlaceholderOnDeletionFailed.ts b/src/workers/sync-engine/modules/files/application/CreateFilePlaceholderOnDeletionFailed.ts new file mode 100644 index 000000000..d434dee54 --- /dev/null +++ b/src/workers/sync-engine/modules/files/application/CreateFilePlaceholderOnDeletionFailed.ts @@ -0,0 +1,18 @@ +import { DomainEventClass } from '../../shared/domain/DomainEvent'; +import { WebdavDomainEventSubscriber } from '../../shared/domain/WebdavDomainEventSubscriber'; +import { OptimisticFileDeletionFailed } from '../domain/events/OptimisticFileDeletionFailed'; +import { FilePlaceholderCreatorFromContentsId } from './FilePlaceholderCreatorFromContentsId'; + +export class CreateFilePlaceholderOnDeletionFailed + implements WebdavDomainEventSubscriber +{ + constructor(private readonly creator: FilePlaceholderCreatorFromContentsId) {} + + subscribedTo(): DomainEventClass[] { + return [OptimisticFileDeletionFailed]; + } + + async on(domainEvent: OptimisticFileDeletionFailed): Promise { + this.creator.run(domainEvent.toPrimitives().contentsId); + } +} diff --git a/src/workers/sync-engine/modules/files/application/FileCreator.ts b/src/workers/sync-engine/modules/files/application/FileCreator.ts index 9f69238be..a771ebac5 100644 --- a/src/workers/sync-engine/modules/files/application/FileCreator.ts +++ b/src/workers/sync-engine/modules/files/application/FileCreator.ts @@ -1,16 +1,16 @@ -import { WebdavFolderFinder } from '../../folders/application/WebdavFolderFinder'; +import { FolderFinder } from '../../folders/application/FolderFinder'; import { FilePath } from '../domain/FilePath'; import { File } from '../domain/File'; import { FileRepository } from '../domain/FileRepository'; import { FileSize } from '../domain/FileSize'; -import { WebdavServerEventBus } from '../../shared/domain/WebdavServerEventBus'; +import { EventBus } from '../../shared/domain/WebdavServerEventBus'; import { RemoteFileContents } from '../../contents/domain/RemoteFileContents'; export class FileCreator { constructor( private readonly repository: FileRepository, - private readonly folderFinder: WebdavFolderFinder, - private readonly eventBus: WebdavServerEventBus + private readonly folderFinder: FolderFinder, + private readonly eventBus: EventBus ) {} async run( diff --git a/src/workers/sync-engine/modules/files/application/FileDeleter.ts b/src/workers/sync-engine/modules/files/application/FileDeleter.ts index b153b24d4..f5d09e6bc 100644 --- a/src/workers/sync-engine/modules/files/application/FileDeleter.ts +++ b/src/workers/sync-engine/modules/files/application/FileDeleter.ts @@ -1,27 +1,40 @@ -import { FileRepository } from '../domain/FileRepository'; import Logger from 'electron-log'; -import { FileFinderByContentsId } from './FileFinderByContentsId'; -import { FileStatuses } from '../domain/FileStatus'; import { SyncEngineIpc } from '../../../ipcRendererSyncEngine'; +import { ParentFoldersExistForDeletion } from '../../folders/application/ParentFoldersExistForDeletion'; +import { FileRepository } from '../domain/FileRepository'; +import { FileStatuses } from '../domain/FileStatus'; +import { FileFinderByContentsId } from './FileFinderByContentsId'; +import { FilePlaceholderCreator } from '../infrastructure/FilePlaceholderCreator'; export class FileDeleter { constructor( private readonly repository: FileRepository, private readonly fileFinder: FileFinderByContentsId, + private readonly parentFoldersExistForDeletion: ParentFoldersExistForDeletion, + // TODO: don't import it directly from infrastructure + private readonly filePlaceholderCreator: FilePlaceholderCreator, private readonly ipc: SyncEngineIpc ) {} async run(contentsId: string): Promise { const file = this.fileFinder.run(contentsId); - Logger.debug('FILE TO BE DELETED, ', file.nameWithExtension); - if (file.status.is(FileStatuses.TRASHED)) { - // TODO: Solve file deleter being called twice Logger.warn(`File ${file.path.value} is already trashed. Will ignore...`); return; } + const allParentsExists = this.parentFoldersExistForDeletion.run( + file.folderId + ); + + if (!allParentsExists) { + Logger.warn( + `Skipped file deletion for ${file.path.value}. A folder in a higher level is already marked as trashed` + ); + return; + } + this.ipc.send('DELETING_FILE', { name: file.name, extension: file.type, @@ -29,17 +42,33 @@ export class FileDeleter { size: file.size, }); - file.trash(); + try { + file.trash(); - await this.repository.delete(file); + await this.repository.delete(file); - Logger.debug('FILE DELETED, ', file.nameWithExtension); + this.ipc.send('FILE_DELETED', { + name: file.name, + extension: file.type, + nameWithExtension: file.nameWithExtension, + size: file.size, + }); + } catch (error: unknown) { + Logger.error( + `Error deleting the file ${file.nameWithExtension}: `, + error + ); - this.ipc.send('FILE_DELETED', { - name: file.name, - extension: file.type, - nameWithExtension: file.nameWithExtension, - size: file.size, - }); + const message = error instanceof Error ? error.message : 'Unknown error'; + + this.ipc.send('FILE_DELETION_ERROR', { + name: file.name, + extension: file.type, + nameWithExtension: file.nameWithExtension, + error: message, + }); + + this.filePlaceholderCreator.run(file); + } } } diff --git a/src/workers/sync-engine/modules/files/application/FilePathUpdater.ts b/src/workers/sync-engine/modules/files/application/FilePathUpdater.ts index 6cc650cb4..3725195f4 100644 --- a/src/workers/sync-engine/modules/files/application/FilePathUpdater.ts +++ b/src/workers/sync-engine/modules/files/application/FilePathUpdater.ts @@ -3,14 +3,14 @@ import { FileAlreadyExistsError } from '../domain/errors/FileAlreadyExistsError' import { FilePath } from '../domain/FilePath'; import { File } from '../domain/File'; import { FileRepository } from '../domain/FileRepository'; -import { WebdavFolderFinder } from '../../folders/application/WebdavFolderFinder'; +import { FolderFinder } from '../../folders/application/FolderFinder'; import { FileFinderByContentsId } from './FileFinderByContentsId'; export class FilePathUpdater { constructor( private readonly repository: FileRepository, private readonly fileFinderByContentsId: FileFinderByContentsId, - private readonly folderFinder: WebdavFolderFinder + private readonly folderFinder: FolderFinder ) {} private async rename(file: File, path: FilePath) { diff --git a/src/workers/sync-engine/modules/files/application/FilePlaceholderCreatorFromContentsId.ts b/src/workers/sync-engine/modules/files/application/FilePlaceholderCreatorFromContentsId.ts new file mode 100644 index 000000000..a59762f74 --- /dev/null +++ b/src/workers/sync-engine/modules/files/application/FilePlaceholderCreatorFromContentsId.ts @@ -0,0 +1,16 @@ +import { File } from '../domain/File'; +import { FilePlaceholderCreator } from '../infrastructure/FilePlaceholderCreator'; +import { FileFinderByContentsId } from './FileFinderByContentsId'; + +export class FilePlaceholderCreatorFromContentsId { + constructor( + private readonly finder: FileFinderByContentsId, + private readonly placeholderCreator: FilePlaceholderCreator + ) {} + + run(contentsId: File['contentsId']) { + const file = this.finder.run(contentsId); + + this.placeholderCreator.run(file); + } +} diff --git a/src/workers/sync-engine/modules/files/domain/events/OptimisticFileDeletionFailed.ts b/src/workers/sync-engine/modules/files/domain/events/OptimisticFileDeletionFailed.ts new file mode 100644 index 000000000..41ccd25f4 --- /dev/null +++ b/src/workers/sync-engine/modules/files/domain/events/OptimisticFileDeletionFailed.ts @@ -0,0 +1,18 @@ +import { DomainEvent } from '../../../../modules/shared/domain/DomainEvent'; + +export class OptimisticFileDeletionFailed extends DomainEvent { + static readonly EVENT_NAME = 'file.deletion.failed'; + + constructor({ aggregateId }: { aggregateId: string }) { + super({ + eventName: OptimisticFileDeletionFailed.EVENT_NAME, + aggregateId, + }); + } + + toPrimitives() { + return { + contentsId: this.aggregateId, + }; + } +} diff --git a/src/workers/sync-engine/modules/files/infrastructure/FilePlaceholderCreator.ts b/src/workers/sync-engine/modules/files/infrastructure/FilePlaceholderCreator.ts new file mode 100644 index 000000000..66712ed08 --- /dev/null +++ b/src/workers/sync-engine/modules/files/infrastructure/FilePlaceholderCreator.ts @@ -0,0 +1,10 @@ +import { VirtualDrive } from 'virtual-drive/dist'; +import { File } from '../domain/File'; + +export class FilePlaceholderCreator { + constructor(private readonly drive: VirtualDrive) {} + + run(file: File): void { + this.drive.createItemByPath(file.path.value, file.contentsId, file.size); + } +} diff --git a/src/workers/sync-engine/modules/files/infrastructure/HttpFileRepository.ts b/src/workers/sync-engine/modules/files/infrastructure/HttpFileRepository.ts index 256b028f6..3c45a5175 100644 --- a/src/workers/sync-engine/modules/files/infrastructure/HttpFileRepository.ts +++ b/src/workers/sync-engine/modules/files/infrastructure/HttpFileRepository.ts @@ -15,7 +15,6 @@ import { RemoteItemsGenerator } from '../../items/application/RemoteItemsGenerat import { FileStatuses } from '../domain/FileStatus'; import { Crypt } from '../../shared/domain/Crypt'; import { SyncEngineIpc } from '../../../ipcRendererSyncEngine'; -import Logger from 'electron-log'; export class HttpFileRepository implements FileRepository { public files: Record = {}; @@ -75,7 +74,6 @@ export class HttpFileRepository implements FileRepository { const keys = Object.keys(partial) as Array>; const file = Object.values(this.files).find((file) => { - Logger.debug(file.attributes()[keys[0]], partial[keys[0]]); return keys.every((key) => file.attributes()[key] === partial[key]); }); diff --git a/src/workers/sync-engine/modules/files/test/application/FileCreator.test.ts b/src/workers/sync-engine/modules/files/test/application/FileCreator.test.ts index 0780c3fb5..728976dd0 100644 --- a/src/workers/sync-engine/modules/files/test/application/FileCreator.test.ts +++ b/src/workers/sync-engine/modules/files/test/application/FileCreator.test.ts @@ -1,6 +1,6 @@ import { FolderMother } from '../../../folders/test/domain/FolderMother'; import { FolderRepositoryMock } from '../../../folders/test/__mocks__/FolderRepositoryMock'; -import { WebdavFolderFinder } from '../../../folders/application/WebdavFolderFinder'; +import { FolderFinder } from '../../../folders/application/FolderFinder'; import { FileCreator } from '../../application/FileCreator'; import { FileRepositoryMock } from '../__mocks__/FileRepositoryMock'; import { EventBusMock } from '../../../shared/test/__mock__/EventBusMock'; @@ -17,7 +17,7 @@ describe('File Creator', () => { beforeEach(() => { fileReposiotry = new FileRepositoryMock(); folderRepository = new FolderRepositoryMock(); - const folderFinder = new WebdavFolderFinder(folderRepository); + const folderFinder = new FolderFinder(folderRepository); eventBus = new EventBusMock(); SUT = new FileCreator(fileReposiotry, folderFinder, eventBus); diff --git a/src/workers/sync-engine/modules/files/test/application/FilePathUpdater.test.ts b/src/workers/sync-engine/modules/files/test/application/FilePathUpdater.test.ts index bff1c568c..59f315a38 100644 --- a/src/workers/sync-engine/modules/files/test/application/FilePathUpdater.test.ts +++ b/src/workers/sync-engine/modules/files/test/application/FilePathUpdater.test.ts @@ -2,7 +2,7 @@ import { FilePathUpdater } from '../../application/FilePathUpdater'; import { FilePath } from '../../domain/FilePath'; import { FileMother } from '../domain/FileMother'; import { FileRepositoryMock } from '../__mocks__/FileRepositoryMock'; -import { WebdavFolderFinder } from '../../../folders/application/WebdavFolderFinder'; +import { FolderFinder } from '../../../folders/application/FolderFinder'; import { FolderFinderMock } from '../../../folders/test/__mocks__/FolderFinderMock'; import { FileFinderByContentsId } from '../../application/FileFinderByContentsId'; @@ -20,7 +20,7 @@ describe('File path updater', () => { SUT = new FilePathUpdater( repository, fileFinderByContentsId, - folderFinder as unknown as WebdavFolderFinder + folderFinder as unknown as FolderFinder ); }); diff --git a/src/workers/sync-engine/modules/files/test/infrastructure/HttpFileRepository.test.ts b/src/workers/sync-engine/modules/files/test/infrastructure/HttpFileRepository.test.ts index d482647e0..1a1be8894 100644 --- a/src/workers/sync-engine/modules/files/test/infrastructure/HttpFileRepository.test.ts +++ b/src/workers/sync-engine/modules/files/test/infrastructure/HttpFileRepository.test.ts @@ -41,7 +41,7 @@ describe('Http File Repository', () => { describe('Rename', () => { it('after a file is renamed cannot be found ', async () => { - const files = ['a', 'b', 'c', 'd'].map((char: string) => + const originalFiles = ['a', 'b', 'c', 'd'].map((char: string) => ServerFileMother.fromPartial({ name: char, folderId: rootFolderId, @@ -50,10 +50,24 @@ describe('Http File Repository', () => { }) ); - ipc.onInvokeMock.mockResolvedValueOnce({ - folders: [rootFolder], - files: files, - }); + const resultFiles = ['aa', 'b', 'c', 'd'].map((char: string) => + ServerFileMother.fromPartial({ + name: char, + folderId: rootFolderId, + fileId: chance.string({ length: 24 }), + type: '', + }) + ); + + ipc.onInvokeMock + .mockResolvedValueOnce({ + folders: [rootFolder], + files: originalFiles, + }) + .mockResolvedValueOnce({ + folders: [rootFolder], + files: resultFiles, + }); axios.post = jest.fn().mockResolvedValueOnce({ status: 200, data: {} }); diff --git a/src/workers/sync-engine/modules/folders/application/FolderCreator.ts b/src/workers/sync-engine/modules/folders/application/FolderCreator.ts new file mode 100644 index 000000000..5e7549097 --- /dev/null +++ b/src/workers/sync-engine/modules/folders/application/FolderCreator.ts @@ -0,0 +1,36 @@ +import { SyncEngineIpc } from '../../../ipcRendererSyncEngine'; +import { PlatformPathConverter } from '../../shared/test/helpers/PlatformPathConverter'; +import { Folder } from '../domain/Folder'; +import { FolderRepository } from '../domain/FolderRepository'; +import { FolderFinder } from './FolderFinder'; +import { FolderPathCreator } from './FolderPathCreator'; + +export class FolderCreator { + constructor( + private readonly folderPathFromAbsolutePathCreator: FolderPathCreator, + private readonly repository: FolderRepository, + private readonly folderFinder: FolderFinder, + private readonly ipc: SyncEngineIpc + ) {} + + async run(absolutePath: string): Promise { + const folderPath = this.folderPathFromAbsolutePathCreator.fromAbsolute( + PlatformPathConverter.winToPosix(absolutePath) + ); + this.ipc.send('CREATING_FOLDER', { + name: folderPath.name(), + }); + + const parent = this.folderFinder.run( + PlatformPathConverter.winToPosix(folderPath.dirname()) + ); + + const folder = await this.repository.create(folderPath, parent.id); + + this.ipc.send('FOLDER_CREATED', { + name: folderPath.name(), + }); + + return folder; + } +} diff --git a/src/workers/sync-engine/modules/folders/application/FolderDeleter.ts b/src/workers/sync-engine/modules/folders/application/FolderDeleter.ts new file mode 100644 index 000000000..ff38c44de --- /dev/null +++ b/src/workers/sync-engine/modules/folders/application/FolderDeleter.ts @@ -0,0 +1,48 @@ +import Logger from 'electron-log'; +import { Folder } from '../domain/Folder'; +import { FolderRepository } from '../domain/FolderRepository'; +import { ActionNotPermittedError } from '../domain/errors/ActionNotPermittedError'; +import { FolderNotFoundError } from '../domain/errors/FolderNotFoundError'; +import { ParentFoldersExistForDeletion } from './ParentFoldersExistForDeletion'; +import { PlaceholderCreator } from '../../placeholders/domain/PlaceholderCreator'; + +export class FolderDeleter { + constructor( + private readonly repository: FolderRepository, + private readonly parentFoldersExistForDeletion: ParentFoldersExistForDeletion, + private readonly placeholderCreator: PlaceholderCreator + ) {} + + async run(uuid: Folder['uuid']): Promise { + const folder = this.repository.searchByPartial({ uuid }); + + if (!folder) { + throw new FolderNotFoundError(uuid); + } + + try { + if (!folder.parentId) { + throw new ActionNotPermittedError('Trash root folder'); + } + + const allParentsExists = this.parentFoldersExistForDeletion.run( + // TODO: Create a new aggregate root for root folder so the rest have the parent Id as number + folder.parentId as number + ); + + if (!allParentsExists) { + Logger.warn( + `Skipped folder deletion for ${folder.path.value}. A folder in a higher level is already marked as trashed` + ); + return; + } + + folder.trash(); + await this.repository.trash(folder); + } catch (error: unknown) { + Logger.error(`Error deleting the folder ${folder.name}: `, error); + + this.placeholderCreator.folder(folder); + } + } +} diff --git a/src/workers/sync-engine/modules/folders/application/WebdavFolderFinder.ts b/src/workers/sync-engine/modules/folders/application/FolderFinder.ts similarity index 95% rename from src/workers/sync-engine/modules/folders/application/WebdavFolderFinder.ts rename to src/workers/sync-engine/modules/folders/application/FolderFinder.ts index d0f9f03de..b59ec3fae 100644 --- a/src/workers/sync-engine/modules/folders/application/WebdavFolderFinder.ts +++ b/src/workers/sync-engine/modules/folders/application/FolderFinder.ts @@ -3,7 +3,7 @@ import { FolderNotFoundError } from '../domain/errors/FolderNotFoundError'; import { Folder } from '../domain/Folder'; import { FolderRepository } from '../domain/FolderRepository'; -export class WebdavFolderFinder { +export class FolderFinder { constructor(private readonly repository: FolderRepository) {} run(path: string): Folder { diff --git a/src/workers/sync-engine/modules/folders/application/WebdavFolderMover.ts b/src/workers/sync-engine/modules/folders/application/FolderMover.ts similarity index 52% rename from src/workers/sync-engine/modules/folders/application/WebdavFolderMover.ts rename to src/workers/sync-engine/modules/folders/application/FolderMover.ts index 9dff57cbd..3791cfa41 100644 --- a/src/workers/sync-engine/modules/folders/application/WebdavFolderMover.ts +++ b/src/workers/sync-engine/modules/folders/application/FolderMover.ts @@ -1,15 +1,13 @@ -import { ActionNotPermitedError } from '../domain/errors/ActionNotPermitedError'; +import { ActionNotPermittedError } from '../domain/errors/ActionNotPermittedError'; import { FolderPath } from '../domain/FolderPath'; import { Folder } from '../domain/Folder'; import { FolderRepository } from '../domain/FolderRepository'; -import { WebdavFolderFinder } from './WebdavFolderFinder'; -import { WebdavFolderRenamer } from './WebdavFolderRenamer'; +import { FolderFinder } from './FolderFinder'; -export class WebdavFolderMover { +export class FolderMover { constructor( private readonly repository: FolderRepository, - private readonly folderFinder: WebdavFolderFinder, - private readonly folderRenamer: WebdavFolderRenamer + private readonly folderFinder: FolderFinder ) {} private async move(folder: Folder, parentFolder: Folder) { @@ -18,23 +16,17 @@ export class WebdavFolderMover { await this.repository.updateParentDir(folder); } - async run(folder: Folder, to: string): Promise { - const destination = new FolderPath(to); + async run(folder: Folder, destination: FolderPath): Promise { const resultFolder = this.repository.search(destination.value); const shouldBeMerge = resultFolder !== undefined; if (shouldBeMerge) { - throw new ActionNotPermitedError('overwrite'); + throw new ActionNotPermittedError('overwrite'); } const destinationFolder = this.folderFinder.run(destination.dirname()); - if (folder.isIn(destinationFolder)) { - await this.folderRenamer.run(folder, to); - return; - } - await this.move(folder, destinationFolder); } } diff --git a/src/workers/sync-engine/modules/folders/application/FolderPathCreator.ts b/src/workers/sync-engine/modules/folders/application/FolderPathCreator.ts new file mode 100644 index 000000000..fca948223 --- /dev/null +++ b/src/workers/sync-engine/modules/folders/application/FolderPathCreator.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { FolderPath } from '../domain/FolderPath'; + +export class FolderPathCreator { + constructor(private readonly baseFolder: string) {} + + private calculateRelativePath(basePath: string, folderPath: string): string { + const relativePath = path.relative(basePath, folderPath); + const relativeFolders = path.dirname(relativePath); + const fileName = path.basename(folderPath); + + return path.join(relativeFolders, fileName); + } + + fromAbsolute(absolutePath: string): FolderPath { + // TODO: path.normalize can be better fit + const sanitized = absolutePath.replace('\\\\', '\\'); + const relative = this.calculateRelativePath(this.baseFolder, sanitized); + + const withSlash = path.sep + relative; + return new FolderPath(withSlash); + } +} diff --git a/src/workers/sync-engine/modules/folders/application/FolderPathUpdater.ts b/src/workers/sync-engine/modules/folders/application/FolderPathUpdater.ts new file mode 100644 index 000000000..a55d71986 --- /dev/null +++ b/src/workers/sync-engine/modules/folders/application/FolderPathUpdater.ts @@ -0,0 +1,46 @@ +import path from 'path'; +import { Folder } from '../domain/Folder'; +import { FolderPathCreator } from './FolderPathCreator'; +import { FolderRepository } from '../domain/FolderRepository'; +import { FolderNotFoundError } from '../domain/errors/FolderNotFoundError'; +import { ActionNotPermittedError } from '../domain/errors/ActionNotPermittedError'; +import { FolderMover } from './FolderMover'; +import { FolderRenamer } from './FolderRenamer'; + +export class FolderPathUpdater { + constructor( + private readonly repository: FolderRepository, + private readonly pathCreator: FolderPathCreator, + private readonly folderMover: FolderMover, + private readonly folderRenamer: FolderRenamer + ) {} + + async run(uuid: Folder['uuid'], absolutePath: string) { + const normalized = path.normalize(absolutePath); + + const folder = this.repository.searchByPartial({ uuid }); + + if (!folder) { + throw new FolderNotFoundError(uuid); + } + + const desiredPath = this.pathCreator.fromAbsolute(normalized); + + const dirnameChanged = folder.dirname !== desiredPath.dirname(); + const nameChanged = folder.name !== desiredPath.name(); + + if (dirnameChanged && nameChanged) { + throw new ActionNotPermittedError('Move and rename (at the same time)'); + } + + if (dirnameChanged) { + return await this.folderMover.run(folder, desiredPath); + } + + if (nameChanged) { + return await this.folderRenamer.run(folder, desiredPath); + } + + throw new Error('No path change detected for folder path update'); + } +} diff --git a/src/workers/sync-engine/modules/folders/application/WebdavFolderRenamer.ts b/src/workers/sync-engine/modules/folders/application/FolderRenamer.ts similarity index 71% rename from src/workers/sync-engine/modules/folders/application/WebdavFolderRenamer.ts rename to src/workers/sync-engine/modules/folders/application/FolderRenamer.ts index ecb005093..e369e1e0a 100644 --- a/src/workers/sync-engine/modules/folders/application/WebdavFolderRenamer.ts +++ b/src/workers/sync-engine/modules/folders/application/FolderRenamer.ts @@ -3,27 +3,25 @@ import { FolderPath } from '../domain/FolderPath'; import { Folder } from '../domain/Folder'; import { FolderRepository } from '../domain/FolderRepository'; -export class WebdavFolderRenamer { +export class FolderRenamer { constructor( private readonly repository: FolderRepository, private readonly ipc: SyncEngineIpc ) {} - async run(folder: Folder, destination: string) { - const path = new FolderPath(destination); - + async run(folder: Folder, destination: FolderPath) { this.ipc.send('RENAMING_FOLDER', { oldName: folder.name, - newName: path.name(), + newName: destination.name(), }); - folder.rename(path); + folder.rename(destination); await this.repository.updateName(folder); this.ipc.send('FOLDER_RENAMED', { oldName: folder.name, - newName: path.name(), + newName: destination.name(), }); } } diff --git a/src/workers/sync-engine/modules/folders/application/ParentFoldersExistForDeletion.ts b/src/workers/sync-engine/modules/folders/application/ParentFoldersExistForDeletion.ts new file mode 100644 index 000000000..2ef02a579 --- /dev/null +++ b/src/workers/sync-engine/modules/folders/application/ParentFoldersExistForDeletion.ts @@ -0,0 +1,25 @@ +import { Folder } from '../domain/Folder'; +import { FolderRepository } from '../domain/FolderRepository'; +import { FolderStatuses } from '../domain/FolderStatus'; + +export class ParentFoldersExistForDeletion { + constructor(private readonly repository: FolderRepository) {} + + run(id: Folder['id']): boolean { + const folder = this.repository.searchByPartial({ id }); + + if (!folder) { + throw new Error(`Folder with id ${id} was not found`); + } + + if (!folder.hasStatus(FolderStatuses.EXISTS)) { + return false; + } + + if (!folder.parentId) { + return true; + } + + return this.run(folder.parentId); + } +} diff --git a/src/workers/sync-engine/modules/folders/application/WebdavFolderCreator.ts b/src/workers/sync-engine/modules/folders/application/WebdavFolderCreator.ts deleted file mode 100644 index bed3f5777..000000000 --- a/src/workers/sync-engine/modules/folders/application/WebdavFolderCreator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SyncEngineIpc } from '../../../ipcRendererSyncEngine'; -import { FolderPath } from '../domain/FolderPath'; -import { FolderRepository } from '../domain/FolderRepository'; -import { WebdavFolderFinder } from './WebdavFolderFinder'; - -export class WebdavFolderCreator { - constructor( - private readonly repository: FolderRepository, - private readonly folderFinder: WebdavFolderFinder, - private readonly ipc: SyncEngineIpc - ) {} - - async run(path: string): Promise { - const folderPath = new FolderPath(path); - this.ipc.send('CREATING_FOLDER', { - name: folderPath.name(), - }); - - const parent = this.folderFinder.run(folderPath.dirname()); - - await this.repository.create(folderPath, parent.id); - - this.ipc.send('FOLDER_CREATED', { - name: folderPath.name(), - }); - } -} diff --git a/src/workers/sync-engine/modules/folders/application/WebdavFolderDeleter.ts b/src/workers/sync-engine/modules/folders/application/WebdavFolderDeleter.ts deleted file mode 100644 index 31577a5a9..000000000 --- a/src/workers/sync-engine/modules/folders/application/WebdavFolderDeleter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Folder } from '../domain/Folder'; -import { FolderRepository } from '../domain/FolderRepository'; - -export class WebdavFolderDeleter { - constructor(private readonly repository: FolderRepository) {} - - async run(folder: Folder): Promise { - folder.trash(); - await this.repository.trash(folder); - } -} diff --git a/src/workers/sync-engine/modules/folders/domain/Folder.ts b/src/workers/sync-engine/modules/folders/domain/Folder.ts index ed4bff58f..703214f09 100644 --- a/src/workers/sync-engine/modules/folders/domain/Folder.ts +++ b/src/workers/sync-engine/modules/folders/domain/Folder.ts @@ -135,8 +135,9 @@ export class Folder extends AggregateRoot { trash() { this._status = this._status.changeTo(FolderStatuses.TRASHED); + this.updatedAt = new Date(); - // TODO: recored trashed event + // TODO: record trashed event } isIn(folder: Folder): boolean { @@ -168,4 +169,18 @@ export class Folder extends AggregateRoot { return attributes; } + + attributes(): FolderAttributes { + const attributes: FolderAttributes = { + id: this.id, + uuid: this.uuid, + parentId: this._parentId || 0, + path: this._path.value, + updatedAt: this.updatedAt.toISOString(), + createdAt: this.createdAt.toISOString(), + status: this.status.value, + }; + + return attributes; + } } diff --git a/src/workers/sync-engine/modules/folders/domain/FolderRepository.ts b/src/workers/sync-engine/modules/folders/domain/FolderRepository.ts index 3c2b6474b..6735c624d 100644 --- a/src/workers/sync-engine/modules/folders/domain/FolderRepository.ts +++ b/src/workers/sync-engine/modules/folders/domain/FolderRepository.ts @@ -5,6 +5,8 @@ import { FolderPath } from './FolderPath'; export interface FolderRepository { search(path: string): Nullable; + searchByPartial(partial: Partial): Nullable; + create( name: FolderPath, parentId: FolderAttributes['parentId'] diff --git a/src/workers/sync-engine/modules/folders/domain/FolderStatus.ts b/src/workers/sync-engine/modules/folders/domain/FolderStatus.ts index d3901d908..567d13aea 100644 --- a/src/workers/sync-engine/modules/folders/domain/FolderStatus.ts +++ b/src/workers/sync-engine/modules/folders/domain/FolderStatus.ts @@ -1,6 +1,6 @@ import { InvalidArgumentError } from '../../../../shared/domain/InvalidArgumentError'; import { EnumValueObject } from '../../../../shared/domain/EnumValueObject'; -import { ActionNotPermitedError } from './errors/ActionNotPermitedError'; +import { ActionNotPermittedError } from './errors/ActionNotPermittedError'; export enum FolderStatuses { EXISTS = 'EXISTS', @@ -28,7 +28,7 @@ export class FolderStatus extends EnumValueObject { changeTo(status: FolderStatuses): FolderStatus { if (this.value === 'TRASHED') { - throw new ActionNotPermitedError('restore from trash'); + throw new ActionNotPermittedError('restore from trash'); } return new FolderStatus(FolderStatuses[status]); diff --git a/src/workers/sync-engine/modules/folders/domain/errors/ActionNotPermitedError.ts b/src/workers/sync-engine/modules/folders/domain/errors/ActionNotPermitedError.ts deleted file mode 100644 index fff78c4d3..000000000 --- a/src/workers/sync-engine/modules/folders/domain/errors/ActionNotPermitedError.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class ActionNotPermitedError extends Error { - constructor(action: string) { - super(`${action} is not permited on folders`); - } -} diff --git a/src/workers/sync-engine/modules/folders/domain/errors/ActionNotPermittedError.ts b/src/workers/sync-engine/modules/folders/domain/errors/ActionNotPermittedError.ts new file mode 100644 index 000000000..aed1f9ece --- /dev/null +++ b/src/workers/sync-engine/modules/folders/domain/errors/ActionNotPermittedError.ts @@ -0,0 +1,5 @@ +export class ActionNotPermittedError extends Error { + constructor(action: string) { + super(`${action} is not permitted on folders`); + } +} diff --git a/src/workers/sync-engine/modules/folders/infrastructure/HttpFolderRepository.ts b/src/workers/sync-engine/modules/folders/infrastructure/HttpFolderRepository.ts index 8fec20bf3..5824f79d0 100644 --- a/src/workers/sync-engine/modules/folders/infrastructure/HttpFolderRepository.ts +++ b/src/workers/sync-engine/modules/folders/infrastructure/HttpFolderRepository.ts @@ -4,7 +4,7 @@ import { ServerFile } from '../../../../filesystems/domain/ServerFile'; import { ServerFolder } from '../../../../filesystems/domain/ServerFolder'; import { Traverser } from '../../items/application/Traverser'; import { FolderPath } from '../domain/FolderPath'; -import { Folder } from '../domain/Folder'; +import { Folder, FolderAttributes } from '../domain/Folder'; import { FolderRepository } from '../domain/FolderRepository'; import Logger from 'electron-log'; import * as uuid from 'uuid'; @@ -12,6 +12,8 @@ import { UpdateFolderNameDTO } from './dtos/UpdateFolderNameDTO'; import { SyncEngineIpc } from '../../../ipcRendererSyncEngine'; import { RemoteItemsGenerator } from '../../items/application/RemoteItemsGenerator'; import { FolderStatuses } from '../domain/FolderStatus'; +import nodePath from 'path'; +import { PlatformPathConverter } from '../../shared/test/helpers/PlatformPathConverter'; export class HttpFolderRepository implements FolderRepository { public folders: Record = {}; @@ -43,16 +45,36 @@ export class HttpFolderRepository implements FolderRepository { ) as Array<[string, Folder]>; this.folders = folders.reduce((items, [key, value]) => { - items[key] = value; + if (items[key] === undefined) { + items[key] = value; + } else if (value.updatedAt > items[key].updatedAt) { + items[key] = value; + } return items; - }, {} as Record); + }, this.folders); } search(path: string): Nullable { + // Logger.debug(Object.keys(this.folders)); return this.folders[path]; } + searchByPartial(partial: Partial): Nullable { + const keys = Object.keys(partial) as Array>; + + const folder = Object.values(this.folders).find((folder) => { + // Logger.debug(folder.attributes()[keys[0]], partial[keys[0]]); + return keys.every((key) => folder.attributes()[key] === partial[key]); + }); + + if (folder) { + return Folder.from(folder.attributes()); + } + + return undefined; + } + async create(path: FolderPath, parentId: number): Promise { const plainName = path.name(); @@ -69,13 +91,13 @@ export class HttpFolderRepository implements FolderRepository { ); if (response.status !== 201) { - throw new Error('Folder creation failded'); + throw new Error('Folder creation failed'); } const serverFolder = response.data as ServerFolder | null; if (!serverFolder) { - throw new Error('Folder creation failded, no data returned'); + throw new Error('Folder creation failed, no data returned'); } const folder = Folder.create({ @@ -88,6 +110,10 @@ export class HttpFolderRepository implements FolderRepository { status: FolderStatuses.EXISTS, }); + const normalized = nodePath.normalize(folder.path.value); + const posix = PlatformPathConverter.winToPosix(normalized); + this.folders[posix] = folder; + return folder; } @@ -106,6 +132,14 @@ export class HttpFolderRepository implements FolderRepository { `[REPOSITORY] Error updating item metadata: ${res.status}` ); } + + const old = this.searchByPartial({ uuid: folder.uuid }); + + if (old) { + delete this.folders[old?.path.value]; + } + + this.folders[folder.path.value] = folder; } async updateParentDir(folder: Folder): Promise { @@ -119,7 +153,13 @@ export class HttpFolderRepository implements FolderRepository { throw new Error(`[REPOSITORY] Error moving item: ${res.status}`); } - await this.init(); + const old = this.searchByPartial({ uuid: folder.uuid }); + + if (old) { + delete this.folders[old?.path.value]; + } + + this.folders[folder.path.value] = folder; } async searchOn(folder: Folder): Promise> { @@ -135,16 +175,17 @@ export class HttpFolderRepository implements FolderRepository { } ); - if (result.status === 200) { + if (result.status !== 200) { + Logger.error( + '[FOLDER REPOSITORY] Folder deletion failed with status: ', + result.status, + result.statusText + ); return; } - Logger.error( - '[FOLDER REPOSITORY] Folder deletion failed with status: ', - result.status, - result.statusText - ); - - await this.ipc.invoke('START_REMOTE_SYNC'); + const normalized = nodePath.normalize(folder.path.value); + const posix = PlatformPathConverter.winToPosix(normalized); + this.folders[posix] = folder; } } diff --git a/src/workers/sync-engine/modules/folders/test/__mocks__/FolderRepositoryMock.ts b/src/workers/sync-engine/modules/folders/test/__mocks__/FolderRepositoryMock.ts index d6deeedc3..7e241a78e 100644 --- a/src/workers/sync-engine/modules/folders/test/__mocks__/FolderRepositoryMock.ts +++ b/src/workers/sync-engine/modules/folders/test/__mocks__/FolderRepositoryMock.ts @@ -1,10 +1,11 @@ import { Nullable } from 'shared/types/Nullable'; import { FolderPath } from '../../domain/FolderPath'; -import { Folder } from '../../domain/Folder'; +import { Folder, FolderAttributes } from '../../domain/Folder'; import { FolderRepository } from '../../domain/FolderRepository'; export class FolderRepositoryMock implements FolderRepository { public mockSearch = jest.fn(); + public mockSearchByPartial = jest.fn(); public mockAdd = jest.fn(); public mockUpdateName = jest.fn(); public mockUpdateParentDir = jest.fn(); @@ -16,6 +17,10 @@ export class FolderRepositoryMock implements FolderRepository { return this.mockSearch(pathLike); } + searchByPartial(partial: Partial): Nullable { + return this.mockSearchByPartial(partial); + } + add(file: Folder): Promise { return this.mockAdd(file); } diff --git a/src/workers/sync-engine/modules/folders/test/application/FolderDeleter.test.ts b/src/workers/sync-engine/modules/folders/test/application/FolderDeleter.test.ts new file mode 100644 index 000000000..4589ccd34 --- /dev/null +++ b/src/workers/sync-engine/modules/folders/test/application/FolderDeleter.test.ts @@ -0,0 +1,81 @@ +import { FolderDeleter } from '../../application/FolderDeleter'; +import { FolderStatus } from '../../domain/FolderStatus'; +import { FolderMother } from '../domain/FolderMother'; +import { FolderRepositoryMock } from '../__mocks__/FolderRepositoryMock'; +import { ParentFoldersExistForDeletion } from '../../application/ParentFoldersExistForDeletion'; +import { PlaceholderCreatorMock } from '../../../placeholders/test/__mock__/PlaceholderCreatorMock'; + +describe('Folder deleter', () => { + let repository: FolderRepositoryMock; + let placeholderCreator: PlaceholderCreatorMock; + let parentFoldersExistForDeletion: ParentFoldersExistForDeletion; + let SUT: FolderDeleter; + + beforeEach(() => { + repository = new FolderRepositoryMock(); + parentFoldersExistForDeletion = new ParentFoldersExistForDeletion( + repository + ); + placeholderCreator = new PlaceholderCreatorMock(); + SUT = new FolderDeleter( + repository, + parentFoldersExistForDeletion, + placeholderCreator + ); + }); + + it('trashes an existing folder', async () => { + const folder = FolderMother.exists(); + + repository.mockSearchByPartial.mockReturnValueOnce(folder); + jest.spyOn(parentFoldersExistForDeletion, 'run').mockReturnValueOnce(true); + + await SUT.run(folder.uuid); + + expect(repository.mockTrash).toBeCalledWith( + expect.objectContaining({ + status: FolderStatus.Trashed, + }) + ); + }); + + it('throws an error when trashing a folder already trashed', async () => { + const folder = FolderMother.trashed(); + + repository.mockSearchByPartial.mockReturnValueOnce(folder); + jest.spyOn(parentFoldersExistForDeletion, 'run').mockReturnValueOnce(true); + + await SUT.run(folder.uuid).catch((err) => { + expect(err).toBeDefined(); + }); + + expect(repository.mockTrash).not.toBeCalled(); + }); + + it('does not delete the folder if a higher folder is already deleted ', async () => { + const folder = FolderMother.exists(); + + repository.mockSearchByPartial.mockReturnValueOnce(folder); + jest.spyOn(parentFoldersExistForDeletion, 'run').mockReturnValueOnce(false); + + await SUT.run(folder.uuid).catch((err) => { + expect(err).toBeDefined(); + }); + + expect(repository.mockTrash).not.toBeCalled(); + }); + + it('recreates the placeholder if the deletion fails', async () => { + const folder = FolderMother.exists(); + + repository.mockSearchByPartial.mockReturnValueOnce(folder); + jest.spyOn(parentFoldersExistForDeletion, 'run').mockReturnValueOnce(true); + repository.mockTrash.mockRejectedValue( + new Error('Error during the deletion') + ); + + await SUT.run(folder.uuid); + + expect(placeholderCreator.folderMock).toBeCalledWith(folder); + }); +}); diff --git a/src/workers/sync-engine/modules/folders/test/application/FolderMover.test.ts b/src/workers/sync-engine/modules/folders/test/application/FolderMover.test.ts new file mode 100644 index 000000000..b52c1befa --- /dev/null +++ b/src/workers/sync-engine/modules/folders/test/application/FolderMover.test.ts @@ -0,0 +1,54 @@ +import { FolderFinder } from '../../application/FolderFinder'; +import { FolderMover } from '../../application/FolderMover'; +import { FolderMother } from '../domain/FolderMother'; +import { FolderRepositoryMock } from '../__mocks__/FolderRepositoryMock'; +import { FolderPath } from '../../domain/FolderPath'; + +describe('Folder Mover', () => { + let repository: FolderRepositoryMock; + let folderFinder: FolderFinder; + let SUT: FolderMover; + + beforeEach(() => { + repository = new FolderRepositoryMock(); + folderFinder = new FolderFinder(repository); + + SUT = new FolderMover(repository, folderFinder); + }); + + it('Folders cannot be overwrite', async () => { + const folder = FolderMother.in(1, '/folderA/folderB'); + const destination = new FolderPath('/folderC/folderB'); + + repository.mockSearch.mockImplementation(() => + FolderMother.in(2, destination.value) + ); + + try { + const hasBeenOverwritten = await SUT.run(folder, destination); + expect(hasBeenOverwritten).not.toBeDefined(); + } catch (err) { + expect(err).toBeDefined(); + } + + expect(repository.mockUpdateName).not.toBeCalled(); + expect(repository.mockUpdateParentDir).not.toBeCalled(); + }); + + describe('Move', () => { + it('moves a folder when the destination folder does not contain a folder with the same folder', async () => { + const folder = FolderMother.in(1, '/folderA/folderB'); + const destination = new FolderPath('/folderC/folderB'); + const folderC = FolderMother.in(2, '/folderC'); + + repository.mockSearch + .mockReturnValueOnce(undefined) + .mockReturnValueOnce(folderC); + + await SUT.run(folder, destination); + + expect(repository.mockUpdateParentDir).toHaveBeenCalled(); + expect(repository.mockUpdateName).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/workers/sync-engine/modules/folders/test/application/FolderPathCreator.test.ts b/src/workers/sync-engine/modules/folders/test/application/FolderPathCreator.test.ts new file mode 100644 index 000000000..31612c56d --- /dev/null +++ b/src/workers/sync-engine/modules/folders/test/application/FolderPathCreator.test.ts @@ -0,0 +1,19 @@ +import path from 'path'; +import { FolderPathCreator } from '../../application/FolderPathCreator'; + +describe('Folder Phat Creator', () => { + describe('Create from absolute', () => { + it('works', () => { + const ab = 'C\\:Users\\JWcer\\InternxtDrive\\\\New folder (4)\\'; + + const sut = new FolderPathCreator('C\\:Users\\JWcer\\InternxtDrive'); + + const result = sut.fromAbsolute(ab); + + // TODO: This behavior need to change. Normalize any path that is returned form bindings + expect(result.value).toBe('\\New folder (4)'); + expect(path.dirname(result.value)).toBe('\\'); + expect(result.dirname()).toBe(path.sep); + }); + }); +}); diff --git a/src/workers/sync-engine/modules/folders/test/application/WebdavFolderDeleter.test.ts b/src/workers/sync-engine/modules/folders/test/application/WebdavFolderDeleter.test.ts deleted file mode 100644 index 79cb5efc0..000000000 --- a/src/workers/sync-engine/modules/folders/test/application/WebdavFolderDeleter.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { WebdavFolderDeleter } from '../../application/WebdavFolderDeleter'; -import { FolderStatus } from '../../domain/FolderStatus'; -import { FolderMother } from '../domain/FolderMother'; -import { FolderRepositoryMock } from '../__mocks__/FolderRepositoryMock'; - -describe('Folder deleter', () => { - let repository: FolderRepositoryMock; - let SUT: WebdavFolderDeleter; - - beforeEach(() => { - repository = new FolderRepositoryMock(); - SUT = new WebdavFolderDeleter(repository); - }); - - it('trashes an existing folder', () => { - const folder = FolderMother.exists(); - - SUT.run(folder); - - expect(repository.mockTrash).toBeCalledWith( - expect.objectContaining({ - status: FolderStatus.Trashed, - }) - ); - }); - - it('throws an error when trashing a folder already trashed', () => { - const folder = FolderMother.trashed(); - - SUT.run(folder).catch((err) => { - expect(err).toBeDefined(); - }); - - expect(repository.mockTrash).not.toBeCalled(); - }); -}); diff --git a/src/workers/sync-engine/modules/folders/test/application/WebdavFolderMover.test.ts b/src/workers/sync-engine/modules/folders/test/application/WebdavFolderMover.test.ts deleted file mode 100644 index 37fc0c51a..000000000 --- a/src/workers/sync-engine/modules/folders/test/application/WebdavFolderMover.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { WebdavFolderFinder } from '../../application/WebdavFolderFinder'; -import { WebdavFolderMover } from '../../application/WebdavFolderMover'; -import { WebdavFolderRenamer } from '../../application/WebdavFolderRenamer'; -import { FolderMother } from '../domain/FolderMother'; -import { FolderRepositoryMock } from '../__mocks__/FolderRepositoryMock'; -import { IpcRendererSyncEngineMock } from '../../../shared/test/__mock__/IpcRendererSyncEngineMock'; - -describe('Folder Mover', () => { - let repository: FolderRepositoryMock; - let folderFinder: WebdavFolderFinder; - let folderRenamer: WebdavFolderRenamer; - let ipc: IpcRendererSyncEngineMock; - let SUT: WebdavFolderMover; - - beforeEach(() => { - repository = new FolderRepositoryMock(); - folderFinder = new WebdavFolderFinder(repository); - ipc = new IpcRendererSyncEngineMock(); - folderRenamer = new WebdavFolderRenamer(repository, ipc); - - SUT = new WebdavFolderMover(repository, folderFinder, folderRenamer); - }); - - it('Folders cannot be ovewrited', async () => { - const folder = FolderMother.in(1, '/folderA/folderB'); - const destination = '/folderC/folderB'; - - repository.mockSearch.mockImplementation(() => - FolderMother.in(2, destination) - ); - - try { - const hasBeenOverwritten = await SUT.run(folder, destination); - expect(hasBeenOverwritten).not.toBeDefined(); - } catch (err) { - expect(err).toBeDefined(); - } - - expect(repository.mockUpdateName).not.toBeCalled(); - expect(repository.mockUpdateParentDir).not.toBeCalled(); - }); - - describe('Move', () => { - it('moves a folder when the destination folder does not contain a folder with the same folder', async () => { - const folder = FolderMother.in(1, '/folderA/folderB'); - const destination = '/folderC/folderB'; - const folderC = FolderMother.in(2, '/folderC'); - - repository.mockSearch - .mockReturnValueOnce(undefined) - .mockReturnValueOnce(folderC); - - await SUT.run(folder, destination); - - expect(repository.mockUpdateParentDir).toHaveBeenCalled(); - expect(repository.mockUpdateName).not.toHaveBeenCalled(); - }); - }); - - describe('Rename', () => { - it('when a folder is moved to same folder its renamed', async () => { - const folderAId = 30010278; - const folder = FolderMother.in(folderAId, '/folderA/folderB'); - const destination = '/folderA/folderC'; - - repository.mockSearch - .mockReturnValueOnce(undefined) - .mockReturnValueOnce(FolderMother.withId(folderAId)); - - await SUT.run(folder, destination); - - expect(repository.mockUpdateName).toHaveBeenCalled(); - expect(repository.mockUpdateParentDir).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/workers/sync-engine/modules/folders/test/domain/FolderMother.ts b/src/workers/sync-engine/modules/folders/test/domain/FolderMother.ts index c342a7019..18c642dfb 100644 --- a/src/workers/sync-engine/modules/folders/test/domain/FolderMother.ts +++ b/src/workers/sync-engine/modules/folders/test/domain/FolderMother.ts @@ -2,6 +2,8 @@ import { File } from '../../../files/domain/File'; import { FolderStatuses } from '../../domain/FolderStatus'; import { Folder } from '../../domain/Folder'; import { FolderUuid } from '../../domain/FolderUuid'; +import Chance from 'chance'; +const chance = new Chance(); export class FolderMother { static containing(file: File) { @@ -57,7 +59,7 @@ export class FolderMother { id: 2048, uuid: FolderUuid.random().value, path: '/Zodseve', - parentId: null, + parentId: chance.integer({ min: 1 }), updatedAt: new Date().toISOString(), createdAt: new Date().toISOString(), status: FolderStatuses.EXISTS, diff --git a/src/workers/sync-engine/modules/folders/test/domain/FolderPath.test.ts b/src/workers/sync-engine/modules/folders/test/domain/FolderPath.test.ts index 13a64820c..d4a595f1d 100644 --- a/src/workers/sync-engine/modules/folders/test/domain/FolderPath.test.ts +++ b/src/workers/sync-engine/modules/folders/test/domain/FolderPath.test.ts @@ -3,7 +3,7 @@ import { PlatformPathConverter } from '../../../shared/test/helpers/PlatformPath import path from 'path'; describe('Path', () => { - describe('path instanciation', () => { + describe('path instantiation', () => { it('path from parts creates expected result', () => { const parts = [path.sep, 'Family']; diff --git a/src/workers/sync-engine/modules/placeholders/domain/PlaceholderCreator.ts b/src/workers/sync-engine/modules/placeholders/domain/PlaceholderCreator.ts new file mode 100644 index 000000000..21102c3e7 --- /dev/null +++ b/src/workers/sync-engine/modules/placeholders/domain/PlaceholderCreator.ts @@ -0,0 +1,7 @@ +import { File } from '../../files/domain/File'; +import { Folder } from '../../folders/domain/Folder'; + +export interface PlaceholderCreator { + folder: (folder: Folder) => void; + file: (file: File) => void; +} diff --git a/src/workers/sync-engine/modules/placeholders/infrastructure/VirtualDrivePlaceholderCreator.ts b/src/workers/sync-engine/modules/placeholders/infrastructure/VirtualDrivePlaceholderCreator.ts new file mode 100644 index 000000000..18730153d --- /dev/null +++ b/src/workers/sync-engine/modules/placeholders/infrastructure/VirtualDrivePlaceholderCreator.ts @@ -0,0 +1,18 @@ +import { VirtualDrive } from 'virtual-drive/dist'; +import { File } from '../../files/domain/File'; +import { Folder } from '../../folders/domain/Folder'; +import { PlaceholderCreator } from '../domain/PlaceholderCreator'; + +export class VirtualDrivePlaceholderCreator implements PlaceholderCreator { + constructor(private readonly drive: VirtualDrive) {} + + folder(folder: Folder): void { + const folderPath = `${folder.path.value}/`; + + this.drive.createItemByPath(folderPath, folder.uuid); + } + + file(file: File): void { + this.drive.createItemByPath(file.path.value, file.contentsId, file.size); + } +} diff --git a/src/workers/sync-engine/modules/placeholders/test/__mock__/PlaceholderCreatorMock.ts b/src/workers/sync-engine/modules/placeholders/test/__mock__/PlaceholderCreatorMock.ts new file mode 100644 index 000000000..ac12fd92a --- /dev/null +++ b/src/workers/sync-engine/modules/placeholders/test/__mock__/PlaceholderCreatorMock.ts @@ -0,0 +1,16 @@ +import { File } from 'workers/sync-engine/modules/files/domain/File'; +import { PlaceholderCreator } from '../../domain/PlaceholderCreator'; +import { Folder } from 'workers/sync-engine/modules/folders/domain/Folder'; + +export class PlaceholderCreatorMock implements PlaceholderCreator { + fileMock = jest.fn(); + folderMock = jest.fn(); + + file(file: File) { + this.fileMock(file); + } + + folder(folder: Folder) { + this.folderMock(folder); + } +} diff --git a/src/workers/sync-engine/modules/shared/application/AllWebdavItemsSearcher.ts b/src/workers/sync-engine/modules/shared/application/AllWebdavItemsSearcher.ts index d1c74d802..7796e9035 100644 --- a/src/workers/sync-engine/modules/shared/application/AllWebdavItemsSearcher.ts +++ b/src/workers/sync-engine/modules/shared/application/AllWebdavItemsSearcher.ts @@ -1,12 +1,12 @@ import { FileRepository } from '../../files/domain/FileRepository'; -import { WebdavFolderFinder } from '../../folders/application/WebdavFolderFinder'; +import { FolderFinder } from '../../folders/application/FolderFinder'; import { FolderRepository } from '../../folders/domain/FolderRepository'; export class AllWebdavItemsNameLister { constructor( private readonly filesRepository: FileRepository, private readonly folderRepository: FolderRepository, - private readonly folderfinder: WebdavFolderFinder + private readonly folderfinder: FolderFinder ) {} async run(path: string): Promise> { diff --git a/src/workers/sync-engine/modules/shared/domain/DelayQueue.ts b/src/workers/sync-engine/modules/shared/domain/DelayQueue.ts new file mode 100644 index 000000000..121c8ea77 --- /dev/null +++ b/src/workers/sync-engine/modules/shared/domain/DelayQueue.ts @@ -0,0 +1,59 @@ +import Logger from 'electron-log'; + +export class DelayQueue { + private static readonly DELAY = 3_000; + private queue: Map; + private timeout: NodeJS.Timeout | null = null; + + constructor( + private readonly name: string, + private readonly fn: (item: string) => Promise, + private readonly canLoop: () => boolean + ) { + this.queue = new Map(); + } + + private clearTimeout() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + } + + private setTimeout() { + this.clearTimeout(); + this.timeout = setTimeout(async () => { + Logger.debug('Will try to run delay queue for: ', this.name); + if (this.canLoop()) { + Logger.debug('Running delay queue for: ', this.name); + + const reversedItems = Array.from(this.queue.entries()).reverse(); + + for (const [item] of reversedItems) { + await this.fn(item); + this.queue.delete(item); + } + + return; + } + + Logger.debug(this.name, ' loop blocked'); + this.setTimeout(); + }, DelayQueue.DELAY); + } + + push(value: string) { + this.setTimeout(); + + this.queue.set(value); + } + + get size(): number { + return this.queue.size; + } + + clear() { + this.clearTimeout(); + this.queue.clear(); + } +} diff --git a/src/workers/sync-engine/modules/shared/domain/MapObserver.ts b/src/workers/sync-engine/modules/shared/domain/MapObserver.ts new file mode 100644 index 000000000..e22a3d7f2 --- /dev/null +++ b/src/workers/sync-engine/modules/shared/domain/MapObserver.ts @@ -0,0 +1,26 @@ +export class MapObserver { + constructor( + private readonly mapToObserve: Map, + private readonly callback: () => void, + private intervalId: NodeJS.Timeout | null = null + ) {} + + startObserving() { + if (this.intervalId === null) { + this.intervalId = setInterval(() => { + if (this.mapToObserve.size === 0) { + clearInterval(this.intervalId!); + this.intervalId = null; + this.callback(); + } + }, 1_000); + } + } + + stopObserving() { + if (this.intervalId !== null) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } +} diff --git a/src/workers/sync-engine/modules/shared/domain/Path.ts b/src/workers/sync-engine/modules/shared/domain/Path.ts index 2151b26d8..5ec02d867 100644 --- a/src/workers/sync-engine/modules/shared/domain/Path.ts +++ b/src/workers/sync-engine/modules/shared/domain/Path.ts @@ -21,7 +21,12 @@ export abstract class Path extends ValueObject { } dirname(): string { - return this.convertPathToCurrentPlatform(path.dirname(this.value)); + const dirname = this.convertPathToCurrentPlatform(path.dirname(this.value)); + if (dirname === '.') { + return path.sep; + } + + return dirname; } posixDirname(): string { diff --git a/src/workers/sync-engine/modules/shared/domain/WebdavServerEventBus.ts b/src/workers/sync-engine/modules/shared/domain/WebdavServerEventBus.ts index af0aeaa2b..f427122ac 100644 --- a/src/workers/sync-engine/modules/shared/domain/WebdavServerEventBus.ts +++ b/src/workers/sync-engine/modules/shared/domain/WebdavServerEventBus.ts @@ -1,7 +1,7 @@ import { DomainEventSubscribers } from '../infrastructure/DomainEventSubscribers'; import { DomainEvent } from './DomainEvent'; -export interface WebdavServerEventBus { +export interface EventBus { publish(events: Array): Promise; addSubscribers(subscribers: DomainEventSubscribers): void; } diff --git a/src/workers/sync-engine/modules/shared/infrastructure/DomainEventSubscribers.ts b/src/workers/sync-engine/modules/shared/infrastructure/DomainEventSubscribers.ts index 5f2f9126d..b309f7621 100644 --- a/src/workers/sync-engine/modules/shared/infrastructure/DomainEventSubscribers.ts +++ b/src/workers/sync-engine/modules/shared/infrastructure/DomainEventSubscribers.ts @@ -7,7 +7,7 @@ export class DomainEventSubscribers { constructor(public items: Array>) {} static from(container: DependencyContainer): DomainEventSubscribers { - const subscribers = DependencyContainerFactory.subscriptors.map( + const subscribers = DependencyContainerFactory.subscribers.map( (subscriber) => { return container[subscriber]; } diff --git a/src/workers/sync-engine/modules/shared/infrastructure/DuplexEventBus.ts b/src/workers/sync-engine/modules/shared/infrastructure/NodeJsEventBus.ts similarity index 78% rename from src/workers/sync-engine/modules/shared/infrastructure/DuplexEventBus.ts rename to src/workers/sync-engine/modules/shared/infrastructure/NodeJsEventBus.ts index 35e31442f..6ce84b9c8 100644 --- a/src/workers/sync-engine/modules/shared/infrastructure/DuplexEventBus.ts +++ b/src/workers/sync-engine/modules/shared/infrastructure/NodeJsEventBus.ts @@ -1,12 +1,9 @@ import EventEmitter from 'events'; import { DomainEvent } from '../domain/DomainEvent'; -import { WebdavServerEventBus } from '../domain/WebdavServerEventBus'; +import { EventBus } from '../domain/WebdavServerEventBus'; import { DomainEventSubscribers } from './DomainEventSubscribers'; -export class NodeJsEventBus - extends EventEmitter - implements WebdavServerEventBus -{ +export class NodeJsEventBus extends EventEmitter implements EventBus { async publish(events: Array): Promise { events.forEach((event) => { this.emit(event.eventName, event); diff --git a/src/workers/sync-engine/modules/shared/test/__mock__/EventBusMock.ts b/src/workers/sync-engine/modules/shared/test/__mock__/EventBusMock.ts index 4f3e32bb4..89212bb69 100644 --- a/src/workers/sync-engine/modules/shared/test/__mock__/EventBusMock.ts +++ b/src/workers/sync-engine/modules/shared/test/__mock__/EventBusMock.ts @@ -1,8 +1,8 @@ import { DomainEvent } from '../../domain/DomainEvent'; -import { WebdavServerEventBus } from '../../domain/WebdavServerEventBus'; +import { EventBus } from '../../domain/WebdavServerEventBus'; import { DomainEventSubscribers } from '../../infrastructure/DomainEventSubscribers'; -export class EventBusMock implements WebdavServerEventBus { +export class EventBusMock implements EventBus { public publishMock = jest.fn(); async publish(events: DomainEvent[]) {