diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index bdc35c473..f287be148 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit bdc35c4731ee37e2191d52d3cf3ba6ccd37eecf8 +Subproject commit f287be148c8345913cba177982c4fb6e18ad70dc diff --git a/installer.py b/installer.py index 94f6592b9..a177e4422 100644 --- a/installer.py +++ b/installer.py @@ -1619,6 +1619,15 @@ def read_options(): opts = json.loads(opts) except Exception as e: log.error(f'Error reading options file: {file} {e}') + if os.path.isfile(args.secrets): + with open(args.secrets, encoding="utf8") as file: + try: + secrets = json.load(file) + if type(secrets) is str: + secrets = json.loads(secrets) + opts = opts | secrets + except Exception as e: + log.error(f"Error reading secrets file: {file} {e}") ts('options', t_start) diff --git a/modules/civitai/api_civitai.py b/modules/civitai/api_civitai.py index 520a5ce43..869e39ab3 100644 --- a/modules/civitai/api_civitai.py +++ b/modules/civitai/api_civitai.py @@ -272,7 +272,7 @@ def post_settings(request: dict): shared.opts.data['civitai_save_subfolder'] = save_subfolder if discard_hash_mismatch is not None: shared.opts.data['civitai_discard_hash_mismatch'] = discard_hash_mismatch - shared.opts.save(shared.config_filename) + shared.opts.save() return get_settings() @@ -606,5 +606,3 @@ def register_api(): # Legacy endpoints (backward compatibility) api.add_api_route("/sdapi/v1/civitai", legacy_get_civitai, methods=["GET"], response_model=list, tags=["Models"]) api.add_api_route("/sdapi/v1/civitai", legacy_post_civitai, methods=["POST"], response_model=list, tags=["Models"]) - - log.debug('CivitAI API: registered endpoints') diff --git a/modules/cmd_args.py b/modules/cmd_args.py index 35b3cfc3b..6c0390201 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -27,6 +27,7 @@ def add_core_args(p): def add_config_arg(p, data_dir): p.add_argument("--config", type=str, default=os.environ.get("SD_CONFIG", os.path.join(data_dir, 'config.json')), help="Use specific server configuration file, default: %(default)s") + p.add_argument("--secrets", type=str, default=os.environ.get("SD_SECRETS", os.path.join(data_dir, 'secrets.json')), help="Use specific server secrets file, default: %(default)s") def add_compute_args(p): @@ -42,9 +43,11 @@ def add_compute_args(p): p.add_argument("--no-half", default=env_flag("SD_NOHALF", False), action='store_true', help="Do not switch the model to 16-bit float, default: %(default)s") p.add_argument("--no-half-vae", default=env_flag("SD_NOHALFVAE", False), action='store_true', help="Do not switch VAE model to 16-bit float, default: %(default)s") + def add_ui_args(p): p.add_argument('--theme', type=str, default=os.environ.get("SD_THEME", None), help='Override UI theme') p.add_argument('--locale', type=str, default=os.environ.get("SD_LOCALE", None), help='Override UI locale') + p.add_argument("--enso", default=env_flag("SD_ENSO", False), action="store_true", help="Enable Enso frontend, default: %(default)s") def add_http_args(p): diff --git a/modules/options_handler.py b/modules/options_handler.py index 52f41bb47..5041eb86c 100644 --- a/modules/options_handler.py +++ b/modules/options_handler.py @@ -1,5 +1,6 @@ from __future__ import annotations import os +import sys import json import threading from typing import TYPE_CHECKING @@ -16,55 +17,79 @@ if TYPE_CHECKING: cmd_opts = cmd_args.parse_args() compatibility_opts = ['clip_skip', 'uni_pc_lower_order_final', 'uni_pc_order'] +secrets_pattern = ['_version', '_token', '_key', '_secret', '_password'] class Options: data_labels: dict[str, OptionInfo | LegacyOption] data: dict[str, Any] + secrets: dict[str, Any] typemap = {int: float} debug = os.environ.get('SD_CONFIG_DEBUG', None) is not None + secrets_debug = os.environ.get("SD_SECRETS_DEBUG", None) is not None - def __init__(self, options_templates: dict[str, OptionInfo | LegacyOption] = None, restricted_opts: set[str] | None = None, *, filename = ''): + def __init__(self, options_templates: dict[str, OptionInfo | LegacyOption] = None, restricted: set[str] | None = None, *, filename = '', secrets = ''): if options_templates is None: options_templates = {} - if restricted_opts is None: - restricted_opts = set() + if restricted is None: + restricted = set() super().__setattr__('data_labels', options_templates) super().__setattr__('data', {k: v.default for k, v in options_templates.items()}) + super().__setattr__('secrets', {}) self.filename: str = filename or cmd_opts.config - self.restricted_opts = restricted_opts + self.secretsfn: str = secrets or cmd_opts.secrets + self.restricted: set[str] = restricted self.legacy = [k for k, v in options_templates.items() if isinstance(v, LegacyOption)] self.load() - def __setattr__(self, key, value): # pylint: disable=inconsistent-return-statements - if key in self.data or key in self.data_labels: - if cmd_opts.freeze: - log.warning(f'Settings are frozen: {key}') - return - if cmd_opts.hide_ui_dir_config and key in self.restricted_opts: - log.warning(f'Settings key is restricted: {key}') - return - if self.debug: - log.trace(f'Settings set: {key}={value}') - if key in self.legacy: - log.warning(f'Settings set: {key}={value} legacy') - self.data[key] = value - return - return super().__setattr__(key, value) # pylint: disable=super-with-arguments + def __getattr__(self, item): + if item == 'secrets': + return super().__getattribute__('secrets') + if item == 'data': + return super().__getattribute__('data') + if item in self.secrets: + if self.secrets_debug: + fn = f"{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}" # pylint: disable=protected-access + log.trace(f"Secret: get={item} fn={fn}") + return self.secrets[item] + if item in self.data: + return self.data[item] + if item in self.data_labels: + return self.data_labels[item].default + return super().__getattribute__(item) # pylint: disable=super-with-arguments def get(self, item): + if item in self.secrets: + if self.secrets_debug: + fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access + log.trace(f"Secret: get={item} fn={fn}") + return self.secrets[item] if item in self.data: return self.data[item] if item in self.data_labels: return self.data_labels[item].default - return super().__getattribute__(item) # pylint: disable=super-with-arguments + return super().__getattribute__(item) # pylint: disable=super-with-arguments - def __getattr__(self, item): - if item in self.data: - return self.data[item] - if item in self.data_labels: - return self.data_labels[item].default - return super().__getattribute__(item) # pylint: disable=super-with-arguments + def __setattr__(self, key, value): # pylint: disable=inconsistent-return-statements + if (key in self.data_labels) or (key in self.data) or (key in self.secrets): + if cmd_opts.freeze: + log.warning(f"Settings are frozen: {key}") + return + if cmd_opts.hide_ui_dir_config and key in self.restricted: + log.warning(f"Settings key is restricted: {key}") + return + if self.debug: + log.trace(f"Settings set: {key}={value}") + if key in self.legacy: + log.warning(f"Settings set: {key}={value} legacy") + if any(key.endswith(pattern) for pattern in secrets_pattern): + if self.secrets_debug: + log.trace(f"Secret: set={key}") + self.secrets[key] = value + else: + self.data[key] = value + return + return super().__setattr__(key, value) # pylint: disable=super-with-arguments def set(self, key, value): """sets an option and calls its onchange callback, returning True if the option changed and False otherwise""" @@ -102,28 +127,22 @@ class Options: components = [k for k, v in self.data_labels.items() if v.visible] return components - def save_atomic(self, filename=None, silent=False): + def save_atomic(self, silent=False): if self.debug: - log.debug(f'Settings: save settings="{self.filename}" override="{filename}" cmd="{cmd_opts.config}" cwd="{os.getcwd()}"') - if filename is None: - filename = self.filename - filename = os.path.abspath(filename) + log.debug(f'Settings: save settings="{self.filename}" secrets="{self.secretsfn}" cmd="{cmd_opts.config}" cwd="{os.getcwd()}"') + filename = os.path.abspath(self.filename) + secretsfn = os.path.abspath(self.secretsfn) if cmd_opts.freeze: log.warning(f'Setting: fn="{filename}" save disabled') return try: - diff = {} unused_settings = [] - - # if self.debug: - # log.debug('Settings: user') - # for k, v in self.data.items(): - # log.trace(f' Config: item={k} value={v} default={self.data_labels[k].default if k in self.data_labels else None}') - if self.debug: - log.debug(f'Settings: total={len(self.data.keys())} known={len(self.data_labels.keys())}') + log.debug(f'Settings: total={len(self.data.keys())} secrets={len(self.secrets.keys())} known={len(self.data_labels.keys())}') - for k, v in self.data.items(): + all_options = self.data | self.secrets + diff = {} + for k, v in all_options.items(): if k in self.data_labels: default = self.data_labels[k].default if isinstance(v, list): @@ -142,16 +161,24 @@ class Options: unused_settings.append(k) if self.debug: log.trace(f'Settings unknown: {k}={v}') - writefile(diff, filename, silent=silent) + options = {} + secrets = {} + for k, v in diff.items(): + if any(k.endswith(pattern) for pattern in secrets_pattern): + secrets[k] = v + else: + options[k] = v + writefile(options, filename, silent=silent) + writefile(secrets, secretsfn, silent=silent) if self.debug: log.trace(f'Settings save: count={len(diff.keys())} {diff}') if len(unused_settings) > 0: log.debug(f"Settings: unused={unused_settings}") except Exception as err: - log.error(f'Settings: fn="{filename}" {err}') + log.error(f'Settings: config="{filename}" secrets="{secretsfn}" {err}') - def save(self, filename=None, silent=False): - threading.Thread(target=self.save_atomic, args=(filename, silent)).start() + def save(self, silent=False): + threading.Thread(target=self.save_atomic, args=(silent,)).start() def same_type(self, x, y): if x is None or y is None: @@ -160,15 +187,15 @@ class Options: type_y = self.typemap.get(type(y), type(y)) return type_x == type_y - def load(self, filename=None): - if filename is None: - filename = self.filename - filename = os.path.abspath(filename) + def load(self): + filename = os.path.abspath(self.filename) + secretsfn = os.path.abspath(self.secretsfn) if not os.path.isfile(filename): - log.debug(f'Settings: fn="{filename}" created') - self.save(filename) + log.debug(f'Settings: config="{filename}" secrets="{secretsfn}" created') + self.save() return self.data = readfile(filename, lock=True, as_type="dict") + self.secrets = readfile(secretsfn, lock=True, as_type="dict") if self.data.get('quicksettings') is not None and self.data.get('quicksettings_list') is None: self.data['quicksettings_list'] = [i.strip() for i in self.data.get('quicksettings', '').split(',')] unknown_settings = [] diff --git a/modules/shared.py b/modules/shared.py index d6d1e043a..2bd4cdc53 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -162,7 +162,8 @@ from modules.shared_legacy import get_legacy_options options_templates.update(get_legacy_options()) from modules.options_handler import Options config_filename = cmd_opts.config -opts = Options(options_templates, restricted_opts, filename=config_filename) +secrets_filename = cmd_opts.secrets +opts = Options(options_templates, restricted=restricted_opts, filename=config_filename, secrets=secrets_filename) cmd_opts = cmd_args.settings_args(opts, cmd_opts) cmd_opts = cmd_args.override_args(opts, cmd_opts) if cmd_opts.locale is not None: