Skip to content

Commit

Permalink
fix(server): prevent feedback loop during library scan (#7944)
Browse files Browse the repository at this point in the history
* prevent feedback loop

* add nesting

* made nesting less ugly

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
  • Loading branch information
mertalev and alextran1502 committed Mar 15, 2024
1 parent eea0a98 commit a9438a9
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 4 deletions.
2 changes: 1 addition & 1 deletion server/src/domain/library/library.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class ValidateLibraryResponseDto {

export class ValidateLibraryImportPathResponseDto {
importPath!: string;
isValid?: boolean = false;
isValid: boolean = false;
message?: string;
}

Expand Down
28 changes: 28 additions & 0 deletions server/src/domain/library/library.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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',
},
]);
});
});
});
11 changes: 8 additions & 3 deletions server/src/domain/library/library.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
StorageEventType,
WithProperty,
} from '../repositories';
import { StorageCore } from '../storage';
import { SystemConfigCore } from '../system-config';
import {
CreateLibraryDto,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = [];
}
}

Expand Down
7 changes: 7 additions & 0 deletions server/src/domain/storage/storage.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions server/test/fixtures/library.stub.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -100,4 +102,18 @@ export const libraryStub = {
isVisible: true,
exclusionPatterns: ['**/dir1/**'],
}),
hasImmichPaths: Object.freeze<LibraryEntity>({
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/**'],
}),
};

0 comments on commit a9438a9

Please sign in to comment.