mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
"""Database-backed configuration with per-key storage.
|
|
|
|
Replaces the old single-row JSON blob machinery with a simple per-key model
|
|
mirroring cptr's Config.
|
|
|
|
Each config key is stored as its own row: key TEXT PK, value JSON.
|
|
Reads are direct DB lookups. Writes are explicit awaited upserts that raise on
|
|
failure (no more fire-and-forget create_task).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from typing import Any, ClassVar
|
|
|
|
from open_webui.internal.db import Base, get_async_db
|
|
from sqlalchemy import JSON, BigInteger, Column, Text, select
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# ── Model ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class Config(Base):
|
|
"""Per-key config storage. Each row is one config key."""
|
|
|
|
__tablename__ = 'config'
|
|
|
|
key = Column(Text, primary_key=True)
|
|
value = Column(JSON, nullable=False)
|
|
updated_at = Column(BigInteger, nullable=True)
|
|
|
|
DEFAULTS: ClassVar[dict[str, Any]] = {}
|
|
PERSISTENT_ENABLED: ClassVar[bool] = True
|
|
OAUTH_PERSISTENT_ENABLED: ClassVar[bool] = False
|
|
|
|
# ── Class methods ────────────────────────────────────────
|
|
|
|
@classmethod
|
|
def configure(
|
|
cls,
|
|
*,
|
|
defaults: dict[str, Any] | None = None,
|
|
enable_persistent: bool = True,
|
|
enable_oauth_persistent: bool = False,
|
|
) -> None:
|
|
cls.DEFAULTS = defaults or {}
|
|
cls.PERSISTENT_ENABLED = enable_persistent
|
|
cls.OAUTH_PERSISTENT_ENABLED = enable_oauth_persistent
|
|
|
|
@classmethod
|
|
def default_value(cls, key: str, default: Any = None) -> Any:
|
|
return cls.DEFAULTS.get(key, default)
|
|
|
|
@classmethod
|
|
def persistent_enabled_for(cls, key: str) -> bool:
|
|
if not cls.PERSISTENT_ENABLED:
|
|
return False
|
|
if key.startswith('oauth.') and not cls.OAUTH_PERSISTENT_ENABLED:
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
async def get(key: str, default: Any = None) -> Any:
|
|
"""Get a config value by key. Returns default if not set."""
|
|
if not Config.persistent_enabled_for(key):
|
|
return Config.default_value(key, default)
|
|
async with get_async_db() as db:
|
|
row = await db.get(Config, key)
|
|
return row.value if row else Config.default_value(key, default)
|
|
|
|
@staticmethod
|
|
async def get_many(*keys: str) -> dict:
|
|
"""Get multiple config values. Returns {key: value} for keys that exist."""
|
|
disabled_values = {
|
|
key: Config.default_value(key)
|
|
for key in keys
|
|
if not Config.persistent_enabled_for(key) and key in Config.DEFAULTS
|
|
}
|
|
enabled_keys = {key for key in keys if Config.persistent_enabled_for(key)}
|
|
if not enabled_keys:
|
|
return disabled_values
|
|
async with get_async_db() as db:
|
|
result = await db.execute(select(Config).where(Config.key.in_(enabled_keys)))
|
|
values = {row.key: row.value for row in result.scalars().all()}
|
|
return {
|
|
key: values.get(key, Config.default_value(key))
|
|
for key in keys
|
|
if key in values or key in Config.DEFAULTS or key in disabled_values
|
|
}
|
|
|
|
@staticmethod
|
|
async def get_namespace(namespace: str) -> dict:
|
|
"""Get all config keys under a dotted namespace."""
|
|
default_values = {
|
|
key: value
|
|
for key, value in Config.DEFAULTS.items()
|
|
if key.startswith(f'{namespace}.') and not Config.persistent_enabled_for(key)
|
|
}
|
|
if not Config.PERSISTENT_ENABLED:
|
|
return default_values
|
|
async with get_async_db() as db:
|
|
result = await db.execute(select(Config).where(Config.key.like(f'{namespace}.%')))
|
|
values = {row.key: row.value for row in result.scalars().all()}
|
|
values.update(default_values)
|
|
return values
|
|
|
|
@staticmethod
|
|
async def get_all() -> dict:
|
|
"""Get all config as {key: value}."""
|
|
if not Config.PERSISTENT_ENABLED:
|
|
return dict(Config.DEFAULTS)
|
|
async with get_async_db() as db:
|
|
result = await db.execute(select(Config))
|
|
values = {row.key: row.value for row in result.scalars().all()}
|
|
if not Config.OAUTH_PERSISTENT_ENABLED:
|
|
values.update({key: value for key, value in Config.DEFAULTS.items() if key.startswith('oauth.')})
|
|
return values
|
|
|
|
@staticmethod
|
|
async def upsert(updates: dict) -> None:
|
|
"""Upsert multiple config key-value pairs. Raises on failure."""
|
|
async with get_async_db() as db:
|
|
now = int(time.time())
|
|
for key, value in updates.items():
|
|
existing = await db.get(Config, key)
|
|
if existing:
|
|
existing.value = value
|
|
existing.updated_at = now
|
|
else:
|
|
db.add(Config(key=key, value=value, updated_at=now))
|
|
await db.commit()
|
|
|
|
@staticmethod
|
|
async def delete(key: str) -> bool:
|
|
"""Delete a config key. Returns True if it existed."""
|
|
async with get_async_db() as db:
|
|
row = await db.get(Config, key)
|
|
if row:
|
|
await db.delete(row)
|
|
await db.commit()
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
async def clear() -> None:
|
|
"""Delete all config rows."""
|
|
from sqlalchemy import delete as sa_delete
|
|
|
|
async with get_async_db() as db:
|
|
await db.execute(sa_delete(Config))
|
|
await db.commit()
|
|
|
|
@staticmethod
|
|
async def seed_defaults(defaults: dict) -> None:
|
|
"""Insert keys that don't yet exist in the DB.
|
|
|
|
Called at startup to ensure all known config keys have values.
|
|
Existing DB values take precedence over defaults.
|
|
"""
|
|
async with get_async_db() as db:
|
|
result = await db.execute(select(Config.key))
|
|
existing_keys = {row[0] for row in result.all()}
|
|
|
|
now = int(time.time())
|
|
new_count = 0
|
|
for key, value in defaults.items():
|
|
if key not in existing_keys:
|
|
db.add(Config(key=key, value=value, updated_at=now))
|
|
existing_keys.add(key)
|
|
new_count += 1
|
|
|
|
if new_count:
|
|
await db.commit()
|
|
log.info('Seeded %d new config defaults', new_count)
|