""" 将主机端 GPKG 和模板文件预拷贝到 Docker 容器本地文件系统。 WSL2 9P 文件系统随机读取极慢(GPKG 62MB 耗时 6-10s,模板 ZIP 也慢), 拷贝到容器内 /data/ 后读取仅需 ~0.5s。 用法: python app/script/copy_data_to_container.py [--container qgis-server] [--dry-run] [--only gpkg|template] 前置条件: Docker 容器已启动(docker start qgis-server) """ import argparse import os import subprocess import sys import time from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent def _load_config(): """从 settings.toml 读取配置""" try: from config import settings return { "gpkg_subdir": getattr(settings, "QGIS_GPKG_DIR", "app/data/gpkg"), "template_subdir": "app/data/template", "container": getattr(settings, "QGIS_DOCKER_CONTAINER", "qgis-server"), "container_gpkg": getattr(settings, "QGIS_DOCKER_GPKG_DIR", "/data/gpkg"), "container_template": getattr(settings, "QGIS_DOCKER_TEMPLATE_DIR", "/data/template"), } except ImportError: pass # fallback: 解析 TOML cfg = { "gpkg_subdir": "app/data/gpkg", "template_subdir": "app/data/template", "container": "qgis-server", "container_gpkg": "/data/gpkg", "container_template": "/data/template", } toml_path = PROJECT_ROOT / "settings.toml" if toml_path.exists(): for line in toml_path.read_text(encoding="utf-8").splitlines(): line = line.strip() for key, prefix in [ ("gpkg_subdir", "QGIS_GPKG_DIR"), ("container", "QGIS_DOCKER_CONTAINER"), ("container_gpkg", "QGIS_DOCKER_GPKG_DIR"), ("container_template", "QGIS_DOCKER_TEMPLATE_DIR"), ]: if line.startswith(prefix) and "=" in line: cfg[key] = line.split("=", 1)[1].strip().strip('"').strip("'") return cfg def _run(cmd, timeout=10, **kwargs): """subprocess.run 封装,统一 UTF-8 编码,避免 Windows GBK 报错""" result = subprocess.run( cmd, capture_output=True, timeout=timeout, encoding="utf-8", errors="replace", **kwargs, ) return result def _check_container(container: str): """检查容器是否运行""" result = _run(["docker", "inspect", "--format={{.State.Running}}", container], timeout=5) if (result.stdout or "").strip() != "true": raise RuntimeError(f"容器 {container} 未运行,请先 docker start {container}") def _copy_dir_to_container(host_dir: Path, container: str, container_dest: str, label: str, dry_run: bool): """拷贝单个目录到容器""" if not host_dir.is_dir(): print(f" [{label}] 目录不存在,跳过: {host_dir}") return 0 files = list(host_dir.rglob("*")) files = [f for f in files if f.is_file()] if not files: print(f" [{label}] 目录为空,跳过: {host_dir}") return 0 total_size = sum(f.stat().st_size for f in files) print(f" [{label}] 主机目录: {host_dir}") print(f" 文件数: {len(files)}, 总大小: {total_size / 1024 / 1024:.1f} MB") print(f" 容器目标: {container}:{container_dest}") if dry_run: print(f" [dry-run] 跳过") return 0 # 确保目标目录存在 parent_dir = container_dest.rsplit("/", 1)[0] _run(["docker", "exec", container, "mkdir", "-p", parent_dir]) # 清理旧目录 _run(["docker", "exec", container, "rm", "-rf", container_dest]) # docker cp t0 = time.time() result = _run(["docker", "cp", str(host_dir), f"{container}:{container_dest}"], timeout=120) elapsed = time.time() - t0 if result.returncode != 0: raise RuntimeError(f"[{label}] docker cp 失败: {result.stderr}") # 验证 verify = _run(["docker", "exec", container, "find", container_dest, "-type", "f"]) stdout = (verify.stdout or "").strip() count = len([l for l in stdout.splitlines() if l.strip()]) print(f" 拷贝完成: {elapsed:.1f}s, 容器内 {count} 个文件") return elapsed def copy_to_container(container: str = None, dry_run: bool = False, only: str = None): """将 GPKG 和模板拷贝到容器本地文件系统""" cfg = _load_config() if container is None: container = cfg["container"] _check_container(container) t_total = time.time() print(f"=== 预拷贝静态数据到容器 {container} ===\n") if only != "template": host_gpkg = PROJECT_ROOT / cfg["gpkg_subdir"] _copy_dir_to_container(host_gpkg, container, cfg["container_gpkg"], "GPKG", dry_run) print() if only != "gpkg": host_template = PROJECT_ROOT / cfg["template_subdir"] _copy_dir_to_container(host_template, container, cfg["container_template"], "模板", dry_run) elapsed = time.time() - t_total print(f"\n=== 总耗时: {elapsed:.1f}s ===") def main(): parser = argparse.ArgumentParser(description="将 GPKG 和模板预拷贝到 Docker 容器本地 FS") parser.add_argument("--container", default=None, help="Docker 容器名称") parser.add_argument("--dry-run", action="store_true", help="仅显示信息,不实际拷贝") parser.add_argument("--only", choices=["gpkg", "template"], help="只拷贝指定类型") args = parser.parse_args() try: copy_to_container(container=args.container, dry_run=args.dry_run, only=args.only) except Exception as e: print(f"错误: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()