Files
xian_algorithm_new/app/services/qgis/qgis_env.py
T

264 lines
8.0 KiB
Python
Raw Normal View History

2026-06-19 17:04:03 +08:00
"""
2026-06-21 22:30:04 +08:00
QGIS 环境配置模块 — Docker 模式。
2026-06-20 15:50:24 +08:00
2026-06-21 22:30:04 +08:00
所有 QGIS 操作通过 docker exec 在容器内执行,
主进程(Python 3.10)无需安装 QGIS 或处理 DLL 加载。
容器内使用 qgis/qgis 官方镜像(QGIS 3.x + Python 3)。
2026-06-20 15:50:24 +08:00
本模块提供:
2026-06-21 22:30:04 +08:00
- 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 解释器路径
2026-06-19 17:04:03 +08:00
"""
import os
2026-06-21 22:30:04 +08:00
import platform
2026-06-19 17:04:03 +08:00
from pathlib import Path
from app.config.paths import get_logger
logger = get_logger("qgis.env")
2026-06-21 22:30:04 +08:00
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:
2026-06-19 17:04:03 +08:00
"""
2026-06-21 22:30:04 +08:00
构建 docker exec 命令。
2026-06-19 17:04:03 +08:00
2026-06-21 22:30:04 +08:00
Args:
python_path: 容器内 Python 路径(如 /usr/bin/python3
runner_script: 容器内 runner 脚本路径
json_file: 容器内 JSON 请求文件路径
2026-06-19 17:04:03 +08:00
2026-06-20 15:50:24 +08:00
Returns:
2026-06-21 22:30:04 +08:00
docker exec 命令列表
2026-06-20 15:50:24 +08:00
"""
2026-06-21 22:30:04 +08:00
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_storeG:/files → /files),其次匹配项目根目录(F:/project/... → /app)。
2026-06-20 15:50:24 +08:00
"""
2026-06-21 22:30:04 +08:00
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
2026-06-21 12:47:43 +08:00
2026-06-22 11:01:15 +08:00
def map_template_to_container(host_path: str) -> str:
"""将主机端模板路径映射到容器内本地路径(预拷贝后绕过 9P)。
主机: F:/project/xian/xian_algorithm_new/app/data/template/rainfall/xxx.qgz
容器: /data/template/rainfall/xxx.qgz
"""
try:
from config import settings
container_tpl = getattr(settings, "QGIS_DOCKER_TEMPLATE_DIR", "") or ""
except Exception:
container_tpl = ""
if not container_tpl:
# fallback: 用通用映射(走 9P,慢)
return map_host_to_container(host_path)
normalized = host_path.replace("\\", "/").lower()
# 找 "app/data/template/" 后面的部分
marker = "app/data/template/"
idx = normalized.find(marker)
if idx >= 0:
relative = host_path.replace("\\", "/")[idx + len(marker):]
return f"{container_tpl.rstrip('/')}/{relative}"
return map_host_to_container(host_path)
2026-06-21 22:30:04 +08:00
def build_docker_volume_mounts() -> list:
2026-06-20 15:50:24 +08:00
"""
2026-06-21 22:30:04 +08:00
构建 Docker 卷挂载参数列表(用于 docker run)。
2026-06-21 12:47:43 +08:00
2026-06-21 22:30:04 +08:00
挂载内容:
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()
2026-06-20 17:13:52 +08:00
2026-06-21 22:30:04 +08:00
mounts = [
f"{project_root}:{project_dir}:ro",
f"{host_file_store}:{container_file_store}",
]
return mounts
2026-06-20 17:13:52 +08:00
2026-06-21 22:30:04 +08:00
def build_docker_run_cmd(image: str = None) -> list:
2026-06-19 17:04:03 +08:00
"""
2026-06-21 22:30:04 +08:00
构建 docker run 启动命令(首次部署用)。
2026-06-19 17:04:03 +08:00
2026-06-21 22:30:04 +08:00
Returns:
docker run 命令列表
2026-06-19 17:04:03 +08:00
"""
2026-06-21 22:30:04 +08:00
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")