settings_loader.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. """Implementations for loading configurations from YAML files. This essentially
  3. includes the configuration of the (:ref:`SearXNG appl <searxng settings.yml>`)
  4. server. The default configuration for the application server is loaded from the
  5. :origin:`DEFAULT_SETTINGS_FILE <searx/settings.yml>`. This default
  6. configuration can be completely replaced or :ref:`customized individually
  7. <use_default_settings.yml>` and the ``SEARXNG_SETTINGS_PATH`` environment
  8. variable can be used to set the location from which the local customizations are
  9. to be loaded. The rules used for this can be found in the
  10. :py:obj:`get_user_cfg_folder` function.
  11. - By default, local configurations are expected in folder ``/etc/searxng`` from
  12. where applications can load them with the :py:obj:`get_yaml_cfg` function.
  13. - By default, customized :ref:`SearXNG appl <searxng settings.yml>` settings are
  14. expected in a file named ``settings.yml``.
  15. """
  16. from __future__ import annotations
  17. import os.path
  18. from collections.abc import Mapping
  19. from itertools import filterfalse
  20. from pathlib import Path
  21. import yaml
  22. from searx.exceptions import SearxSettingsException
  23. searx_dir = os.path.abspath(os.path.dirname(__file__))
  24. SETTINGS_YAML = Path("settings.yml")
  25. DEFAULT_SETTINGS_FILE = Path(searx_dir) / SETTINGS_YAML
  26. """The :origin:`searx/settings.yml` file with all the default settings."""
  27. def load_yaml(file_name: str | Path):
  28. """Load YAML config from a file."""
  29. try:
  30. with open(file_name, 'r', encoding='utf-8') as settings_yaml:
  31. return yaml.safe_load(settings_yaml) or {}
  32. except IOError as e:
  33. raise SearxSettingsException(e, str(file_name)) from e
  34. except yaml.YAMLError as e:
  35. raise SearxSettingsException(e, str(file_name)) from e
  36. def get_yaml_cfg(file_name: str | Path) -> dict:
  37. """Shortcut to load a YAML config from a file, located in the
  38. - :py:obj:`get_user_cfg_folder` or
  39. - in the ``searx`` folder of the SearXNG installation
  40. """
  41. folder = get_user_cfg_folder() or Path(searx_dir)
  42. fname = folder / file_name
  43. if not fname.is_file():
  44. raise FileNotFoundError(f"File {fname} does not exist!")
  45. return load_yaml(fname)
  46. def get_user_cfg_folder() -> Path | None:
  47. """Returns folder where the local configurations are located.
  48. 1. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a
  49. folder (e.g. ``/etc/mysxng/``), all local configurations are expected in
  50. this folder. The settings of the :ref:`SearXNG appl <searxng
  51. settings.yml>` then expected in ``settings.yml``
  52. (e.g. ``/etc/mysxng/settings.yml``).
  53. 2. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a file
  54. (e.g. ``/etc/mysxng/myinstance.yml``), this file contains the settings of
  55. the :ref:`SearXNG appl <searxng settings.yml>` and the folder
  56. (e.g. ``/etc/mysxng/``) is used for all other configurations.
  57. This type (``SEARXNG_SETTINGS_PATH`` points to a file) is suitable for
  58. use cases in which different profiles of the :ref:`SearXNG appl <searxng
  59. settings.yml>` are to be managed, such as in test scenarios.
  60. 3. If folder ``/etc/searxng`` exists, it is used.
  61. In case none of the above path exists, ``None`` is returned. In case of
  62. environment ``SEARXNG_SETTINGS_PATH`` is set, but the (folder or file) does
  63. not exists, a :py:obj:`EnvironmentError` is raised.
  64. """
  65. folder = None
  66. settings_path = os.environ.get("SEARXNG_SETTINGS_PATH")
  67. # Disable default /etc/searxng is intended exclusively for internal testing purposes
  68. # and is therefore not documented!
  69. disable_etc = os.environ.get('SEARXNG_DISABLE_ETC_SETTINGS', '').lower() in ('1', 'true')
  70. if settings_path:
  71. # rule 1. and 2.
  72. settings_path = Path(settings_path)
  73. if settings_path.is_dir():
  74. folder = settings_path
  75. elif settings_path.is_file():
  76. folder = settings_path.parent
  77. else:
  78. raise EnvironmentError(1, f"{settings_path} not exists!", settings_path)
  79. if not folder and not disable_etc:
  80. # default: rule 3.
  81. folder = Path("/etc/searxng")
  82. if not folder.is_dir():
  83. folder = None
  84. return folder
  85. def update_dict(default_dict, user_dict):
  86. for k, v in user_dict.items():
  87. if isinstance(v, Mapping):
  88. default_dict[k] = update_dict(default_dict.get(k, {}), v)
  89. else:
  90. default_dict[k] = v
  91. return default_dict
  92. def update_settings(default_settings: dict, user_settings: dict):
  93. # pylint: disable=too-many-branches
  94. # merge everything except the engines
  95. for k, v in user_settings.items():
  96. if k not in ('use_default_settings', 'engines'):
  97. if k in default_settings and isinstance(v, Mapping):
  98. update_dict(default_settings[k], v)
  99. else:
  100. default_settings[k] = v
  101. categories_as_tabs = user_settings.get('categories_as_tabs')
  102. if categories_as_tabs:
  103. default_settings['categories_as_tabs'] = categories_as_tabs
  104. # parse the engines
  105. remove_engines = None
  106. keep_only_engines = None
  107. use_default_settings = user_settings.get('use_default_settings')
  108. if isinstance(use_default_settings, dict):
  109. remove_engines = use_default_settings.get('engines', {}).get('remove')
  110. keep_only_engines = use_default_settings.get('engines', {}).get('keep_only')
  111. if 'engines' in user_settings or remove_engines is not None or keep_only_engines is not None:
  112. engines = default_settings['engines']
  113. # parse "use_default_settings.engines.remove"
  114. if remove_engines is not None:
  115. engines = list(filterfalse(lambda engine: (engine.get('name')) in remove_engines, engines))
  116. # parse "use_default_settings.engines.keep_only"
  117. if keep_only_engines is not None:
  118. engines = list(filter(lambda engine: (engine.get('name')) in keep_only_engines, engines))
  119. # parse "engines"
  120. user_engines = user_settings.get('engines')
  121. if user_engines:
  122. engines_dict = dict((definition['name'], definition) for definition in engines)
  123. for user_engine in user_engines:
  124. default_engine = engines_dict.get(user_engine['name'])
  125. if default_engine:
  126. update_dict(default_engine, user_engine)
  127. else:
  128. engines.append(user_engine)
  129. # store the result
  130. default_settings['engines'] = engines
  131. return default_settings
  132. def is_use_default_settings(user_settings):
  133. use_default_settings = user_settings.get('use_default_settings')
  134. if use_default_settings is True:
  135. return True
  136. if isinstance(use_default_settings, dict):
  137. return True
  138. if use_default_settings is False or use_default_settings is None:
  139. return False
  140. raise ValueError('Invalid value for use_default_settings')
  141. def load_settings(load_user_settings=True) -> tuple[dict, str]:
  142. """Function for loading the settings of the SearXNG application
  143. (:ref:`settings.yml <searxng settings.yml>`)."""
  144. msg = f"load the default settings from {DEFAULT_SETTINGS_FILE}"
  145. cfg = load_yaml(DEFAULT_SETTINGS_FILE)
  146. cfg_folder = get_user_cfg_folder()
  147. if not load_user_settings or not cfg_folder:
  148. return cfg, msg
  149. settings_yml = os.environ.get("SEARXNG_SETTINGS_PATH")
  150. if settings_yml and Path(settings_yml).is_file():
  151. # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a file
  152. settings_yml = Path(settings_yml).name
  153. else:
  154. # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a folder
  155. settings_yml = SETTINGS_YAML
  156. cfg_file = cfg_folder / settings_yml
  157. if not cfg_file.exists():
  158. return cfg, msg
  159. msg = f"load the user settings from {cfg_file}"
  160. user_cfg = load_yaml(cfg_file)
  161. if is_use_default_settings(user_cfg):
  162. # the user settings are merged with the default configuration
  163. msg = f"merge the default settings ( {DEFAULT_SETTINGS_FILE} ) and the user settings ( {cfg_file} )"
  164. update_settings(cfg, user_cfg)
  165. else:
  166. cfg = user_cfg
  167. return cfg, msg