fix(server): make library exclusion matching consistently case-insensitive (#31973)

fix(server): make library exclusion matching consistently case-insensitive (#31970)

(cherry picked from commit de6dcf669c498192ce73b181ec6d5de5d844b03a)

Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
This commit is contained in:
immich-push-o-matic[bot]
2026-10-01 10:26:56 +00:00
committed by GitHub
co-authored by Jonathan Jogenfors
parent d2136aae98
commit 84fb51a110
5 changed files with 55 additions and 6 deletions
+1 -1
View File
@@ -532,7 +532,7 @@ where
and "libraryId" = $5::uuid
and (
not "originalPath" like $6
or "originalPath" ~ $7
or "originalPath" ~* $7
)
-- AssetRepository.filterNewExternalAssetPaths
+1 -1
View File
@@ -1102,7 +1102,7 @@ export class AssetRepository {
.where((eb) =>
eb.or([
eb.not(eb.or(paths.map((path) => eb('originalPath', 'like', path)))),
eb.or(exclusions.map((pattern) => eb('originalPath', '~', pattern))),
eb.or(exclusions.map((pattern) => eb('originalPath', '~*', pattern))),
]),
)
.executeTakeFirstOrThrow();
+3 -1
View File
@@ -521,7 +521,9 @@ export class LibraryService extends BaseService {
break;
}
const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern));
const isExcluded = job.exclusionPatterns.some((pattern) =>
picomatch.isMatch(asset.originalPath, pattern, { nocase: true }),
);
if (!isExcluded) {
this.logger.debug(`Offline asset ${asset.originalPath} is now online in library ${job.libraryId}`);
@@ -476,6 +476,50 @@ describe(LibraryService.name, () => {
await expect(ctx.getAssetPaths(library.id)).resolves.toEqual([]);
});
// https://github.com/immich-app/immich/issues/23619
it.each([
{ pattern: '**/*.{arw,ARW,dng,DNG}', extensions: ['arw', 'ARW', 'dng', 'DNG'] },
{ pattern: '**/*.{arw,dng}', extensions: ['arw', 'ARW', 'ArW', 'dng', 'DNG', 'DnG'] },
{ pattern: '**/*.ARW', extensions: ['arw', 'ARW', 'ArW'] },
{ pattern: '**/*.{tif,jpg}', extensions: ['tif', 'TIF', 'TiF', 'jpg', 'JPG', 'JpG'] },
])(
'should offline existing assets covered by $pattern regardless of extension case',
async ({ pattern, extensions }) => {
const { sut, ctx } = setup();
const excludedAssets = await Promise.all(
extensions.map((extension, index) => createFile(join(importRoot, 'nested', `asset${index}.${extension}`))),
);
const includedAsset = await createFile(join(importRoot, 'included.png'));
const library = await ctx.createLibrary({ importPaths: [importRoot] });
await ctx.scan(library.id);
await expect(ctx.getAssetPaths(library.id)).resolves.toEqual([...excludedAssets, includedAsset].sort());
await sut.update(library.id, { exclusionPatterns: [pattern] });
await ctx.scan(library.id);
await expect(ctx.getAssetPaths(library.id)).resolves.toEqual([includedAsset]);
const excluded = await ctx.database
.selectFrom('asset')
.select(['originalPath', 'isOffline', 'deletedAt'])
.where('libraryId', '=', library.id)
.where('originalPath', 'in', excludedAssets)
.orderBy('originalPath')
.execute();
expect(excluded).toEqual(
excludedAssets.sort().map((originalPath) => ({
originalPath,
isOffline: true,
deletedAt: expect.any(Date),
})),
);
await ctx.scan(library.id);
await expect(ctx.getAssetPaths(library.id)).resolves.toEqual([includedAsset]);
},
);
// https://github.com/immich-app/immich/issues/17121
it('should respect exclusion patterns when using multiple import paths', async () => {
const { sut, ctx } = setup();