mirror of
https://github.com/immich-app/static-pages.git
synced 2026-09-30 13:23:05 +08:00
fix(builder): reject external library paths that collide with /data (#777)
This commit is contained in:
@@ -152,6 +152,42 @@ describe('validate', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses an external library that lands inside the container upload directory', () => {
|
||||
const config = structuredClone(DEFAULT_CONFIG);
|
||||
|
||||
for (const path of ['/data', '/data/library', '/data/upload', '/data/thumbs', '/data/anything']) {
|
||||
config.storage.externalLibraries = [{ path, readOnly: true }];
|
||||
expect(validate(config)['storage.externalLibraries.0.path'], path).toContain('/data');
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses the filesystem root as an external library', () => {
|
||||
const config = structuredClone(DEFAULT_CONFIG);
|
||||
|
||||
for (const path of ['/', '//']) {
|
||||
config.storage.externalLibraries = [{ path, readOnly: true }];
|
||||
expect(validate(config)['storage.externalLibraries.0.path'], path).toContain('root');
|
||||
}
|
||||
});
|
||||
|
||||
it('flags an external library that reuses an override host path', () => {
|
||||
const config = structuredClone(DEFAULT_CONFIG);
|
||||
config.storage.customFolders = true;
|
||||
config.storage.overrides.thumbs = '/mnt/thumbs';
|
||||
config.storage.externalLibraries = [{ path: '/mnt/thumbs', readOnly: true }];
|
||||
const errors = validate(config);
|
||||
expect(errors['storage.overrides.thumbs']).toBeTruthy();
|
||||
expect(errors['storage.externalLibraries.0.path']).toBeTruthy();
|
||||
});
|
||||
|
||||
it('still accepts an ordinary external library alongside overrides', () => {
|
||||
const config = structuredClone(DEFAULT_CONFIG);
|
||||
config.storage.customFolders = true;
|
||||
config.storage.overrides.thumbs = '/mnt/fast/thumbs';
|
||||
config.storage.externalLibraries = [{ path: '/mnt/media/photos', readOnly: true }];
|
||||
expect(validate(config)).toEqual({});
|
||||
});
|
||||
|
||||
it('flags an external library that overlaps another mount', () => {
|
||||
const config = structuredClone(DEFAULT_CONFIG);
|
||||
config.storage.uploadLocation = '/mnt/media';
|
||||
|
||||
@@ -264,6 +264,8 @@ type ValidationErrors = Record<string, string>;
|
||||
|
||||
const normalizePath = (path: string) => path.trim().replace(/\/+$/, '');
|
||||
|
||||
const UPLOAD_TARGET = '/data';
|
||||
|
||||
type Mount = { path: string[]; location: string };
|
||||
|
||||
const collectMounts = (config: ImmichConfig): Mount[] => {
|
||||
@@ -301,11 +303,18 @@ const validationSchema = immichConfigSchema.superRefine((config, ctx) => {
|
||||
}
|
||||
|
||||
for (const [index, { path }] of config.storage.externalLibraries.entries()) {
|
||||
const location = path.trim();
|
||||
if (!location) {
|
||||
fail(['storage', 'externalLibraries', String(index), 'path'], 'A path is required.');
|
||||
const field = ['storage', 'externalLibraries', String(index), 'path'];
|
||||
const trimmed = path.trim();
|
||||
const location = normalizePath(path);
|
||||
|
||||
if (!trimmed) {
|
||||
fail(field, 'A path is required.');
|
||||
} else if (!location) {
|
||||
fail(field, 'The filesystem root cannot be mounted as a library.');
|
||||
} else if (!location.startsWith('/')) {
|
||||
fail(['storage', 'externalLibraries', String(index), 'path'], 'Enter an absolute path, e.g. /mnt/media/photos.');
|
||||
fail(field, 'Enter an absolute path, e.g. /mnt/media/photos.');
|
||||
} else if (location === UPLOAD_TARGET || location.startsWith(`${UPLOAD_TARGET}/`)) {
|
||||
fail(field, `Immich manages ${UPLOAD_TARGET} inside the container, so a library cannot live there.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user