""" QGIS 环境配置模块 — Docker 模式。 所有 QGIS 操作通过 docker exec 在容器内执行, 主进程(Python 3.10)无需安装 QGIS 或处理 DLL 加载。 容器内使用 qgis/qgis 官方镜像(QGIS 3.x + Python 3)。 本模块提供: - get_docker_container(): 获取 Docker 容器名称 - get_docker_project_dir(): 获取容器内项目根目录 - build_docker_exec_cmd(): 构建 docker exec 命令 - build_docker_volume_mounts(): 构建 docker run 卷挂载参数 - get_runner_script(): 获取 qgis_runner.py 的绝对路径 - get_container_python_path(): 获取容器内 Python 解释器路径 """ import os import platform from pathlib import Path from app.config.paths import get_logger logger = get_logger("qgis.env") IS_WINDOWS = platform.system() == "Windows" # ============================================================ # Docker 容器配置 # ============================================================ def get_docker_container() -> str: """获取 Docker 容器名称/ID""" try: from config import settings container = getattr(settings, "QGIS_DOCKER_CONTAINER", "") or "" if container.strip(): return container.strip() except Exception: pass # 环境变量覆盖 env_container = os.environ.get("QGIS_DOCKER_CONTAINER", "") if env_container.strip(): return env_container.strip() return "qgis-server" def get_docker_project_dir() -> str: """获取容器内项目根目录(代码挂载目标路径)""" try: from config import settings project_dir = getattr(settings, "QGIS_DOCKER_PROJECT_DIR", "") or "" if project_dir.strip(): return project_dir.strip() except Exception: pass env_dir = os.environ.get("QGIS_DOCKER_PROJECT_DIR", "") if env_dir.strip(): return env_dir.strip() return "/app" def get_container_python_path() -> str: """获取容器内 Python 解释器路径""" try: from config import settings python_path = getattr(settings, "QGIS_DOCKER_PYTHON", "") or "" if python_path.strip(): return python_path.strip() except Exception: pass env_path = os.environ.get("QGIS_DOCKER_PYTHON", "") if env_path.strip(): return env_path.strip() # qgis/qgis 官方镜像默认路径 return "/usr/bin/python3" # ============================================================ # docker exec 命令构建 # ============================================================ def build_docker_exec_cmd(python_path: str, runner_script: str, json_file: str) -> list: """ 构建 docker exec 命令。 Args: python_path: 容器内 Python 路径(如 /usr/bin/python3) runner_script: 容器内 runner 脚本路径 json_file: 容器内 JSON 请求文件路径 Returns: docker exec 命令列表 """ container = get_docker_container() project_dir = get_docker_project_dir() # Qt 平台插件(从配置读取,避免硬编码) try: from config import settings qt_platform = getattr(settings, "QGIS_DOCKER_QT_PLATFORM", "offscreen") except Exception: qt_platform = "offscreen" cmd = [ "docker", "exec", "-w", project_dir, "-e", f"QT_QPA_PLATFORM={qt_platform}", container, python_path, runner_script, json_file, ] return cmd # ============================================================ # docker run 卷挂载(首次启动容器用) # ============================================================ def get_host_file_store() -> str: """获取主机端文件输出目录""" try: from config import settings fs = getattr(settings, "FILE_STORE_DIR", "") or "" if fs.strip(): return fs.strip().replace("\\", "/") except Exception: pass return "G:/files" def get_container_file_store() -> str: """获取容器内文件输出目录(Linux 路径)""" # 挂载时主机 G:/files → 容器内 /files host = get_host_file_store() # 取最后一段目录名作为容器内路径 tail = host.rstrip("/").rsplit("/", 1)[-1] return f"/{tail}" def map_host_to_container(path: str) -> str: """将主机路径映射为容器内路径。 优先匹配 file_store(G:/files → /files),其次匹配项目根目录(F:/project/... → /app)。 """ normalized = path.replace("\\", "/") # 1. file_store 路径映射 host_fs = get_host_file_store() container_fs = get_container_file_store() if normalized.lower().startswith(host_fs.lower()): return container_fs + normalized[len(host_fs):] # 2. 项目根目录映射 project_root = str(Path(__file__).parent.parent.parent.parent).replace("\\", "/") project_dir = get_docker_project_dir() if normalized.lower().startswith(project_root.lower()): return project_dir + normalized[len(project_root):] return normalized def map_container_to_host(path: str) -> str: """将容器内路径映射回主机路径""" host = get_host_file_store() container = get_container_file_store() if path.startswith(container): return host + path[len(container):] return path def build_docker_volume_mounts() -> list: """ 构建 Docker 卷挂载参数列表(用于 docker run)。 挂载内容: 1. 项目代码目录 → 容器内 /app(只读) 2. 输出文件目录 → 容器内 /files(读写) """ project_root = str(Path(__file__).parent.parent.parent.parent) host_file_store = get_host_file_store() container_file_store = get_container_file_store() project_dir = get_docker_project_dir() mounts = [ f"{project_root}:{project_dir}:ro", f"{host_file_store}:{container_file_store}", ] return mounts def build_docker_run_cmd(image: str = None) -> list: """ 构建 docker run 启动命令(首次部署用)。 Returns: docker run 命令列表 """ if image is None: try: from config import settings image = getattr(settings, "QGIS_DOCKER_IMAGE", "") or "" if not image.strip(): image = "qgis/qgis:latest" except Exception: image = "qgis/qgis:latest" # 容器保活命令(从配置读取) try: from config import settings keep_alive = getattr(settings, "QGIS_DOCKER_KEEP_ALIVE", "sleep infinity") except Exception: keep_alive = "sleep infinity" container = get_docker_container() mounts = build_docker_volume_mounts() cmd = [ "docker", "run", "-d", "--name", container, "--restart", "unless-stopped", ] for m in mounts: cmd.extend(["-v", m]) cmd.append(image) # 保活命令 cmd.extend(keep_alive.split()) return cmd # ============================================================ # Runner 脚本路径 # ============================================================ def get_runner_script() -> str: """获取 qgis_runner.py 的绝对路径(主机端)""" return str(Path(__file__).parent / "qgis_runner.py")