import { api } from 'e2e/client'; import fs from 'node:fs/promises'; import path from 'node:path'; import { LoginResponseDto } from 'src/dtos/auth.dto'; import { LibraryResponseDto } from 'src/dtos/library.dto'; import { AssetType } from 'src/entities/asset.entity'; import { LibraryType } from 'src/entities/library.entity'; import { StorageEventType } from 'src/interfaces/storage.interface'; import { LibraryService } from 'src/services/library.service'; import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp, waitForEvent, } from 'test/utils'; describe(`Library watcher (e2e)`, () => { let server: any; let admin: LoginResponseDto; let libraryService: LibraryService; const configFilePath = process.env.IMMICH_CONFIG_FILE; beforeAll(async () => { process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/../config/library-watcher-e2e-config.json`); const app = await testApp.create(); server = app.getHttpServer(); libraryService = testApp.get(LibraryService); }); beforeEach(async () => { await testApp.reset(); await restoreTempFolder(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); }); afterEach(async () => { await libraryService.teardown(); }); afterAll(async () => { await testApp.teardown(); await restoreTempFolder(); process.env.IMMICH_CONFIG_FILE = configFilePath; }); describe('Event handling', () => { describe('Single import path', () => { beforeEach(async () => { await api.libraryApi.create(server, admin.accessToken, { ownerId: admin.userId, type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); }); it('should import a new file', async () => { await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); await waitForEvent(libraryService, StorageEventType.ADD); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(1); }); it('should import new files with case insensitive extensions', async () => { await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file2.JPG`, ); await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file3.Jpg`, ); await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file4.jpG`, ); await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file5.jPg`, ); await waitForEvent(libraryService, StorageEventType.ADD, 4); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(4); }); it('should update a changed file', async () => { await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); await waitForEvent(libraryService, StorageEventType.ADD); const originalAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(originalAssets.length).toEqual(1); await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/prairie_falcon.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); await waitForEvent(libraryService, StorageEventType.CHANGE); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets).toEqual([ expect.objectContaining({ // Make sure we keep the original asset id id: originalAssets[0].id, type: AssetType.IMAGE, exifInfo: expect.objectContaining({ make: 'Canon', model: 'Canon EOS R5', exifImageWidth: 800, exifImageHeight: 533, exposureTime: '1/4000', }), }), ]); }); }); describe('Multiple import paths', () => { beforeEach(async () => { await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true }); await api.libraryApi.create(server, admin.accessToken, { ownerId: admin.userId, type: LibraryType.EXTERNAL, importPaths: [ `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, ], }); }); it('should add new files in multiple import paths', async () => { await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file2.jpg`, ); await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir2/file3.jpg`, ); await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3/file4.jpg`, ); await waitForEvent(libraryService, StorageEventType.ADD, 3); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets.length).toEqual(3); }); it('should offline a removed file', async () => { await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`, ); await waitForEvent(libraryService, StorageEventType.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, StorageEventType.UNLINK); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets[0].isOffline).toEqual(true); }); }); }); describe('Configuration', () => { let library: LibraryResponseDto; beforeEach(async () => { library = await api.libraryApi.create(server, admin.accessToken, { ownerId: admin.userId, type: LibraryType.EXTERNAL, importPaths: [ `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, ], }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true }); await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true }); }); it('should use an updated import path', async () => { await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, { recursive: true }); await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [ `${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, ]); await fs.copyFile( `${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/dir4/file.jpg`, ); await waitForEvent(libraryService, StorageEventType.ADD); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(1); }); }); });