mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
fix(server): prevent feedback loop during library scan (#7944)
* prevent feedback loop * add nesting * made nesting less ugly --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
eea0a98090
commit
a9438a9c2d
@ -89,7 +89,7 @@ export class ValidateLibraryResponseDto {
|
||||
|
||||
export class ValidateLibraryImportPathResponseDto {
|
||||
importPath!: string;
|
||||
isValid?: boolean = false;
|
||||
isValid: boolean = false;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
16
server/test/fixtures/library.stub.ts
vendored
16
server/test/fixtures/library.stub.ts
vendored
@ -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<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/**'],
|
||||
}),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user