forked from Ponysearch/Ponysearch
2039060b64
The intention of this PR is to modernize the settings_loader implementations. The concept is old (remember, this is partly from 2014), back then we only had one config file, meanwhile we have had a folder with config files for a very long time. Callers can now load a YAML configuration from this folder as follows :: settings_loader.get_yaml_cfg('my-config.yml') - BTW this is a fix of #3557. - Further the `existing_filename_or_none` construct dates back to times when there was not yet a `pathlib.Path` in all Python versions we supported in the past. - Typehints have been added wherever appropriate At the same time, this patch should also be downward compatible and not introduce a new environment variable. The localization of the folder with the configurations is further based on: SEARXNG_SETTINGS_PATH (wich defaults to /etc/searxng/settings.yml) Which means, the default config folder is `/etc/searxng/`. ATTENTION: intended functional changes! If SEARXNG_SETTINGS_PATH was set and pointed to a not existing file, the previous implementation silently loaded the default configuration. This behavior has been changed: if the file or folder does not exist, an EnvironmentError exception will be thrown in future. Closes: https://github.com/searxng/searxng/issues/3557 Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
221 lines
8.1 KiB
Python
221 lines
8.1 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Implementations for loading configurations from YAML files. This essentially
|
|
includes the configuration of the (:ref:`SearXNG appl <searxng settings.yml>`)
|
|
server. The default configuration for the application server is loaded from the
|
|
:origin:`DEFAULT_SETTINGS_FILE <searx/settings.yml>`. This default
|
|
configuration can be completely replaced or :ref:`customized individually
|
|
<use_default_settings.yml>` and the ``SEARXNG_SETTINGS_PATH`` environment
|
|
variable can be used to set the location from which the local customizations are
|
|
to be loaded. The rules used for this can be found in the
|
|
:py:obj:`get_user_cfg_folder` function.
|
|
|
|
- By default, local configurations are expected in folder ``/etc/searxng`` from
|
|
where applications can load them with the :py:obj:`get_yaml_cfg` function.
|
|
|
|
- By default, customized :ref:`SearXNG appl <searxng settings.yml>` settings are
|
|
expected in a file named ``settings.yml``.
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os.path
|
|
from collections.abc import Mapping
|
|
from itertools import filterfalse
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from searx.exceptions import SearxSettingsException
|
|
|
|
searx_dir = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
SETTINGS_YAML = Path("settings.yml")
|
|
DEFAULT_SETTINGS_FILE = Path(searx_dir) / SETTINGS_YAML
|
|
"""The :origin:`searx/settings.yml` file with all the default settings."""
|
|
|
|
|
|
def load_yaml(file_name: str | Path):
|
|
"""Load YAML config from a file."""
|
|
try:
|
|
with open(file_name, 'r', encoding='utf-8') as settings_yaml:
|
|
return yaml.safe_load(settings_yaml) or {}
|
|
except IOError as e:
|
|
raise SearxSettingsException(e, str(file_name)) from e
|
|
except yaml.YAMLError as e:
|
|
raise SearxSettingsException(e, str(file_name)) from e
|
|
|
|
|
|
def get_yaml_cfg(file_name: str | Path) -> dict:
|
|
"""Shortcut to load a YAML config from a file, located in the
|
|
|
|
- :py:obj:`get_user_cfg_folder` or
|
|
- in the ``searx`` folder of the SearXNG installation
|
|
"""
|
|
|
|
folder = get_user_cfg_folder() or Path(searx_dir)
|
|
fname = folder / file_name
|
|
if not fname.is_file():
|
|
raise FileNotFoundError(f"File {fname} does not exist!")
|
|
|
|
return load_yaml(fname)
|
|
|
|
|
|
def get_user_cfg_folder() -> Path | None:
|
|
"""Returns folder where the local configurations are located.
|
|
|
|
1. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a
|
|
folder (e.g. ``/etc/mysxng/``), all local configurations are expected in
|
|
this folder. The settings of the :ref:`SearXNG appl <searxng
|
|
settings.yml>` then expected in ``settings.yml``
|
|
(e.g. ``/etc/mysxng/settings.yml``).
|
|
|
|
2. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a file
|
|
(e.g. ``/etc/mysxng/myinstance.yml``), this file contains the settings of
|
|
the :ref:`SearXNG appl <searxng settings.yml>` and the folder
|
|
(e.g. ``/etc/mysxng/``) is used for all other configurations.
|
|
|
|
This type (``SEARXNG_SETTINGS_PATH`` points to a file) is suitable for
|
|
use cases in which different profiles of the :ref:`SearXNG appl <searxng
|
|
settings.yml>` are to be managed, such as in test scenarios.
|
|
|
|
3. If folder ``/etc/searxng`` exists, it is used.
|
|
|
|
In case none of the above path exists, ``None`` is returned. In case of
|
|
environment ``SEARXNG_SETTINGS_PATH`` is set, but the (folder or file) does
|
|
not exists, a :py:obj:`EnvironmentError` is raised.
|
|
|
|
"""
|
|
|
|
folder = None
|
|
settings_path = os.environ.get("SEARXNG_SETTINGS_PATH")
|
|
|
|
# Disable default /etc/searxng is intended exclusively for internal testing purposes
|
|
# and is therefore not documented!
|
|
disable_etc = os.environ.get('SEARXNG_DISABLE_ETC_SETTINGS', '').lower() in ('1', 'true')
|
|
|
|
if settings_path:
|
|
# rule 1. and 2.
|
|
settings_path = Path(settings_path)
|
|
if settings_path.is_dir():
|
|
folder = settings_path
|
|
elif settings_path.is_file():
|
|
folder = settings_path.parent
|
|
else:
|
|
raise EnvironmentError(1, f"{settings_path} not exists!", settings_path)
|
|
|
|
if not folder and not disable_etc:
|
|
# default: rule 3.
|
|
folder = Path("/etc/searxng")
|
|
if not folder.is_dir():
|
|
folder = None
|
|
|
|
return folder
|
|
|
|
|
|
def update_dict(default_dict, user_dict):
|
|
for k, v in user_dict.items():
|
|
if isinstance(v, Mapping):
|
|
default_dict[k] = update_dict(default_dict.get(k, {}), v)
|
|
else:
|
|
default_dict[k] = v
|
|
return default_dict
|
|
|
|
|
|
def update_settings(default_settings: dict, user_settings: dict):
|
|
# pylint: disable=too-many-branches
|
|
|
|
# merge everything except the engines
|
|
for k, v in user_settings.items():
|
|
if k not in ('use_default_settings', 'engines'):
|
|
if k in default_settings and isinstance(v, Mapping):
|
|
update_dict(default_settings[k], v)
|
|
else:
|
|
default_settings[k] = v
|
|
|
|
categories_as_tabs = user_settings.get('categories_as_tabs')
|
|
if categories_as_tabs:
|
|
default_settings['categories_as_tabs'] = categories_as_tabs
|
|
|
|
# parse the engines
|
|
remove_engines = None
|
|
keep_only_engines = None
|
|
use_default_settings = user_settings.get('use_default_settings')
|
|
if isinstance(use_default_settings, dict):
|
|
remove_engines = use_default_settings.get('engines', {}).get('remove')
|
|
keep_only_engines = use_default_settings.get('engines', {}).get('keep_only')
|
|
|
|
if 'engines' in user_settings or remove_engines is not None or keep_only_engines is not None:
|
|
engines = default_settings['engines']
|
|
|
|
# parse "use_default_settings.engines.remove"
|
|
if remove_engines is not None:
|
|
engines = list(filterfalse(lambda engine: (engine.get('name')) in remove_engines, engines))
|
|
|
|
# parse "use_default_settings.engines.keep_only"
|
|
if keep_only_engines is not None:
|
|
engines = list(filter(lambda engine: (engine.get('name')) in keep_only_engines, engines))
|
|
|
|
# parse "engines"
|
|
user_engines = user_settings.get('engines')
|
|
if user_engines:
|
|
engines_dict = dict((definition['name'], definition) for definition in engines)
|
|
for user_engine in user_engines:
|
|
default_engine = engines_dict.get(user_engine['name'])
|
|
if default_engine:
|
|
update_dict(default_engine, user_engine)
|
|
else:
|
|
engines.append(user_engine)
|
|
|
|
# store the result
|
|
default_settings['engines'] = engines
|
|
|
|
return default_settings
|
|
|
|
|
|
def is_use_default_settings(user_settings):
|
|
|
|
use_default_settings = user_settings.get('use_default_settings')
|
|
if use_default_settings is True:
|
|
return True
|
|
if isinstance(use_default_settings, dict):
|
|
return True
|
|
if use_default_settings is False or use_default_settings is None:
|
|
return False
|
|
raise ValueError('Invalid value for use_default_settings')
|
|
|
|
|
|
def load_settings(load_user_settings=True) -> tuple[dict, str]:
|
|
"""Function for loading the settings of the SearXNG application
|
|
(:ref:`settings.yml <searxng settings.yml>`)."""
|
|
|
|
msg = f"load the default settings from {DEFAULT_SETTINGS_FILE}"
|
|
cfg = load_yaml(DEFAULT_SETTINGS_FILE)
|
|
cfg_folder = get_user_cfg_folder()
|
|
|
|
if not load_user_settings or not cfg_folder:
|
|
return cfg, msg
|
|
|
|
settings_yml = os.environ.get("SEARXNG_SETTINGS_PATH")
|
|
if settings_yml and Path(settings_yml).is_file():
|
|
# see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a file
|
|
settings_yml = Path(settings_yml).name
|
|
else:
|
|
# see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a folder
|
|
settings_yml = SETTINGS_YAML
|
|
|
|
cfg_file = cfg_folder / settings_yml
|
|
if not cfg_file.exists():
|
|
return cfg, msg
|
|
|
|
msg = f"load the user settings from {cfg_file}"
|
|
user_cfg = load_yaml(cfg_file)
|
|
|
|
if is_use_default_settings(user_cfg):
|
|
# the user settings are merged with the default configuration
|
|
msg = f"merge the default settings ( {DEFAULT_SETTINGS_FILE} ) and the user settings ( {cfg_file} )"
|
|
update_settings(cfg, user_cfg)
|
|
else:
|
|
cfg = user_cfg
|
|
|
|
return cfg, msg
|