diff --git a/server/src/domain/library/library.dto.ts b/server/src/domain/library/library.dto.ts index b11bc9998..5e4bb4ec6 100644 --- a/server/src/domain/library/library.dto.ts +++ b/server/src/domain/library/library.dto.ts @@ -89,7 +89,7 @@ export class ValidateLibraryResponseDto { export class ValidateLibraryImportPathResponseDto { importPath!: string; - isValid?: boolean = false; + isValid: boolean = false; message?: string; } diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 98c334509..ffcb80df0 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -18,6 +18,7 @@ import { userStub, } from '@test'; import { when } from 'jest-when'; +import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; import { @@ -1632,5 +1633,32 @@ describe(LibraryService.name, () => { }, ]); }); + + it('should detect when import path is in immich media folder', async () => { + storageMock.stat.mockResolvedValue({ isDirectory: () => true } as Stats); + const validImport = libraryStub.hasImmichPaths.importPaths[1]; + when(storageMock.checkFileExists).calledWith(validImport, R_OK).mockResolvedValue(true); + + const result = await sut.validate(authStub.external1, libraryStub.hasImmichPaths.id, { + importPaths: libraryStub.hasImmichPaths.importPaths, + }); + + expect(result.importPaths).toEqual([ + { + importPath: libraryStub.hasImmichPaths.importPaths[0], + isValid: false, + message: 'Cannot use media upload folder for external libraries', + }, + { + importPath: validImport, + isValid: true, + }, + { + importPath: libraryStub.hasImmichPaths.importPaths[2], + isValid: false, + message: 'Cannot use media upload folder for external libraries', + }, + ]); + }); }); }); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index dec54c155..d24c9cdc1 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -26,6 +26,7 @@ import { StorageEventType, WithProperty, } from '../repositories'; +import { StorageCore } from '../storage'; import { SystemConfigCore } from '../system-config'; import { CreateLibraryDto, @@ -327,9 +328,13 @@ export class LibraryService extends EventEmitter { const validation = new ValidateLibraryImportPathResponseDto(); validation.importPath = importPath; + if (StorageCore.isImmichPath(importPath)) { + validation.message = 'Cannot use media upload folder for external libraries'; + return validation; + } + try { const stat = await this.storageRepository.stat(importPath); - if (!stat.isDirectory()) { validation.message = 'Not a directory'; return validation; @@ -678,13 +683,13 @@ export class LibraryService extends EventEmitter { this.logger.debug(`Will import ${crawledAssetPaths.size} new asset(s)`); } - const batch = []; + let batch = []; for (const assetPath of crawledAssetPaths) { batch.push(assetPath); if (batch.length >= LIBRARY_SCAN_BATCH_SIZE) { await this.scanAssets(job.id, batch, library.ownerId, job.refreshAllFiles ?? false); - batch.length = 0; + batch = []; } } diff --git a/server/src/domain/storage/storage.core.ts b/server/src/domain/storage/storage.core.ts index 30a6002be..36e600b24 100644 --- a/server/src/domain/storage/storage.core.ts +++ b/server/src/domain/storage/storage.core.ts @@ -20,6 +20,9 @@ export enum StorageFolder { THUMBNAILS = 'thumbs', } +export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS)); +export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO)); + export interface MoveRequest { entityId: string; pathType: PathType; @@ -115,6 +118,10 @@ export class StorageCore { return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION)); } + static isGeneratedAsset(path: string) { + return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR); + } + async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) { const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset; switch (pathType) { diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts index ec8e19683..db7687f28 100644 --- a/server/test/fixtures/library.stub.ts +++ b/server/test/fixtures/library.stub.ts @@ -1,4 +1,6 @@ +import { APP_MEDIA_LOCATION, THUMBNAIL_DIR } from '@app/domain'; import { LibraryEntity, LibraryType } from '@app/infra/entities'; +import { join } from 'node:path'; import { userStub } from './user.stub'; export const libraryStub = { @@ -100,4 +102,18 @@ export const libraryStub = { isVisible: true, exclusionPatterns: ['**/dir1/**'], }), + hasImmichPaths: Object.freeze({ + id: 'library-id1337', + name: 'importpath-exclusion-library1', + assets: [], + owner: userStub.admin, + ownerId: 'user-id', + type: LibraryType.EXTERNAL, + importPaths: [join(THUMBNAIL_DIR, 'library'), '/xyz', join(APP_MEDIA_LOCATION, 'library')], + createdAt: new Date('2023-01-01'), + updatedAt: new Date('2023-01-01'), + refreshedAt: null, + isVisible: true, + exclusionPatterns: ['**/dir1/**'], + }), };