| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- """DMS环境配置。"""
- from __future__ import annotations
- import json
- import os
- from pathlib import Path
- from typing import Any
- from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
- import shutil
- from dms.storage.envelope import load_kek
- def _json_serializer(value: Any) -> str:
- """SQLAlchemy JSON 列序列化:原文中文(ensure_ascii=False),对齐达梦存量与 Java 版。"""
- return json.dumps(value, ensure_ascii=False)
- BACKEND_ROOT = Path(__file__).resolve().parents[1]
- DEFAULT_STORAGE_ROOT = BACKEND_ROOT / "dms-storage"
- DEFAULT_UI_DICTIONARY_CONFIG_PATH = (
- BACKEND_ROOT / "dms" / "resources" / "ui-dictionaries.zh-CN.json"
- )
- DEFAULT_DOCUMENT_RELATION_PRESET_CONFIG_PATH = (
- BACKEND_ROOT / "dms" / "resources" / "document-relation-presets.zh-CN.json"
- )
- # 当前数据库:达梦 DM(dm+dmPython 方言,默认端口 5236,schema 由用户 DMS 决定)。
- # 原 MySQL 默认链接(保留备查,回切时恢复):
- # "mysql+pymysql://dms_app:change-me@127.0.0.1:3306/dms?charset=utf8mb4"
- DEFAULT_DATABASE_URL = "dm+dmPython://DMS:change-me@127.0.0.1:5236"
- def _positive_int(name: str, default: int) -> int:
- raw_value = os.environ.get(name)
- if raw_value is None or not raw_value.strip():
- return default
- try:
- value = int(raw_value)
- except ValueError as exc:
- raise ValueError(f"{name}必须是正整数") from exc
- if value <= 0:
- raise ValueError(f"{name}必须是正整数")
- return value
- def _non_negative_int(name: str, default: int) -> int:
- """非负整数(0 有语义:表示关闭该能力,如空闲登出)。"""
- raw_value = os.environ.get(name)
- if raw_value is None or not raw_value.strip():
- return default
- try:
- value = int(raw_value)
- except ValueError as exc:
- raise ValueError(f"{name}必须是非负整数") from exc
- if value < 0:
- raise ValueError(f"{name}必须是非负整数")
- return value
- def resolve_backup_config_path() -> Path:
- """备份配置文件路径:环境变量覆盖,缺省为工作目录(与 Java 版口径一致)。"""
- value = os.environ.get("DMS_BACKUP_CONFIG_PATH", "").strip()
- if value:
- return Path(value).expanduser().resolve()
- return Path("dms-backup-config.json").resolve()
- def resolve_env_path() -> Path:
- """.env 路径(备份 best-effort 副本用):环境变量覆盖,缺省为工作目录。"""
- value = os.environ.get("DMS_ENV_FILE", "").strip()
- if value:
- return Path(value).expanduser().resolve()
- return Path(".env").resolve()
- def load_dms_config() -> dict[str, Any]:
- """从环境变量加载DMS配置,不读取或覆盖现有AI配置。"""
- storage_value = os.environ.get("DMS_STORAGE_ROOT", "").strip()
- storage_root = Path(storage_value or DEFAULT_STORAGE_ROOT).expanduser()
- max_upload_size = _positive_int(
- "DMS_MAX_UPLOAD_SIZE", 100 * 1024 * 1024
- )
- max_file_size = _positive_int(
- "DMS_MAX_FILE_SIZE", min(50 * 1024 * 1024, max_upload_size)
- )
- if max_file_size > max_upload_size:
- raise ValueError("DMS_MAX_FILE_SIZE不得超过DMS_MAX_UPLOAD_SIZE")
- database_url = os.environ.get("DMS_DATABASE_URL", "").strip()
- ui_dictionary_config_value = os.environ.get(
- "DMS_UI_DICTIONARY_CONFIG_PATH", ""
- ).strip()
- ui_dictionary_config_path = Path(
- ui_dictionary_config_value or DEFAULT_UI_DICTIONARY_CONFIG_PATH
- ).expanduser()
- relation_preset_config_value = os.environ.get(
- "DMS_DOCUMENT_RELATION_PRESET_CONFIG_PATH", ""
- ).strip()
- relation_preset_config_path = Path(
- relation_preset_config_value
- or DEFAULT_DOCUMENT_RELATION_PRESET_CONFIG_PATH
- ).expanduser()
- business_timezone = (
- os.environ.get("DMS_BUSINESS_TIMEZONE", "").strip()
- or "Asia/Shanghai"
- )
- try:
- ZoneInfo(business_timezone)
- except (ZoneInfoNotFoundError, ValueError) as exc:
- raise ValueError("DMS_BUSINESS_TIMEZONE必须是有效的IANA时区") from exc
- office_preview_enabled = (
- os.environ.get("DMS_OFFICE_PREVIEW_ENABLED", "true").strip().lower()
- in {"1", "true", "yes", "on"}
- )
- libreoffice_executable = (
- os.environ.get("DMS_LIBREOFFICE_EXECUTABLE", "").strip()
- or shutil.which("soffice")
- or shutil.which("libreoffice")
- or ""
- )
- libreoffice_timeout_seconds = _positive_int(
- "DMS_LIBREOFFICE_TIMEOUT_SECONDS", 60
- )
- libreoffice_max_concurrency = _positive_int(
- "DMS_LIBREOFFICE_MAX_CONCURRENCY", 2
- )
- resolved_database_url = database_url or DEFAULT_DATABASE_URL
- # init_command(SET time_zone)为 pymysql 专属参数,达梦 dmPython 不接受;
- # 仅在 MySQL 链接时下发。
- connect_args = (
- {"init_command": "SET time_zone = '+00:00'"}
- if resolved_database_url.startswith(("mysql", "jdbc:mysql"))
- else {}
- )
- engine_options: dict[str, Any] = {
- "isolation_level": "READ COMMITTED",
- "pool_pre_ping": True,
- "pool_recycle": 1800,
- "connect_args": connect_args,
- }
- # json_serializer 同为 MySQL/通用方言参数,达梦方言(DMDialect_dmPython)
- # 不接受该 kwarg(create_engine 直接报错);仅非达梦链接下发。
- if not resolved_database_url.startswith(("dm", "jdbc:dm")):
- # JSON 列统一原文中文序列化(ensure_ascii=False),与存量数据及
- # Java 版 Jackson 写入格式一致;否则 \uXXXX 转义会导致 LIKE 检索漏配。
- engine_options["json_serializer"] = _json_serializer
- return {
- "SQLALCHEMY_DATABASE_URI": resolved_database_url,
- "SQLALCHEMY_TRACK_MODIFICATIONS": False,
- "SQLALCHEMY_ENGINE_OPTIONS": engine_options,
- "DMS_STORAGE_ROOT": str(storage_root.resolve()),
- "DMS_MAX_UPLOAD_SIZE": max_upload_size,
- "DMS_MAX_FILE_SIZE": max_file_size,
- "MAX_CONTENT_LENGTH": max_upload_size,
- "DMS_BATCH_MAX_FILES": _positive_int("DMS_BATCH_MAX_FILES", 50),
- "DMS_BUSINESS_TIMEZONE": business_timezone,
- "DMS_JWT_SECRET": os.environ.get("DMS_JWT_SECRET", ""),
- "DMS_JWT_ISSUER": "dms",
- "DMS_JWT_EXPIRES_SECONDS": _positive_int(
- "DMS_JWT_EXPIRES_SECONDS", 3600
- ),
- "DMS_JWT_KEEP_SIGNED_IN_EXPIRES_SECONDS": 604800,
- # 页面空闲自动登出(契约1.29):分钟数,0 表示关闭
- "DMS_IDLE_LOGOUT_MINUTES": _non_negative_int(
- "DMS_IDLE_LOGOUT_MINUTES", 5
- ),
- # 契约1.21:应用层信封加密 KEK(base64 编码 32 字节);未配置则明文落盘
- "DMS_STORAGE_KEK_BYTES": load_kek(
- os.environ.get("DMS_STORAGE_KEK", "")
- ),
- "DMS_UI_DICTIONARY_CONFIG_PATH": str(
- ui_dictionary_config_path.resolve()
- ),
- "DMS_DOCUMENT_RELATION_PRESET_CONFIG_PATH": str(
- relation_preset_config_path.resolve()
- ),
- "DMS_OFFICE_PREVIEW_ENABLED": office_preview_enabled,
- "DMS_LIBREOFFICE_EXECUTABLE": libreoffice_executable,
- "DMS_LIBREOFFICE_TIMEOUT_SECONDS": libreoffice_timeout_seconds,
- "DMS_LIBREOFFICE_MAX_CONCURRENCY": libreoffice_max_concurrency,
- # 运行日志文件(契约1.23):默认 logs/dms-server.log,OFF/NONE 关闭
- "DMS_LOG_FILE": (
- os.environ.get("DMS_LOG_FILE", "").strip() or "logs/dms-server.log"
- ),
- # 资源采样(契约1.23):采样间隔秒数与保留天数
- "DMS_METRIC_INTERVAL_SECONDS": _positive_int(
- "DMS_METRIC_INTERVAL_SECONDS", 30
- ),
- "DMS_METRIC_RETENTION_DAYS": _positive_int(
- "DMS_METRIC_RETENTION_DAYS", 7
- ),
- # 系统管理页监控信息展示(契约1.26):false 时仅显示维护操作与备份管理
- "DMS_SYSTEM_MONITOR_ENABLED": (
- os.environ.get("DMS_SYSTEM_MONITOR_ENABLED", "false").strip().lower()
- in {"1", "true", "yes", "on"}
- ),
- # 登录图片验证码(安全需求4):缺省启用;关闭时登录回归三键请求体
- "DMS_CAPTCHA_ENABLED": (
- os.environ.get("DMS_CAPTCHA_ENABLED", "true").strip().lower()
- in {"1", "true", "yes", "on"}
- ),
- # 超级管理员登录开关(安全需求7):false 时内置admin禁止登录(报错与
- # 密码错误一致)、用户列表隐藏admin、创建同名用户报"不能创建该名称的用户"
- "DMS_ALLOW_ADMIN_LOGIN": (
- os.environ.get("DMS_ALLOW_ADMIN_LOGIN", "true").strip().lower()
- in {"1", "true", "yes", "on"}
- ),
- }
|