diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index ec5d25a3b1..720824b672 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -144,6 +144,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']); + assetMock.getPathsNotInLibrary.mockResolvedValue(['/data/user1/photo.jpg']); assetMock.getByLibraryId.mockResolvedValue([]); userMock.get.mockResolvedValue(userStub.admin); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 33edf74bf3..2c509cdaaa 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -621,29 +621,18 @@ export class LibraryService extends EventEmitter { pathsToCrawl: validImportPaths, exclusionPatterns: library.exclusionPatterns, }); - const crawledAssetPaths = rawPaths.map((filePath) => path.normalize(filePath)); this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`); - const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]); - const onlineFiles = new Set(crawledAssetPaths); - const offlineAssetIds = assetsInLibrary - .filter((asset) => !onlineFiles.has(asset.originalPath)) - .filter((asset) => !asset.isOffline) - .map((asset) => asset.id); - this.logger.debug(`Marking ${offlineAssetIds.length} assets as offline`); - await this.assetRepository.updateAll(offlineAssetIds, { isOffline: true }); + await this.assetRepository.updateOfflineLibraryAssets(library.id, crawledAssetPaths); if (crawledAssetPaths.length > 0) { let filteredPaths: string[] = []; if (job.refreshAllFiles || job.refreshModifiedFiles) { filteredPaths = crawledAssetPaths; } else { - const onlinePathsInLibrary = new Set( - assetsInLibrary.filter((asset) => !asset.isOffline).map((asset) => asset.originalPath), - ); - filteredPaths = crawledAssetPaths.filter((assetPath) => !onlinePathsInLibrary.has(assetPath)); + filteredPaths = await this.assetRepository.getPathsNotInLibrary(library.id, crawledAssetPaths); this.logger.debug(`Will import ${filteredPaths.length} new asset(s)`); } diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index d0e22f676f..dd5e76577c 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -136,6 +136,8 @@ export interface IAssetRepository { getLastUpdatedAssetForAlbumId(albumId: string): Promise; getByLibraryId(libraryIds: string[]): Promise; getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise; + getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise; + updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise; deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; getAllByFileCreationDate( diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 4bcfc963fa..4813056659 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -199,6 +199,29 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] }) + @ChunkedArray({ paramIndex: 1 }) + async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise { + const result = await this.repository.query( + ` + WITH paths AS (SELECT unnest($2::text[]) AS path) + SELECT path FROM paths + WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path); + `, + [libraryId, originalPaths], + ); + return result.map((row: { path: string }) => row.path); + } + + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] }) + @ChunkedArray({ paramIndex: 1 }) + async updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise { + await this.repository.update( + { library: { id: libraryId }, originalPath: Not(In(originalPaths)), isOffline: false }, + { isOffline: true }, + ); + } + getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { let builder = this.repository.createQueryBuilder('asset'); builder = searchAssetBuilder(builder, options); diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/infra/sql/asset.repository.sql index e5cf6771fd..54992e5f87 100644 --- a/server/src/infra/sql/asset.repository.sql +++ b/server/src/infra/sql/asset.repository.sql @@ -395,6 +395,39 @@ ORDER BY LIMIT 1 +-- AssetRepository.getPathsNotInLibrary +WITH + paths AS ( + SELECT + unnest($2::text []) AS path + ) +SELECT + path +FROM + paths +WHERE + NOT EXISTS ( + SELECT + 1 + FROM + assets + WHERE + "libraryId" = $1 + AND "originalPath" = path + ); + +-- AssetRepository.updateOfflineLibraryAssets +UPDATE "assets" +SET + "isOffline" = $1, + "updatedAt" = CURRENT_TIMESTAMP +WHERE + ( + "libraryId" = $2 + AND NOT ("originalPath" IN ($3)) + AND "isOffline" = $4 + ) + -- AssetRepository.getAllByFileCreationDate SELECT "asset"."id" AS "asset_id", diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 0be384b3ae..63f1229a23 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -23,6 +23,8 @@ export const newAssetRepositoryMock = (): jest.Mocked => { updateAll: jest.fn(), getByLibraryId: jest.fn(), getByLibraryIdAndOriginalPath: jest.fn(), + updateOfflineLibraryAssets: jest.fn(), + getPathsNotInLibrary: jest.fn(), deleteAll: jest.fn(), save: jest.fn(), remove: jest.fn(),