123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- # 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
|