diff --git a/src/apps/fuse/AsyncFunctionQueue.ts b/src/apps/fuse/AsyncFunctionQueue.ts new file mode 100644 index 000000000..f960d54e3 --- /dev/null +++ b/src/apps/fuse/AsyncFunctionQueue.ts @@ -0,0 +1,25 @@ +export class AsyncFunctionQueue { + private readonly queue = new Map>(); + + constructor( + private readonly asyncFunction: (...params: any[]) => Promise + ) {} + + async enqueue(...params: any[]): Promise { + const key = params[0]; + + if (this.queue.has(key)) { + const promise = this.queue.get(key); + await promise; + } + + const added = this.asyncFunction(...params); + this.queue.set(key, added); + + try { + await added; + } finally { + this.queue.delete(key); + } + } +} diff --git a/src/apps/fuse/FuseApp.ts b/src/apps/fuse/FuseApp.ts index 8150b68d5..799135c4e 100644 --- a/src/apps/fuse/FuseApp.ts +++ b/src/apps/fuse/FuseApp.ts @@ -31,13 +31,17 @@ export class FuseApp { private async getOpt() { const readdir = new ReaddirCallback( - this.fuseContainer.virtualDriveContainer + this.fuseContainer.virtualDriveContainer, + this.fuseContainer.offlineDriveContainer ); const getattr = new GetAttributesCallback( this.fuseContainer.virtualDriveContainer, this.fuseContainer.offlineDriveContainer ); - const open = new OpenCallback(this.fuseContainer.virtualDriveContainer); + const open = new OpenCallback( + this.fuseContainer.virtualDriveContainer, + this.fuseContainer.offlineDriveContainer + ); const read = new ReadCallback( this.fuseContainer.virtualDriveContainer, this.fuseContainer.offlineDriveContainer @@ -51,7 +55,8 @@ export class FuseApp { this.fuseContainer.virtualDriveContainer ); const trashFile = new TrashFileCallback( - this.fuseContainer.virtualDriveContainer + this.fuseContainer.virtualDriveContainer, + this.fuseContainer.offlineDriveContainer ); const trashFolder = new TrashFolderCallback( this.fuseContainer.virtualDriveContainer diff --git a/src/apps/fuse/callbacks/AccessCallback.ts b/src/apps/fuse/callbacks/AccessCallback.ts new file mode 100644 index 000000000..799ee2850 --- /dev/null +++ b/src/apps/fuse/callbacks/AccessCallback.ts @@ -0,0 +1,11 @@ +import { NotifyFuseCallback } from './FuseCallback'; + +export class AccessCallback extends NotifyFuseCallback { + constructor() { + super('Access', { input: true, output: true }); + } + + async execute() { + return this.right(); + } +} diff --git a/src/apps/fuse/callbacks/ChownCallback.ts b/src/apps/fuse/callbacks/ChownCallback.ts new file mode 100644 index 000000000..9109e3cc3 --- /dev/null +++ b/src/apps/fuse/callbacks/ChownCallback.ts @@ -0,0 +1,11 @@ +import { NotifyFuseCallback } from './FuseCallback'; + +export class ChownCallback extends NotifyFuseCallback { + constructor() { + super('Chown', { input: true, output: true }); + } + + async execute(_path: string, _uid: number, _gid: number) { + return this.right(); + } +} diff --git a/src/apps/fuse/callbacks/FuseCallback.ts b/src/apps/fuse/callbacks/FuseCallback.ts index 6c50e978d..56ed4c6f6 100644 --- a/src/apps/fuse/callbacks/FuseCallback.ts +++ b/src/apps/fuse/callbacks/FuseCallback.ts @@ -128,7 +128,7 @@ export abstract class NotifyFuseCallback extends FuseCallback { } if (this.debug.output) { - Logger.debug(`${this.name} completed successfully`); + Logger.debug(`${this.name} completed successfully ${params[0]}`); } callback(NotifyFuseCallback.OK); diff --git a/src/apps/fuse/callbacks/GetAttributesCallback.ts b/src/apps/fuse/callbacks/GetAttributesCallback.ts index 47f440269..b8c73f873 100644 --- a/src/apps/fuse/callbacks/GetAttributesCallback.ts +++ b/src/apps/fuse/callbacks/GetAttributesCallback.ts @@ -4,7 +4,6 @@ import { OfflineDriveDependencyContainer } from '../dependency-injection/offline import { VirtualDriveDependencyContainer } from '../dependency-injection/virtual-drive/VirtualDriveDependencyContainer'; import { FuseCallback } from './FuseCallback'; import { FuseError, FuseNoSuchFileOrDirectoryError } from './FuseErrors'; -import Logger from 'electron-log'; type GetAttributesCallbackData = { mode: number; @@ -12,6 +11,8 @@ type GetAttributesCallbackData = { mtime: Date; ctime: Date; atime?: Date; + uid: number; + gid: number; }; export class GetAttributesCallback extends FuseCallback { @@ -30,7 +31,6 @@ export class GetAttributesCallback extends FuseCallback { // When the OS wants to check if a node exists will try to get the attributes of it // so not founding them is not an error - Logger.info(`No attributes found for ${error.path}`); return left(error); } @@ -42,6 +42,8 @@ export class GetAttributesCallback extends FuseCallback { - constructor(private readonly container: VirtualDriveDependencyContainer) { + constructor( + private readonly virtual: VirtualDriveDependencyContainer, + private readonly offline: OfflineDriveDependencyContainer + ) { super('Open'); } async execute(path: string, _flags: Array) { - const file = await this.container.filesSearcher.run({ path }); + const virtual = await this.virtual.filesSearcher.run({ path }); - if (!file) { + if (!virtual) { + const offline = await this.offline.offlineFileSearcher.run({ path }); + if (offline) { + return this.right(0); + } return this.left(new FuseNoSuchFileOrDirectoryError(path)); } try { - await this.container.downloadContentsToPlainFile.run(file); + await this.virtual.downloadContentsToPlainFile.run(virtual); - return this.right(file.id); + return this.right(0); } catch (err: unknown) { Logger.error('Error downloading file: ', err); if (err instanceof Error) { diff --git a/src/apps/fuse/callbacks/ReadCallback.ts b/src/apps/fuse/callbacks/ReadCallback.ts index a3b0bf7d1..226e9767c 100644 --- a/src/apps/fuse/callbacks/ReadCallback.ts +++ b/src/apps/fuse/callbacks/ReadCallback.ts @@ -1,6 +1,7 @@ import Logger from 'electron-log'; import { VirtualDriveDependencyContainer } from '../dependency-injection/virtual-drive/VirtualDriveDependencyContainer'; import { OfflineDriveDependencyContainer } from '../dependency-injection/offline/OfflineDriveDependencyContainer'; +import { Optional } from '../../../shared/types/Optional'; // eslint-disable-next-line @typescript-eslint/no-var-requires const fuse = require('@gcas/fuse'); @@ -33,6 +34,17 @@ export class ReadCallback { return chunk.length; // number of bytes read } + private async copyToBuffer(buffer: Buffer, bufferOptional: Optional) { + if (!bufferOptional.isPresent()) { + return 0; + } + + const chunk = bufferOptional.get(); + + chunk.copy(buffer); // write the result of the read to the result buffer + return chunk.length; // number of bytes read + } + async execute( path: string, _fd: any, @@ -41,15 +53,34 @@ export class ReadCallback { pos: number, cb: (code: number, params?: any) => void ) { - const file = await this.virtualDrive.filesSearcher.run({ path }); + const virtualFile = await this.virtualDrive.filesSearcher.run({ path }); + + if (!virtualFile) { + const offlineFile = await this.offlineDrive.offlineFileSearcher.run({ + path, + }); + + if (!offlineFile) { + Logger.error('READ FILE NOT FOUND', path); + cb(fuse.ENOENT); + return; + } + + const chunk = + await this.offlineDrive.auxiliarOfflineContentsChucksReader.run( + offlineFile.id, + len, + pos + ); + + const result = await this.copyToBuffer(buf, chunk); - if (!file) { - cb(fuse.ENOENT); + cb(result); return; } const filePath = this.virtualDrive.relativePathToAbsoluteConverter.run( - file.contentsId + virtualFile.contentsId ); try { @@ -57,7 +88,7 @@ export class ReadCallback { cb(bytesRead); } catch (err) { Logger.error(`Error reading file: ${err}`); - cb(fuse.ENOENT); + cb(fuse.EIO); } } } diff --git a/src/apps/fuse/callbacks/ReaddirCallback.ts b/src/apps/fuse/callbacks/ReaddirCallback.ts index ec3eee751..3b4a61732 100644 --- a/src/apps/fuse/callbacks/ReaddirCallback.ts +++ b/src/apps/fuse/callbacks/ReaddirCallback.ts @@ -1,23 +1,40 @@ +import { OfflineDriveDependencyContainer } from '../dependency-injection/offline/OfflineDriveDependencyContainer'; import { VirtualDriveDependencyContainer } from '../dependency-injection/virtual-drive/VirtualDriveDependencyContainer'; import { FuseCallback } from './FuseCallback'; export class ReaddirCallback extends FuseCallback> { - constructor(private readonly container: VirtualDriveDependencyContainer) { + constructor( + private readonly virtual: VirtualDriveDependencyContainer, + private readonly offline: OfflineDriveDependencyContainer + ) { super('Read Directory'); } async execute(path: string) { const filesNamesPromise = - this.container.filesByFolderPathNameLister.run(path); + this.virtual.filesByFolderPathNameLister.run(path); - const folderNamesPromise = - this.container.foldersByParentPathLister.run(path); + const folderNamesPromise = this.virtual.foldersByParentPathLister.run(path); + + const offlineFiles = await this.offline.offlineFilesByParentPathLister.run( + path + ); + + const auxiliaryFileName = offlineFiles + .filter((file) => file.isAuxiliary()) + .map((file) => file.name); const [filesNames, foldersNames] = await Promise.all([ filesNamesPromise, folderNamesPromise, ]); - return this.right(['.', '..', ...filesNames, ...foldersNames]); + return this.right([ + '.', + '..', + ...filesNames, + ...foldersNames, + ...auxiliaryFileName, + ]); } } diff --git a/src/apps/fuse/callbacks/ReleaseCallback.ts b/src/apps/fuse/callbacks/ReleaseCallback.ts index 8a7db622a..7acddcb78 100644 --- a/src/apps/fuse/callbacks/ReleaseCallback.ts +++ b/src/apps/fuse/callbacks/ReleaseCallback.ts @@ -1,6 +1,8 @@ import { OfflineDriveDependencyContainer } from '../dependency-injection/offline/OfflineDriveDependencyContainer'; import { VirtualDriveDependencyContainer } from '../dependency-injection/virtual-drive/VirtualDriveDependencyContainer'; import { NotifyFuseCallback } from './FuseCallback'; +import Logger from 'electron-log'; +import { FuseIOError } from './FuseErrors'; export class ReleaseCallback extends NotifyFuseCallback { constructor( @@ -11,31 +13,44 @@ export class ReleaseCallback extends NotifyFuseCallback { } async execute(path: string, _fd: number) { - const offlineFile = await this.offlineDrive.offlineFileSearcher.run({ - path, - }); - - if (offlineFile) { - await this.offlineDrive.offlineContentsUploader.run( - offlineFile.id, - offlineFile.path - ); - return this.right(); - } + try { + const offlineFile = await this.offlineDrive.offlineFileSearcher.run({ + path, + }); + + if (offlineFile) { + if (offlineFile.size.value === 0) { + return this.right(); + } + + if (offlineFile.isAuxiliary()) { + return this.right(); + } + + await this.offlineDrive.offlineContentsUploader.run( + offlineFile.id, + offlineFile.path + ); + return this.right(); + } - const virtualFile = await this.virtualDrive.filesSearcher.run({ path }); + const virtualFile = await this.virtualDrive.filesSearcher.run({ path }); - if (virtualFile) { - const contentsPath = - this.virtualDrive.relativePathToAbsoluteConverter.run( - virtualFile.contentsId - ); + if (virtualFile) { + const contentsPath = + this.virtualDrive.relativePathToAbsoluteConverter.run( + virtualFile.contentsId + ); + + await this.offlineDrive.offlineContentsCacheCleaner.run(contentsPath); - await this.offlineDrive.offlineContentsCacheCleaner.run(contentsPath); + return this.right(); + } return this.right(); + } catch (err: unknown) { + Logger.error('RELEASE', err); + return this.left(new FuseIOError()); } - - return this.right(); } } diff --git a/src/apps/fuse/callbacks/TrashFileCallback.ts b/src/apps/fuse/callbacks/TrashFileCallback.ts index 96597cc6a..007d58603 100644 --- a/src/apps/fuse/callbacks/TrashFileCallback.ts +++ b/src/apps/fuse/callbacks/TrashFileCallback.ts @@ -1,25 +1,42 @@ import { FileStatuses } from '../../../context/virtual-drive/files/domain/FileStatus'; +import { OfflineDriveDependencyContainer } from '../dependency-injection/offline/OfflineDriveDependencyContainer'; import { VirtualDriveDependencyContainer } from '../dependency-injection/virtual-drive/VirtualDriveDependencyContainer'; import { NotifyFuseCallback } from './FuseCallback'; import { FuseIOError, FuseNoSuchFileOrDirectoryError } from './FuseErrors'; export class TrashFileCallback extends NotifyFuseCallback { - constructor(private readonly container: VirtualDriveDependencyContainer) { + constructor( + private readonly virtual: VirtualDriveDependencyContainer, + private readonly offline: OfflineDriveDependencyContainer + ) { super('Trash file'); } async execute(path: string) { - const file = await this.container.filesSearcher.run({ + const file = await this.virtual.filesSearcher.run({ path, status: FileStatuses.EXISTS, }); if (!file) { - return this.left(new FuseNoSuchFileOrDirectoryError(path)); + const offline = await this.offline.offlineFileSearcher.run({ + path, + }); + + if (!offline) { + return this.left(new FuseNoSuchFileOrDirectoryError(path)); + } + + if (offline.isAuxiliary()) { + await this.offline.auxiliarOfflineContentsDeleter.run(offline); + await this.offline.temporalOfflineDeleter.run(offline); + } + + return this.right(); } try { - await this.container.fileDeleter.run(file.contentsId); + await this.virtual.fileDeleter.run(file.contentsId); return this.right(); } catch { diff --git a/src/apps/fuse/callbacks/UploadOnRename.ts b/src/apps/fuse/callbacks/UploadOnRename.ts index 59a656d07..c2cc7f43c 100644 --- a/src/apps/fuse/callbacks/UploadOnRename.ts +++ b/src/apps/fuse/callbacks/UploadOnRename.ts @@ -3,6 +3,7 @@ import { VirtualDriveDependencyContainer } from '../dependency-injection/virtual import { FileStatuses } from '../../../context/virtual-drive/files/domain/FileStatus'; import { Either, right } from '../../../context/shared/domain/Either'; import { FuseError } from './FuseErrors'; +import Logger from 'electron-log'; type Result = 'no-op' | 'success'; @@ -15,16 +16,22 @@ export class UploadOnRename { ) {} async run(src: string, dest: string): Promise> { - const offlineFile = await this.offline.offlineFileSearcher.run({ - path: src, - }); - const virtualFile = await this.virtual.filesSearcher.run({ path: dest, status: FileStatuses.EXISTS, }); - if (!offlineFile || !virtualFile) { + if (!virtualFile) { + Logger.debug('[UPLOAD ON RENAME] virtual file not found', dest); + return right(UploadOnRename.NO_OP); + } + + const offlineFile = await this.offline.offlineFileSearcher.run({ + path: src, + }); + + if (!offlineFile) { + Logger.debug('[UPLOAD ON RENAME] offline file not found', src); return right(UploadOnRename.NO_OP); } diff --git a/src/apps/fuse/callbacks/WriteCallback.ts b/src/apps/fuse/callbacks/WriteCallback.ts index 9303df339..0e35bb90a 100644 --- a/src/apps/fuse/callbacks/WriteCallback.ts +++ b/src/apps/fuse/callbacks/WriteCallback.ts @@ -1,5 +1,4 @@ import { OfflineDriveDependencyContainer } from '../dependency-injection/offline/OfflineDriveDependencyContainer'; -import Logger from 'electron-log'; export class WriteCallback { constructor(private readonly container: OfflineDriveDependencyContainer) {} @@ -12,9 +11,7 @@ export class WriteCallback { pos: number, cb: (a: number) => void ) { - Logger.debug('WRITE: ', path, len, pos); - - await this.container.offlineContentsAppender.run(path, buffer); + await this.container.offlineContentsAppender.run(path, buffer, len, pos); return cb(len); } diff --git a/src/apps/fuse/dependency-injection/offline/OfflineContents/OfflineDriveDependencyContainer.ts b/src/apps/fuse/dependency-injection/offline/OfflineContents/OfflineDriveDependencyContainer.ts index cc19cc88b..1fc59d098 100644 --- a/src/apps/fuse/dependency-injection/offline/OfflineContents/OfflineDriveDependencyContainer.ts +++ b/src/apps/fuse/dependency-injection/offline/OfflineContents/OfflineDriveDependencyContainer.ts @@ -3,6 +3,8 @@ import { ContentsChunkReader } from '../../../../../context/offline-drive/conten import { OfflineContentsAppender } from '../../../../../context/offline-drive/contents/application/OfflineContentsAppender'; import { OfflineContentsCreator } from '../../../../../context/offline-drive/contents/application/OfflineContentsCreator'; import { OfflineContentsUploader } from '../../../../../context/offline-drive/contents/application/OfflineContentsUploader'; +import { AuxiliarOfflineContentsChucksReader } from '../../../../../context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsChucksReader'; +import { AuxiliarOfflineContentsDeleter } from '../../../../../context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsDeleter'; export interface OfflineContentsDependencyContainer { offlineContentsCreator: OfflineContentsCreator; @@ -10,4 +12,6 @@ export interface OfflineContentsDependencyContainer { offlineContentsUploader: OfflineContentsUploader; contentsChunkReader: ContentsChunkReader; offlineContentsCacheCleaner: OfflineContentsCacheCleaner; + auxiliarOfflineContentsChucksReader: AuxiliarOfflineContentsChucksReader; + auxiliarOfflineContentsDeleter: AuxiliarOfflineContentsDeleter; } diff --git a/src/apps/fuse/dependency-injection/offline/OfflineContents/offlineContentsContainerBuilder.ts b/src/apps/fuse/dependency-injection/offline/OfflineContents/offlineContentsContainerBuilder.ts index 13f77c27b..f7f8f955e 100644 --- a/src/apps/fuse/dependency-injection/offline/OfflineContents/offlineContentsContainerBuilder.ts +++ b/src/apps/fuse/dependency-injection/offline/OfflineContents/offlineContentsContainerBuilder.ts @@ -3,9 +3,10 @@ import { OfflineContentsAppender } from '../../../../../context/offline-drive/co import { OfflineContentsCacheCleaner } from '../../../../../context/offline-drive/contents/application/OfflineContentsCacheCleaner'; import { OfflineContentsCreator } from '../../../../../context/offline-drive/contents/application/OfflineContentsCreator'; import { OfflineContentsUploader } from '../../../../../context/offline-drive/contents/application/OfflineContentsUploader'; +import { AuxiliarOfflineContentsDeleter } from '../../../../../context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsDeleter'; +import { AuxiliarOfflineContentsChucksReader } from '../../../../../context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsChucksReader'; import { EnvironmentOfflineContentsManagersFactory } from '../../../../../context/offline-drive/contents/infrastructure/EnvironmentRemoteFileContentsManagersFactory'; import { NodeFSOfflineContentsRepository } from '../../../../../context/offline-drive/contents/infrastructure/NodeFSOfflineContentsRepository'; -import { CachedFSContentsRepository } from '../../../../../context/offline-drive/contents/infrastructure/cache/CachedFSContentsRepository'; import { MainProcessUploadProgressTracker } from '../../../../../context/shared/infrastructure/MainProcessUploadProgressTracker'; import { FuseAppDataLocalFileContentsDirectoryProvider } from '../../../../../context/virtual-drive/shared/infrastructure/LocalFileContentsDirectoryProviders/FuseAppDataLocalFileContentsDirectoryProvider'; import { DependencyInjectionEventBus } from '../../../../fuse/dependency-injection/common/eventBus'; @@ -53,11 +54,17 @@ export async function buildOfflineContentsContainer( const offlineContentsCreator = new OfflineContentsCreator(repository); - const contentsRepository = new CachedFSContentsRepository(); - const contentsChunkReader = new ContentsChunkReader(contentsRepository); + const contentsChunkReader = new ContentsChunkReader(repository); const offlineContentsCacheCleaner = new OfflineContentsCacheCleaner( - contentsRepository + repository + ); + + const auxiliarOfflineContentsChucksReader = + new AuxiliarOfflineContentsChucksReader(repository); + + const auxiliarOfflineContentsDeleter = new AuxiliarOfflineContentsDeleter( + repository ); return { @@ -66,5 +73,7 @@ export async function buildOfflineContentsContainer( offlineContentsUploader, contentsChunkReader, offlineContentsCacheCleaner, + auxiliarOfflineContentsChucksReader, + auxiliarOfflineContentsDeleter, }; } diff --git a/src/apps/fuse/dependency-injection/offline/OfflineFiles/OfflineFilesContainer.ts b/src/apps/fuse/dependency-injection/offline/OfflineFiles/OfflineFilesContainer.ts index 5b9932a62..a77f8f3dc 100644 --- a/src/apps/fuse/dependency-injection/offline/OfflineFiles/OfflineFilesContainer.ts +++ b/src/apps/fuse/dependency-injection/offline/OfflineFiles/OfflineFilesContainer.ts @@ -1,8 +1,10 @@ import { ClearOfflineFileOnFileCreated } from '../../../../../context/offline-drive/files/application/ClearOfflineFileOnFileCreated'; import { OfflineFileCreator } from '../../../../../context/offline-drive/files/application/OfflineFileCreator'; import { OfflineFileFinder } from '../../../../../context/offline-drive/files/application/OfflineFileFinder'; +import { OfflineFilesByParentPathLister } from '../../../../../context/offline-drive/files/application/OfflineFileListerByParentFolder'; import { OfflineFileSearcher } from '../../../../../context/offline-drive/files/application/OfflineFileSearcher'; import { OfflineFileSizeIncreaser } from '../../../../../context/offline-drive/files/application/OfflineFileSizeIncreaser'; +import { TemporalOfflineDeleter } from '../../../../../context/offline-drive/files/application/TemporalOfflineDeleter'; export interface OfflineFilesContainer { offlineFileCreator: OfflineFileCreator; @@ -10,4 +12,6 @@ export interface OfflineFilesContainer { offlineFileFinder: OfflineFileFinder; offlineFileSizeIncreaser: OfflineFileSizeIncreaser; clearOfflineFileOnFileCreated: ClearOfflineFileOnFileCreated; + offlineFilesByParentPathLister: OfflineFilesByParentPathLister; + temporalOfflineDeleter: TemporalOfflineDeleter; } diff --git a/src/apps/fuse/dependency-injection/offline/OfflineFiles/builder.ts b/src/apps/fuse/dependency-injection/offline/OfflineFiles/builder.ts index c408e5781..3d9c848eb 100644 --- a/src/apps/fuse/dependency-injection/offline/OfflineFiles/builder.ts +++ b/src/apps/fuse/dependency-injection/offline/OfflineFiles/builder.ts @@ -7,6 +7,8 @@ import { OfflineFileCreator } from '../../../../../context/offline-drive/files/a import { DependencyInjectionEventBus } from '../../common/eventBus'; import { ClearOfflineFileOnFileCreated } from '../../../../../context/offline-drive/files/application/ClearOfflineFileOnFileCreated'; import { OfflineFileDeleter } from '../../../../../context/offline-drive/files/application/OfflineFileDeleter'; +import { OfflineFilesByParentPathLister } from '../../../../../context/offline-drive/files/application/OfflineFileListerByParentFolder'; +import { TemporalOfflineDeleter } from '../../../../../context/offline-drive/files/application/TemporalOfflineDeleter'; export async function buildOfflineFilesContainer(): Promise { const { bus: eventBus } = DependencyInjectionEventBus; @@ -23,6 +25,12 @@ export async function buildOfflineFilesContainer(): Promise { + async run( + path: string, + buffer: Buffer, + length: number, + position: number + ): Promise { const file = await this.offlineFileFinder.run({ path }); try { - await this.contentsRepository.writeToFile(file.id, buffer); + await this.contentsRepository.writeToFile( + file.id, + buffer, + length, + position + ); } catch (error: unknown) { throw new OfflineContentsIOError(); } diff --git a/src/context/offline-drive/contents/application/OfflineContentsCacheCleaner.ts b/src/context/offline-drive/contents/application/OfflineContentsCacheCleaner.ts index 89cd1557a..91cd56828 100644 --- a/src/context/offline-drive/contents/application/OfflineContentsCacheCleaner.ts +++ b/src/context/offline-drive/contents/application/OfflineContentsCacheCleaner.ts @@ -1,7 +1,7 @@ -import { ContentsRepository } from '../domain/ContentsRepository'; +import { OfflineContentsRepository } from '../domain/OfflineContentsRepository'; export class OfflineContentsCacheCleaner { - constructor(private readonly repository: ContentsRepository) {} + constructor(private readonly repository: OfflineContentsRepository) {} run(contentsPath: string): Promise { return this.repository.forget(contentsPath); diff --git a/src/context/offline-drive/contents/application/OfflineContentsUploader.ts b/src/context/offline-drive/contents/application/OfflineContentsUploader.ts index 756bd1bf4..dbf4cb898 100644 --- a/src/context/offline-drive/contents/application/OfflineContentsUploader.ts +++ b/src/context/offline-drive/contents/application/OfflineContentsUploader.ts @@ -4,6 +4,7 @@ import { OfflineContentsRepository } from '../domain/OfflineContentsRepository'; import { OfflineContentsUploadedDomainEvent } from '../domain/events/OfflineContentsUploadedDomainEvent'; import { FilePath } from '../../../virtual-drive/files/domain/FilePath'; import { OfflineContentsName } from '../domain/OfflineContentsName'; +import Logger from 'electron-log'; export class OfflineContentsUploader { constructor( @@ -17,7 +18,8 @@ export class OfflineContentsUploader { path: FilePath, replaces?: string ): Promise { - const { contents, stream, abortSignal } = await this.repository.read(name); + const { contents, stream, abortSignal } = + await this.repository.createStream(name); const uploader = this.contentsManagersFactory.uploader( stream, @@ -31,6 +33,8 @@ export class OfflineContentsUploader { const contentsId = await uploader(); + Logger.debug(`${path.value} uploaded with id ${contentsId}`); + const contentsUploadedEvent = new OfflineContentsUploadedDomainEvent({ aggregateId: contentsId, offlineContentsPath: contents.absolutePath, diff --git a/src/context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsChucksReader.ts b/src/context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsChucksReader.ts new file mode 100644 index 000000000..46fdfda82 --- /dev/null +++ b/src/context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsChucksReader.ts @@ -0,0 +1,26 @@ +import { Optional } from '../../../../../shared/types/Optional'; +import { OfflineFileId } from '../../../files/domain/OfflineFileId'; +import { OfflineContentsRepository } from '../../domain/OfflineContentsRepository'; +import Logger from 'electron-log'; + +export class AuxiliarOfflineContentsChucksReader { + constructor(private readonly repository: OfflineContentsRepository) {} + + async run( + id: OfflineFileId, + length: number, + position: number + ): Promise> { + const data = await this.repository.readFromId(id); + + if (position >= data.length) { + return Optional.empty(); + } + + const chunk = data.slice(position, position + length); + + Logger.debug('Read from auxiliar file', id); + + return Optional.of(chunk); + } +} diff --git a/src/context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsDeleter.ts b/src/context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsDeleter.ts new file mode 100644 index 000000000..8727577a0 --- /dev/null +++ b/src/context/offline-drive/contents/application/auxiliar/AuxiliarOfflineContentsDeleter.ts @@ -0,0 +1,13 @@ +import { OfflineFile } from '../../../files/domain/OfflineFile'; +import { OfflineContentsRepository } from '../../domain/OfflineContentsRepository'; +import Logger from 'electron-log'; + +export class AuxiliarOfflineContentsDeleter { + constructor(private readonly repository: OfflineContentsRepository) {} + + async run(offlineFile: OfflineFile): Promise { + await this.repository.remove(offlineFile.id); + + Logger.debug('Temporal file ', offlineFile.id, 'deleted'); + } +} diff --git a/src/context/offline-drive/contents/domain/ContentsRepository.ts b/src/context/offline-drive/contents/domain/ContentsRepository.ts deleted file mode 100644 index 2c532128b..000000000 --- a/src/context/offline-drive/contents/domain/ContentsRepository.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ContentsRepository { - read(path: string): Promise; - - forget(path: string): Promise; -} diff --git a/src/context/offline-drive/contents/domain/OfflineContentsRepository.ts b/src/context/offline-drive/contents/domain/OfflineContentsRepository.ts index 845dd9bcf..a75a80ad4 100644 --- a/src/context/offline-drive/contents/domain/OfflineContentsRepository.ts +++ b/src/context/offline-drive/contents/domain/OfflineContentsRepository.ts @@ -2,17 +2,31 @@ import { Readable } from 'stream'; import { OfflineFile } from '../../files/domain/OfflineFile'; import { OfflineContents } from './OfflineContents'; import { OfflineContentsName } from './OfflineContentsName'; +import { OfflineFileId } from '../../files/domain/OfflineFileId'; export interface OfflineContentsRepository { - writeToFile(id: OfflineFile['id'], buffer: Buffer): Promise; + writeToFile( + id: OfflineFile['id'], + buffer: Buffer, + length: number, + position: number + ): Promise; createEmptyFile(id: OfflineFile['id']): Promise; getAbsolutePath(id: OfflineContentsName): Promise; - read: (offlineContentsName: OfflineContentsName) => Promise<{ + createStream: (offlineContentsName: OfflineContentsName) => Promise<{ contents: OfflineContents; stream: Readable; abortSignal: AbortSignal; }>; + + read(path: string): Promise; + + readFromId(id: OfflineFileId): Promise; + + forget(path: string): Promise; + + remove(id: OfflineFileId): Promise; } diff --git a/src/context/offline-drive/contents/infrastructure/EnvironmentRemoteFileContentsManagersFactory.ts b/src/context/offline-drive/contents/infrastructure/EnvironmentRemoteFileContentsManagersFactory.ts index 727012d06..5be51026d 100644 --- a/src/context/offline-drive/contents/infrastructure/EnvironmentRemoteFileContentsManagersFactory.ts +++ b/src/context/offline-drive/contents/infrastructure/EnvironmentRemoteFileContentsManagersFactory.ts @@ -7,6 +7,7 @@ import { EnvironmentOfflineContentsUploader } from './EnvironmentOfflineContents import { OfflineContents } from '../domain/OfflineContents'; import { UploadProgressTracker } from '../../../shared/domain/UploadProgressTracker'; import { Readable } from 'stream'; +import Logger from 'electron-log'; export class EnvironmentOfflineContentsManagersFactory implements OfflineContentsManagersFactory @@ -54,8 +55,9 @@ export class EnvironmentOfflineContentsManagersFactory }); }); - uploader.on('error', (_error: Error) => { + uploader.on('error', (error: Error) => { // TODO: use error to determine the cause + Logger.debug('UPLOADER ERROR', error); this.progressTracker.uploadError(name, extension, 'UNKNOWN'); }); diff --git a/src/context/offline-drive/contents/infrastructure/NodeFSOfflineContentsRepository.ts b/src/context/offline-drive/contents/infrastructure/NodeFSOfflineContentsRepository.ts index fd0cf8537..3255b8c05 100644 --- a/src/context/offline-drive/contents/infrastructure/NodeFSOfflineContentsRepository.ts +++ b/src/context/offline-drive/contents/infrastructure/NodeFSOfflineContentsRepository.ts @@ -1,13 +1,14 @@ -import fs, { createReadStream, watch } from 'fs'; -import { stat as statPromises } from 'fs/promises'; +import fs, { createReadStream, unlink, watch } from 'fs'; +import { readFile, stat as statPromises } from 'fs/promises'; import { OfflineContentsRepository } from '../domain/OfflineContentsRepository'; import { OfflineFile } from '../../files/domain/OfflineFile'; import { LocalFileContentsDirectoryProvider } from '../../../virtual-drive/shared/domain/LocalFileContentsDirectoryProvider'; -import path from 'path'; +import { basename, dirname, join } from 'path'; import Logger from 'electron-log'; import { Readable } from 'stream'; import { OfflineContents } from '../domain/OfflineContents'; import { OfflineContentsName } from '../domain/OfflineContentsName'; +import { OfflineFileId } from '../../files/domain/OfflineFileId'; export class NodeFSOfflineContentsRepository implements OfflineContentsRepository @@ -20,13 +21,13 @@ export class NodeFSOfflineContentsRepository private async folderPath(): Promise { const location = await this.locationProvider.provide(); - return path.join(location, this.subfolder); + return join(location, this.subfolder); } private async filePath(name: OfflineContentsName): Promise { const folder = await this.folderPath(); - return path.join(folder, name.value); + return join(folder, name.value); } private createAbortableStream(filePath: string): { @@ -46,10 +47,24 @@ export class NodeFSOfflineContentsRepository fs.mkdirSync(folder, { recursive: true }); } - async writeToFile(id: OfflineFile['id'], buffer: Buffer): Promise { + async writeToFile( + id: OfflineFile['id'], + buffer: Buffer, + length: number, + position: number + ): Promise { const file = await this.filePath(id); - fs.appendFileSync(file, buffer); + // Open the file in write mode with the 'r+' flag to allow reading and writing. + const fd = fs.openSync(file, 'r+'); + + try { + // Write the buffer to the file at the specified position. + fs.writeSync(fd, buffer, 0, length, position); + } finally { + // Close the file descriptor to release resources. + fs.closeSync(fd); + } } async createEmptyFile(id: OfflineFile['id']): Promise { @@ -71,7 +86,7 @@ export class NodeFSOfflineContentsRepository return this.filePath(id); } - async read(offlineContentsName: OfflineContentsName): Promise<{ + async createStream(offlineContentsName: OfflineContentsName): Promise<{ contents: OfflineContents; stream: Readable; abortSignal: AbortSignal; @@ -83,8 +98,8 @@ export class NodeFSOfflineContentsRepository const { size, mtimeMs, birthtimeMs } = await statPromises(absoluteFilePath); - const absoluteFolderPath = path.dirname(absoluteFilePath); - const nameWithExtension = path.basename(absoluteFilePath); + const absoluteFolderPath = dirname(absoluteFilePath); + const nameWithExtension = basename(absoluteFilePath); const watcher = watch(absoluteFolderPath, (_, filename) => { if (filename !== nameWithExtension) { @@ -116,4 +131,63 @@ export class NodeFSOfflineContentsRepository abortSignal: controller.signal, }; } + + private buffers: Map = new Map(); + + async read(path: string): Promise { + const cached = this.buffers.get(path); + + if (cached) { + return cached; + } + + const read = await readFile(path); + this.buffers.set(path, read); + + return read; + } + + async forget(path: string): Promise { + const deleted = this.buffers.delete(path); + + if (deleted) { + Logger.debug(`Buffer from ${basename(path)} deleted from cache`); + } + } + + async readFromId(id: OfflineFileId): Promise { + const path = await this.getAbsolutePath(id); + + const cached = this.buffers.get(path); + + if (cached) { + return cached; + } + + const read = await readFile(path); + this.buffers.set(path, read); + + return read; + } + + async remove(id: OfflineFileId): Promise { + const path = await this.getAbsolutePath(id); + + return new Promise((resolve, reject) => { + unlink(path, (err: NodeJS.ErrnoException | null) => { + if (err) { + if (err.code !== 'ENOENT') { + Logger.debug(`Could not delete ${id}, it already does not exists`); + resolve(); + return; + } + + reject(err); + return; + } + + resolve(); + }); + }); + } } diff --git a/src/context/offline-drive/contents/infrastructure/cache/CachedFSContentsRepository.ts b/src/context/offline-drive/contents/infrastructure/cache/CachedFSContentsRepository.ts index 921068bce..bd2314b6b 100644 --- a/src/context/offline-drive/contents/infrastructure/cache/CachedFSContentsRepository.ts +++ b/src/context/offline-drive/contents/infrastructure/cache/CachedFSContentsRepository.ts @@ -1,8 +1,8 @@ -import { ContentsRepository } from '../../domain/ContentsRepository'; import fs from 'fs/promises'; import Logger from 'electron-log'; import { basename } from 'path'; -export class CachedFSContentsRepository implements ContentsRepository { + +export class CachedFSContentsRepository { private buffers: Map = new Map(); async read(path: string): Promise { diff --git a/src/context/offline-drive/files/application/OfflineFileListerByParentFolder.ts b/src/context/offline-drive/files/application/OfflineFileListerByParentFolder.ts new file mode 100644 index 000000000..10fe1eb5f --- /dev/null +++ b/src/context/offline-drive/files/application/OfflineFileListerByParentFolder.ts @@ -0,0 +1,15 @@ +import { dirname } from 'path'; +import { OfflineFile } from '../domain/OfflineFile'; +import { OfflineFileRepository } from '../domain/OfflineFileRepository'; + +export class OfflineFilesByParentPathLister { + constructor(private readonly repository: OfflineFileRepository) {} + + async run(path: string): Promise> { + const parentPath = dirname(path); + + const all = await this.repository.all(); + + return all.filter((file) => file.path.dirname() === parentPath); + } +} diff --git a/src/context/offline-drive/files/application/TemporalOfflineDeleter.ts b/src/context/offline-drive/files/application/TemporalOfflineDeleter.ts new file mode 100644 index 000000000..cdfd39d6f --- /dev/null +++ b/src/context/offline-drive/files/application/TemporalOfflineDeleter.ts @@ -0,0 +1,10 @@ +import { OfflineFile } from '../domain/OfflineFile'; +import { OfflineFileRepository } from '../domain/OfflineFileRepository'; + +export class TemporalOfflineDeleter { + constructor(private readonly repository: OfflineFileRepository) {} + + async run(file: OfflineFile): Promise { + return this.repository.delete(file.id); + } +} diff --git a/src/context/offline-drive/files/domain/OfflineFile.ts b/src/context/offline-drive/files/domain/OfflineFile.ts index 906a2605e..5f6ed5b98 100644 --- a/src/context/offline-drive/files/domain/OfflineFile.ts +++ b/src/context/offline-drive/files/domain/OfflineFile.ts @@ -11,6 +11,9 @@ export type OfflineFileAttributes = { }; export class OfflineFile extends AggregateRoot { + private static readonly TEMPORAL_EXTENSION = 'tmp'; + private static readonly LOCK_FILE_NAME_PREFIX = '.~lock.'; + private constructor( private _id: OfflineFileId, private _createdAt: Date, @@ -63,6 +66,21 @@ export class OfflineFile extends AggregateRoot { this._size = this._size.increment(bytes); } + isAuxiliary(): boolean { + const isLockFile = this.isLockFile(); + const isTemporal = this.isTemporal(); + + return isLockFile || isTemporal; + } + + isLockFile(): boolean { + return this.name.startsWith(OfflineFile.LOCK_FILE_NAME_PREFIX); + } + + isTemporal(): boolean { + return this.extension === OfflineFile.TEMPORAL_EXTENSION; + } + attributes(): OfflineFileAttributes { return { id: this._id.value, diff --git a/src/context/offline-drive/files/domain/OfflineFileRepository.ts b/src/context/offline-drive/files/domain/OfflineFileRepository.ts index ccfdd8ec1..9c4edc59b 100644 --- a/src/context/offline-drive/files/domain/OfflineFileRepository.ts +++ b/src/context/offline-drive/files/domain/OfflineFileRepository.ts @@ -8,4 +8,6 @@ export interface OfflineFileRepository { ): Promise; delete(id: OfflineFile['id']): Promise; + + all(): Promise>; } diff --git a/src/context/offline-drive/files/domain/OfflineFileSize.ts b/src/context/offline-drive/files/domain/OfflineFileSize.ts index 7446410e9..500daa1bb 100644 --- a/src/context/offline-drive/files/domain/OfflineFileSize.ts +++ b/src/context/offline-drive/files/domain/OfflineFileSize.ts @@ -1,6 +1,24 @@ import { BucketEntry } from '../../../shared/domain/value-objects/BucketEntry'; +import { ValueObject } from '../../../shared/domain/value-objects/ValueObject'; + +export class OfflineFileSize extends ValueObject { + public static MAX_SIZE = BucketEntry.MAX_SIZE; + + constructor(value: number) { + super(value); + this.ensureIsValid(value); + } + + private ensureIsValid(value: number) { + if (value > BucketEntry.MAX_SIZE) { + throw new Error('Offline File size to big'); + } + + if (value < 0) { + throw new Error('Offline File size cannot be negative'); + } + } -export class OfflineFileSize extends BucketEntry { increment(bytes: number): OfflineFileSize { return new OfflineFileSize(this.value + bytes); } diff --git a/src/context/offline-drive/files/infrastructure/InMemoryOfflineFileRepository.ts b/src/context/offline-drive/files/infrastructure/InMemoryOfflineFileRepository.ts index 88d15cd37..4a6ea3c82 100644 --- a/src/context/offline-drive/files/infrastructure/InMemoryOfflineFileRepository.ts +++ b/src/context/offline-drive/files/infrastructure/InMemoryOfflineFileRepository.ts @@ -17,6 +17,8 @@ export class InMemoryOfflineFileRepository implements OfflineFileRepository { const values = Array.from(this.files.values()); + // Logger.debug(values); + const file = values.find((attributes) => { return keys.every( (key: keyof OfflineFileAttributes) => attributes[key] == partial[key] @@ -33,4 +35,11 @@ export class InMemoryOfflineFileRepository implements OfflineFileRepository { async delete(id: OfflineFile['id']): Promise { this.files.delete(id.value); } + + async all(): Promise> { + const fileAttributes = Array.from(this.files); + return fileAttributes.map(([_, attributes]) => + OfflineFile.from(attributes) + ); + } } diff --git a/src/context/shared/domain/value-objects/BucketEntry.ts b/src/context/shared/domain/value-objects/BucketEntry.ts index fba1e19dd..2ba6206de 100644 --- a/src/context/shared/domain/value-objects/BucketEntry.ts +++ b/src/context/shared/domain/value-objects/BucketEntry.ts @@ -16,5 +16,9 @@ export class BucketEntry extends ValueObject { if (value < 0) { throw new Error('File size cannot be negative'); } + + // if (value === 0) { + // throw new Error('File size cannot be zero'); + // } } } diff --git a/src/context/virtual-drive/contents/application/LocalContentsMover.ts b/src/context/virtual-drive/contents/application/LocalContentsMover.ts index ebc267591..d1ced002e 100644 --- a/src/context/virtual-drive/contents/application/LocalContentsMover.ts +++ b/src/context/virtual-drive/contents/application/LocalContentsMover.ts @@ -7,13 +7,10 @@ export class LocalContentsMover { async run(contentsId: ContentsId, src: string): Promise { const exists = await this.fileSystem.exists(contentsId); - if (exists) { this.fileSystem.remove(contentsId); } - await this.fileSystem.add(contentsId, src); - Logger.info('Added', contentsId.value, 'to offline files contents cache'); } } diff --git a/src/context/virtual-drive/contents/application/MoveOfflineContentsOnContentsUploaded.ts b/src/context/virtual-drive/contents/application/MoveOfflineContentsOnContentsUploaded.ts index e8f43c183..0020b7ebd 100644 --- a/src/context/virtual-drive/contents/application/MoveOfflineContentsOnContentsUploaded.ts +++ b/src/context/virtual-drive/contents/application/MoveOfflineContentsOnContentsUploaded.ts @@ -10,7 +10,7 @@ export class MoveOfflineContentsOnContentsUploaded constructor(private readonly mover: LocalContentsMover) {} subscribedTo(): DomainEventClass[] { - return [OfflineContentsUploadedDomainEvent]; + return []; // Disabled because moving the contents created an error while editing some files } async on(domainEvent: OfflineContentsUploadedDomainEvent): Promise { const contentsId = new ContentsId(domainEvent.aggregateId); diff --git a/src/context/virtual-drive/files/application/FileCreator.ts b/src/context/virtual-drive/files/application/FileCreator.ts index b8936e621..ec0e64910 100644 --- a/src/context/virtual-drive/files/application/FileCreator.ts +++ b/src/context/virtual-drive/files/application/FileCreator.ts @@ -63,7 +63,7 @@ export class FileCreator { } catch (error: unknown) { const message = error instanceof Error ? error.message : 'unknown error'; - Logger.error('[File Creator]', message); + Logger.error(`[File Creator] ${path}`, message); const cause = error instanceof DriveDesktopError ? error.syncErrorCause : 'UNKNOWN'; diff --git a/tests/context/offline-drive/files/__mocks__/OfflineFileRepositoryMock.ts b/tests/context/offline-drive/files/__mocks__/OfflineFileRepositoryMock.ts index f33fa6350..819a04b4a 100644 --- a/tests/context/offline-drive/files/__mocks__/OfflineFileRepositoryMock.ts +++ b/tests/context/offline-drive/files/__mocks__/OfflineFileRepositoryMock.ts @@ -9,6 +9,7 @@ export class OfflineFileRepositoryMock implements OfflineFileRepository { public saveMock = jest.fn(); public searchByPartialMock = jest.fn(); public deleteMock = jest.fn(); + public allMock = jest.fn(); save(file: OfflineFile): Promise { return this.saveMock(file); @@ -22,4 +23,8 @@ export class OfflineFileRepositoryMock implements OfflineFileRepository { delete(id: OfflineFileId): Promise { return this.deleteMock(id); } + + all(): Promise { + return this.allMock(); + } }