mirror of
https://github.com/immich-app/immich.git
synced 2025-01-02 12:48:35 +02:00
feat(server): YAML config file support (#7894)
* test(server): Load config from yaml * docs: YAML config support * feat(server): YAML config file support * fix format --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
1683bb75e1
commit
72f9295490
@ -4,7 +4,7 @@ A config file can be provided as an alternative to the UI configuration.
|
|||||||
|
|
||||||
### Step 1 - Create a new config file
|
### Step 1 - Create a new config file
|
||||||
|
|
||||||
In JSON format, create a new config file (e.g. `immich.config`) and put it in a location that can be accessed by Immich.
|
In JSON format, create a new config file (e.g. `immich.json`) and put it in a location that can be accessed by Immich.
|
||||||
The default configuration looks like this:
|
The default configuration looks like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -163,3 +163,7 @@ So you can just grab it from there, paste it into a file and you're pretty much
|
|||||||
|
|
||||||
In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
|
In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
|
||||||
For more information, refer to the [Environment Variables](/docs/install/environment-variables.md) section.
|
For more information, refer to the [Environment Variables](/docs/install/environment-variables.md) section.
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
YAML-formatted config files are also supported.
|
||||||
|
:::
|
||||||
|
14
server/package-lock.json
generated
14
server/package-lock.json
generated
@ -44,6 +44,7 @@
|
|||||||
"i18n-iso-countries": "^7.6.0",
|
"i18n-iso-countries": "^7.6.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"joi": "^17.10.0",
|
"joi": "^17.10.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.4.2",
|
"luxon": "^3.4.2",
|
||||||
"nest-commander": "^3.11.1",
|
"nest-commander": "^3.11.1",
|
||||||
@ -75,6 +76,7 @@
|
|||||||
"@types/imagemin": "^8.0.1",
|
"@types/imagemin": "^8.0.1",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/jest-when": "^3.5.2",
|
"@types/jest-when": "^3.5.2",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
@ -4571,6 +4573,12 @@
|
|||||||
"@types/jest": "*"
|
"@types/jest": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.13",
|
"version": "7.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
||||||
@ -17578,6 +17586,12 @@
|
|||||||
"@types/jest": "*"
|
"@types/jest": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.13",
|
"version": "7.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
"i18n-iso-countries": "^7.6.0",
|
"i18n-iso-countries": "^7.6.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"joi": "^17.10.0",
|
"joi": "^17.10.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^3.4.2",
|
"luxon": "^3.4.2",
|
||||||
"nest-commander": "^3.11.1",
|
"nest-commander": "^3.11.1",
|
||||||
@ -99,6 +100,7 @@
|
|||||||
"@types/imagemin": "^8.0.1",
|
"@types/imagemin": "^8.0.1",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/jest-when": "^3.5.2",
|
"@types/jest-when": "^3.5.2",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
|
@ -17,6 +17,7 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com
|
|||||||
import { CronExpression } from '@nestjs/schedule';
|
import { CronExpression } from '@nestjs/schedule';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { validate } from 'class-validator';
|
import { validate } from 'class-validator';
|
||||||
|
import { load as loadYaml } from 'js-yaml';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { QueueName } from '../job/job.constants';
|
import { QueueName } from '../job/job.constants';
|
||||||
@ -341,19 +342,19 @@ export class SystemConfigCore {
|
|||||||
if (force || !this.configCache) {
|
if (force || !this.configCache) {
|
||||||
try {
|
try {
|
||||||
const file = await this.repository.readFile(filepath);
|
const file = await this.repository.readFile(filepath);
|
||||||
const json = JSON.parse(file.toString());
|
const config = loadYaml(file.toString()) as any;
|
||||||
const overrides: SystemConfigEntity<SystemConfigValue>[] = [];
|
const overrides: SystemConfigEntity<SystemConfigValue>[] = [];
|
||||||
|
|
||||||
for (const key of Object.values(SystemConfigKey)) {
|
for (const key of Object.values(SystemConfigKey)) {
|
||||||
const value = _.get(json, key);
|
const value = _.get(config, key);
|
||||||
this.unsetDeep(json, key);
|
this.unsetDeep(config, key);
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
overrides.push({ key, value });
|
overrides.push({ key, value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(json)) {
|
if (!_.isEmpty(config)) {
|
||||||
this.logger.warn(`Unknown keys found: ${JSON.stringify(json, null, 2)}`);
|
this.logger.warn(`Unknown keys found: ${JSON.stringify(config, null, 2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.configCache = overrides;
|
this.configCache = overrides;
|
||||||
|
@ -209,7 +209,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load the config from a file', async () => {
|
it('should load the config from a json file', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||||
const partialConfig = {
|
const partialConfig = {
|
||||||
ffmpeg: { crf: 30 },
|
ffmpeg: { crf: 30 },
|
||||||
@ -224,6 +224,25 @@ describe(SystemConfigService.name, () => {
|
|||||||
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should load the config from a yaml file', async () => {
|
||||||
|
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
|
||||||
|
const partialConfig = `
|
||||||
|
ffmpeg:
|
||||||
|
crf: 30
|
||||||
|
oauth:
|
||||||
|
autoLaunch: true
|
||||||
|
trash:
|
||||||
|
days: 10
|
||||||
|
user:
|
||||||
|
deleteDelay: 15
|
||||||
|
`;
|
||||||
|
configMock.readFile.mockResolvedValue(partialConfig);
|
||||||
|
|
||||||
|
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
|
||||||
|
|
||||||
|
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
|
||||||
|
});
|
||||||
|
|
||||||
it('should accept an empty configuration file', async () => {
|
it('should accept an empty configuration file', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
||||||
configMock.readFile.mockResolvedValue(JSON.stringify({}));
|
configMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||||
@ -242,6 +261,17 @@ describe(SystemConfigService.name, () => {
|
|||||||
expect(config.machineLearning.url).toEqual('immich_machine_learning');
|
expect(config.machineLearning.url).toEqual('immich_machine_learning');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should warn for unknown options in yaml', async () => {
|
||||||
|
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
|
||||||
|
const partialConfig = `
|
||||||
|
unknownOption: true
|
||||||
|
`;
|
||||||
|
configMock.readFile.mockResolvedValue(partialConfig);
|
||||||
|
|
||||||
|
await sut.getConfig();
|
||||||
|
expect(warnLog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
const tests = [
|
const tests = [
|
||||||
{ should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } },
|
{ should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } },
|
||||||
{ should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } },
|
{ should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } },
|
||||||
|
Loading…
Reference in New Issue
Block a user