160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
|
|
"""
|
|||
|
|
将主机端 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()
|