diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 20bd230159c28..9f5adc4e27d92 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -347,6 +347,62 @@ describe('/libraries', () => { expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).toBeDefined(); }); + it('should scan multiple import paths with commas', async () => { + // https://github.com/immich-app/immich/issues/10699 + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/folder, a`, `${testAssetDirInternal}/temp/folder, b`], + }); + + utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`); + utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`); + + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(2); + expect(assets.items.find((asset) => asset.originalPath.includes('folder, a'))).toBeDefined(); + expect(assets.items.find((asset) => asset.originalPath.includes('folder, b'))).toBeDefined(); + + utils.removeImageFile(`${testAssetDir}/temp/folder, a/assetA.png`); + utils.removeImageFile(`${testAssetDir}/temp/folder, b/assetB.png`); + }); + + it('should scan multiple import paths with braces', async () => { + // https://github.com/immich-app/immich/issues/10699 + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/folder{ a`, `${testAssetDirInternal}/temp/folder} b`], + }); + + utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`); + utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`); + + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(2); + expect(assets.items.find((asset) => asset.originalPath.includes('folder{ a'))).toBeDefined(); + expect(assets.items.find((asset) => asset.originalPath.includes('folder} b'))).toBeDefined(); + + utils.removeImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`); + utils.removeImageFile(`${testAssetDir}/temp/folder} b/assetB.png`); + }); + it('should reimport a modified file', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 6fd9bb8b04147..b95744998403f 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -156,7 +156,9 @@ export class StorageRepository implements IStorageRepository { return Promise.resolve([]); } - return glob(this.asGlob(pathsToCrawl), { + const globbedPaths = pathsToCrawl.map((path) => this.asGlob(path)); + + return glob(globbedPaths, { absolute: true, caseSensitiveMatch: false, onlyFiles: true, @@ -172,7 +174,9 @@ export class StorageRepository implements IStorageRepository { return emptyGenerator(); } - const stream = globStream(this.asGlob(pathsToCrawl), { + const globbedPaths = pathsToCrawl.map((path) => this.asGlob(path)); + + const stream = globStream(globbedPaths, { absolute: true, caseSensitiveMatch: false, onlyFiles: true, @@ -206,10 +210,9 @@ export class StorageRepository implements IStorageRepository { return () => watcher.close(); } - private asGlob(pathsToCrawl: string[]): string { - const escapedPaths = pathsToCrawl.map((path) => escapePath(path)); - const base = escapedPaths.length === 1 ? escapedPaths[0] : `{${escapedPaths.join(',')}}`; + private asGlob(pathToCrawl: string): string { + const escapedPath = escapePath(pathToCrawl); const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`; - return `${base}/**/${extensions}`; + return `${escapedPath}/**/${extensions}`; } }