From 9fbd6369b9ebfafab51c63c5fa1a760e81d7a359 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 26 Mar 2025 16:10:53 +0100 Subject: [PATCH] fix(server): check asset against multiple import paths (#17128) * fix sql logic * refactor: map import paths into not or sql statements --------- Co-authored-by: Zack Pollard --- e2e/src/api/specs/library.e2e-spec.ts | 127 ++++++++++++++++++++ server/src/repositories/asset.repository.ts | 5 +- 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 7560672727..e08079ebf3 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -454,6 +454,133 @@ describe('/libraries', () => { utils.removeImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`); }); + it('should respect exclusion patterns when using multiple import paths', async () => { + // https://github.com/immich-app/immich/issues/17121 + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/exclusion/`, `${testAssetDirInternal}/temp/exclusion2/`], + }); + + const excludedFolder = `Raw`; + + utils.createImageFile(`${testAssetDir}/temp/exclusion/asset1.png`); + utils.createImageFile(`${testAssetDir}/temp/exclusion/${excludedFolder}/asset2.png`); + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + expect.objectContaining({ originalPath: expect.stringContaining(`${excludedFolder}/asset2.png`) }), + ]), + ); + } + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + expect.objectContaining({ originalPath: expect.stringContaining(`${excludedFolder}/asset2.png`) }), + ]), + ); + } + + await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: [`**/${excludedFolder}/**`] }); + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + ]); + } + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + ]); + } + + utils.removeImageFile(`${testAssetDir}/temp/exclusion/asset1.png`); + utils.removeImageFile(`${testAssetDir}/temp/exclusion/${excludedFolder}/asset2.png`); + }); + + const annoyingExclusionPatterns = ['@', '#', '$', '%', '^', '&', '=']; + + it.each(annoyingExclusionPatterns)('should support exclusion patterns with %s', async (char) => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/exclusion/`], + }); + + const excludedFolder = `${char}folder`; + + utils.createImageFile(`${testAssetDir}/temp/exclusion/asset1.png`); + utils.createImageFile(`${testAssetDir}/temp/exclusion/${excludedFolder}/asset2.png`); + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + expect.objectContaining({ originalPath: expect.stringContaining(`${excludedFolder}/asset2.png`) }), + ]), + ); + } + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + expect.objectContaining({ originalPath: expect.stringContaining(`${excludedFolder}/asset2.png`) }), + ]), + ); + } + + await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: [`**/${excludedFolder}/**`] }); + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + ]); + } + + await utils.scan(admin.accessToken, library.id); + + { + const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual([ + expect.objectContaining({ originalPath: expect.stringContaining(`/asset1.png`) }), + ]); + } + + utils.removeImageFile(`${testAssetDir}/temp/exclusion/asset1.png`); + utils.removeImageFile(`${testAssetDir}/temp/exclusion/${excludedFolder}/asset2.png`); + }); + it('should reimport a modified file', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 94af400d05..896110d39b 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1062,7 +1062,10 @@ export class AssetRepository { .where('isExternal', '=', true) .where('libraryId', '=', asUuid(libraryId)) .where((eb) => - eb.or([eb('originalPath', 'not like', paths.join('|')), eb('originalPath', 'like', exclusions.join('|'))]), + eb.or([ + eb.not(eb.or(paths.map((path) => eb('originalPath', 'like', path)))), + eb('originalPath', 'like', exclusions.join('|')), + ]), ) .executeTakeFirstOrThrow(); }