mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04: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 {
|
export class ValidateLibraryImportPathResponseDto {
|
||||||
importPath!: string;
|
importPath!: string;
|
||||||
isValid?: boolean = false;
|
isValid: boolean = false;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
userStub,
|
userStub,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import { when } from 'jest-when';
|
import { when } from 'jest-when';
|
||||||
|
import { R_OK } from 'node:constants';
|
||||||
import { Stats } from 'node:fs';
|
import { Stats } from 'node:fs';
|
||||||
import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job';
|
import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job';
|
||||||
import {
|
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,
|
StorageEventType,
|
||||||
WithProperty,
|
WithProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
|
import { StorageCore } from '../storage';
|
||||||
import { SystemConfigCore } from '../system-config';
|
import { SystemConfigCore } from '../system-config';
|
||||||
import {
|
import {
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
@ -327,9 +328,13 @@ export class LibraryService extends EventEmitter {
|
|||||||
const validation = new ValidateLibraryImportPathResponseDto();
|
const validation = new ValidateLibraryImportPathResponseDto();
|
||||||
validation.importPath = importPath;
|
validation.importPath = importPath;
|
||||||
|
|
||||||
|
if (StorageCore.isImmichPath(importPath)) {
|
||||||
|
validation.message = 'Cannot use media upload folder for external libraries';
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stat = await this.storageRepository.stat(importPath);
|
const stat = await this.storageRepository.stat(importPath);
|
||||||
|
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
validation.message = 'Not a directory';
|
validation.message = 'Not a directory';
|
||||||
return validation;
|
return validation;
|
||||||
@ -678,13 +683,13 @@ export class LibraryService extends EventEmitter {
|
|||||||
this.logger.debug(`Will import ${crawledAssetPaths.size} new asset(s)`);
|
this.logger.debug(`Will import ${crawledAssetPaths.size} new asset(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const batch = [];
|
let batch = [];
|
||||||
for (const assetPath of crawledAssetPaths) {
|
for (const assetPath of crawledAssetPaths) {
|
||||||
batch.push(assetPath);
|
batch.push(assetPath);
|
||||||
|
|
||||||
if (batch.length >= LIBRARY_SCAN_BATCH_SIZE) {
|
if (batch.length >= LIBRARY_SCAN_BATCH_SIZE) {
|
||||||
await this.scanAssets(job.id, batch, library.ownerId, job.refreshAllFiles ?? false);
|
await this.scanAssets(job.id, batch, library.ownerId, job.refreshAllFiles ?? false);
|
||||||
batch.length = 0;
|
batch = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@ export enum StorageFolder {
|
|||||||
THUMBNAILS = 'thumbs',
|
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 {
|
export interface MoveRequest {
|
||||||
entityId: string;
|
entityId: string;
|
||||||
pathType: PathType;
|
pathType: PathType;
|
||||||
@ -115,6 +118,10 @@ export class StorageCore {
|
|||||||
return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION));
|
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) {
|
async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) {
|
||||||
const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset;
|
const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset;
|
||||||
switch (pathType) {
|
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 { LibraryEntity, LibraryType } from '@app/infra/entities';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { userStub } from './user.stub';
|
import { userStub } from './user.stub';
|
||||||
|
|
||||||
export const libraryStub = {
|
export const libraryStub = {
|
||||||
@ -100,4 +102,18 @@ export const libraryStub = {
|
|||||||
isVisible: true,
|
isVisible: true,
|
||||||
exclusionPatterns: ['**/dir1/**'],
|
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…
x
Reference in New Issue
Block a user