From f5073a1a7a966f039718a8c8585f594ee242fa2e Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 20 Mar 2024 08:17:22 +0100 Subject: [PATCH] check import paths when testing offline --- server/src/domain/job/job.interface.ts | 4 +++ .../domain/library/library.service.spec.ts | 28 ++++++++++++++---- server/src/domain/library/library.service.ts | 29 +++++++++++++++---- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/server/src/domain/job/job.interface.ts b/server/src/domain/job/job.interface.ts index 8e7420bf9..1ea7c8c8f 100644 --- a/server/src/domain/job/job.interface.ts +++ b/server/src/domain/job/job.interface.ts @@ -21,6 +21,10 @@ export interface ILibraryRefreshJob extends IEntityJob { refreshAllFiles: boolean; } +export interface ILibraryOfflineJob extends IEntityJob { + importPaths: string[]; +} + export interface IBulkEntityJob extends IBaseJob { ids: string[]; } diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index ef1fa68a2..89de792e1 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -20,7 +20,7 @@ import { import { when } from 'jest-when'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; -import { IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; +import { IEntityJob, ILibraryFileJob, ILibraryOfflineJob, ILibraryRefreshJob, JobName } from '../job'; import { IAssetRepository, ICryptoRepository, @@ -277,8 +277,9 @@ describe(LibraryService.name, () => { describe('handleOfflineCheck', () => { it('should set missing assets offline', async () => { - const mockAssetJob: IEntityJob = { + const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, + importPaths: ['/'], }; assetMock.getById.mockResolvedValue(assetStub.external); @@ -290,9 +291,25 @@ describe(LibraryService.name, () => { expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); }); - it('should skip an offline asset', async () => { - const mockAssetJob: IEntityJob = { + it('should set an asset outside of import paths as offline', async () => { + const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, + importPaths: ['/data/user2'], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + + storageMock.checkFileExists.mockResolvedValue(true); + + await sut.handleOfflineCheck(mockAssetJob); + + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + }); + + it('should skip an offline asset', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], }; assetMock.getById.mockResolvedValue(assetStub.offline); @@ -306,8 +323,9 @@ describe(LibraryService.name, () => { }); it('should skip a nonexistent asset id', async () => { - const mockAssetJob: IEntityJob = { + const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, + importPaths: ['/'], }; assetMock.getById.mockImplementation(() => Promise.resolve(null)); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index e7ae2a709..7de49bb34 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -1,4 +1,4 @@ -import { AssetType, LibraryType } from '@app/infra/entities'; +import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; @@ -10,7 +10,15 @@ import picomatch from 'picomatch'; import { AccessCore } from '../access'; import { mimeTypes } from '../domain.constant'; import { handlePromiseError, usePagination, validateCronExpression } from '../domain.util'; -import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; +import { + IBaseJob, + IEntityJob, + ILibraryFileJob, + ILibraryOfflineJob, + ILibraryRefreshJob, + JOBS_ASSET_PAGINATION_SIZE, + JobName, +} from '../job'; import { DatabaseLock, IAccessRepository, @@ -575,7 +583,7 @@ export class LibraryService extends EventEmitter { } // Check if an asset is has no file or is outside of import paths, marking it as offline - async handleOfflineCheck(job: IEntityJob): Promise { + async handleOfflineCheck(job: ILibraryOfflineJob): Promise { const asset = await this.assetRepository.getById(job.id); if (!asset || asset.isOffline) { @@ -585,7 +593,16 @@ export class LibraryService extends EventEmitter { const exists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK); - if (exists) { + let existsInImportPath = false; + + for (const importPath of job.importPaths) { + if (asset.originalPath.startsWith(importPath)) { + existsInImportPath = true; + break; + } + } + + if (exists && existsInImportPath) { this.logger.verbose(`Asset is still online: ${asset.originalPath}`); } else { this.logger.debug(`Marking asset as offline: ${asset.originalPath}`); @@ -657,9 +674,9 @@ export class LibraryService extends EventEmitter { `Queuing online check of ${existingAssetPage.value.length} asset(s) in library ${library.id}...`, ); await this.jobRepository.queueAll( - existingAssetPage.value.map((asset) => ({ + existingAssetPage.value.map((asset: AssetEntity) => ({ name: JobName.LIBRARY_CHECK_OFFLINE, - data: { id: asset.id }, + data: { id: asset.id, importPaths: validImportPaths }, })), ); }