diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts index 1dc68ad12c..8ee83442f4 100644 --- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts +++ b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LibraryResponseDto, LibraryService, LoginResponseDto } from '@app/domain'; +import { LibraryResponseDto, LibraryService, LoginResponseDto, StorageEvent } from '@app/domain'; import { AssetType, LibraryType } from '@app/infra/entities'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -57,7 +57,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEvent.ADD); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(1); @@ -84,10 +84,10 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file5.jPg`, ); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEvent.ADD); + await waitForEvent(libraryService, StorageEvent.ADD); + await waitForEvent(libraryService, StorageEvent.ADD); + await waitForEvent(libraryService, StorageEvent.ADD); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(4); @@ -99,7 +99,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEvent.ADD); const originalAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(originalAssets.length).toEqual(1); @@ -109,7 +109,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); - await waitForEvent(libraryService, 'change'); + await waitForEvent(libraryService, StorageEvent.CHANGE); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets).toEqual([ @@ -161,9 +161,9 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3/file4.jpg`, ); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEvent.ADD); + await waitForEvent(libraryService, StorageEvent.ADD); + await waitForEvent(libraryService, StorageEvent.ADD); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets.length).toEqual(3); @@ -175,14 +175,14 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`, ); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEvent.ADD); const addedAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(addedAssets.length).toEqual(1); await fs.unlink(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`); - await waitForEvent(libraryService, 'unlink'); + await waitForEvent(libraryService, StorageEvent.UNLINK); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets[0].isOffline).toEqual(true); diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 50b26d158b..45980f286e 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -1312,7 +1312,7 @@ describe(LibraryService.name, () => { storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); await sut.init(); - await sut.tearDown(); + await sut.teardown(); expect(mockClose).toHaveBeenCalledTimes(2); }); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index a9c34195ce..34660a30d5 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -23,6 +23,7 @@ import { IStorageRepository, ISystemConfigRepository, IUserRepository, + StorageEvent, WithProperty, } from '../repositories'; import { SystemConfigCore } from '../system-config'; @@ -141,7 +142,7 @@ export class LibraryService extends EventEmitter { if (matcher(path)) { await this.scanAssets(library.id, [path], library.ownerId, false); } - this.emit('add', path); + this.emit(StorageEvent.ADD, path); }, onChange: async (path) => { this.logger.debug(`Detected file change for ${path} in library ${library.id}`); @@ -149,7 +150,7 @@ export class LibraryService extends EventEmitter { // Note: if the changed file was not previously imported, it will be imported now. await this.scanAssets(library.id, [path], library.ownerId, false); } - this.emit('change', path); + this.emit(StorageEvent.CHANGE, path); }, onUnlink: async (path) => { this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); @@ -157,11 +158,12 @@ export class LibraryService extends EventEmitter { if (asset && matcher(path)) { await this.assetRepository.save({ id: asset.id, isOffline: true }); } - this.emit('unlink', path); + this.emit(StorageEvent.UNLINK, path); }, onError: (error) => { // TODO: should we log, or throw an exception? this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`); + this.emit(StorageEvent.ERROR, error); }, }, ); diff --git a/server/src/domain/repositories/storage.repository.ts b/server/src/domain/repositories/storage.repository.ts index c88095b17b..b07fa78f82 100644 --- a/server/src/domain/repositories/storage.repository.ts +++ b/server/src/domain/repositories/storage.repository.ts @@ -31,6 +31,14 @@ export interface WatchEvents { onError(error: Error): void; } +export enum StorageEvent { + READY = 'ready', + ADD = 'add', + CHANGE = 'change', + UNLINK = 'unlink', + ERROR = 'error', +} + export interface IStorageRepository { createZipStream(): ImmichZipStream; createReadStream(filepath: string, mimeType?: string | null): Promise; diff --git a/server/src/infra/repositories/filesystem.provider.ts b/server/src/infra/repositories/filesystem.provider.ts index 9398a90462..6b306d8f2d 100644 --- a/server/src/infra/repositories/filesystem.provider.ts +++ b/server/src/infra/repositories/filesystem.provider.ts @@ -1,11 +1,12 @@ import { CrawlOptionsDto, DiskUsage, + IStorageRepository, ImmichReadStream, ImmichZipStream, - IStorageRepository, - mimeTypes, + StorageEvent, WatchEvents, + mimeTypes, } from '@app/domain'; import { ImmichLogger } from '@app/infra/logger'; import archiver from 'archiver'; @@ -141,11 +142,11 @@ export class FilesystemProvider implements IStorageRepository { watch(paths: string[], options: WatchOptions, events: Partial) { const watcher = chokidar.watch(paths, options); - watcher.on('ready', () => events.onReady?.()); - watcher.on('add', (path) => events.onAdd?.(path)); - watcher.on('change', (path) => events.onChange?.(path)); - watcher.on('unlink', (path) => events.onUnlink?.(path)); - watcher.on('error', (error) => events.onError?.(error)); + watcher.on(StorageEvent.READY, () => events.onReady?.()); + watcher.on(StorageEvent.ADD, (path) => events.onAdd?.(path)); + watcher.on(StorageEvent.CHANGE, (path) => events.onChange?.(path)); + watcher.on(StorageEvent.UNLINK, (path) => events.onUnlink?.(path)); + watcher.on(StorageEvent.ERROR, (error) => events.onError?.(error)); return () => watcher.close(); } diff --git a/server/src/test-utils/utils.ts b/server/src/test-utils/utils.ts index b13a1af732..5f8a6dcfbc 100644 --- a/server/src/test-utils/utils.ts +++ b/server/src/test-utils/utils.ts @@ -1,4 +1,4 @@ -import { IJobRepository, IMediaRepository, JobItem, JobItemHandler, QueueName } from '@app/domain'; +import { IJobRepository, IMediaRepository, JobItem, JobItemHandler, QueueName, StorageEvent } from '@app/domain'; import { AppModule } from '@app/immich'; import { InfraModule, InfraTestModule, dataSource } from '@app/infra'; import { MediaRepository } from '@app/infra/repositories'; @@ -141,7 +141,7 @@ export const testApp = { export function waitForEvent(emitter: EventEmitter, event: string): Promise { return new Promise((resolve, reject) => { const success = (value: T) => { - emitter.off('error', fail); + emitter.off(StorageEvent.ERROR, fail); resolve(value); }; const fail = (error: Error) => { @@ -149,7 +149,7 @@ export function waitForEvent(emitter: EventEmitter, event: string): Promise