From 50a901c1672d8f39a4d2ec1c31759c9176b4ad55 Mon Sep 17 00:00:00 2001 From: lingyuzeng Date: Mon, 13 Oct 2025 22:10:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B5=8B=E8=AF=95=E6=A1=88?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/utils/docker_client.py | 88 ++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/backend/app/utils/docker_client.py b/backend/app/utils/docker_client.py index 38812da..91f4c08 100644 --- a/backend/app/utils/docker_client.py +++ b/backend/app/utils/docker_client.py @@ -7,6 +7,7 @@ import logging import time from pathlib import Path from typing import Dict, Any, Optional, List +import sys try: import docker # type: ignore @@ -76,6 +77,14 @@ class DockerContainerManager: remove: bool = True, ) -> Dict[str, Any]: """在容器中执行命令,返回执行结果。""" + # 确保挂载目录存在且可写 + for host_path, spec in volumes.items(): + p = Path(host_path) + p.mkdir(parents=True, exist_ok=True) + try: + p.chmod(0o777) + except Exception: + pass if self._engine == "docker-sdk" and self._client is not None: return self._run_with_docker_sdk( command, volumes, environment, working_dir, name, detach, remove @@ -144,12 +153,36 @@ class DockerContainerManager: threads: int = 4, **kwargs: Any, ) -> Dict[str, Any]: - """在容器中运行 BtToxin_Digger 主分析(工作目录挂载到 /workspace)。""" - command: List[str] = [ + """在容器中运行 BtToxin_Digger 主分析(单目录方案)。""" + + # 1) 在宿主输出目录下准备 input_files,并复制输入文件 + work_input_dir = (output_dir / "input_files").resolve() + work_input_dir.mkdir(parents=True, exist_ok=True) + + import shutil + if sequence_type == "nucl": + pattern = f"*{scaf_suffix}" + elif sequence_type == "orfs": + pattern = f"*{kwargs.get('orfs_suffix', '.ffn')}" + elif sequence_type == "prot": + pattern = f"*{kwargs.get('prot_suffix', '.faa')}" + elif sequence_type == "reads": + pattern = "*" + else: + pattern = "*" + + copied_files = 0 + for f in input_dir.glob(pattern): + if f.is_file(): + shutil.copy2(f, work_input_dir / f.name) + copied_files += 1 + logger.info(f"已复制 {copied_files} 个输入文件到 {work_input_dir}") + + base_cmd: List[str] = [ "/usr/local/env-execute", "BtToxin_Digger", "--SeqPath", - "/data/input", + "/workspace/input_files", "--SequenceType", sequence_type, "--threads", @@ -157,32 +190,32 @@ class DockerContainerManager: ] if sequence_type == "nucl": - command += ["--Scaf_suffix", scaf_suffix] + base_cmd += ["--Scaf_suffix", scaf_suffix] elif sequence_type == "orfs": - command += ["--orfs_suffix", kwargs.get("orfs_suffix", ".ffn")] + base_cmd += ["--orfs_suffix", kwargs.get("orfs_suffix", ".ffn")] elif sequence_type == "prot": - command += ["--prot_suffix", kwargs.get("prot_suffix", ".faa")] + base_cmd += ["--prot_suffix", kwargs.get("prot_suffix", ".faa")] elif sequence_type == "reads": platform = kwargs.get("platform", "illumina") - command += ["--platform", platform] + base_cmd += ["--platform", platform] if platform == "illumina": r1 = kwargs.get("reads1_suffix", "_R1.fastq.gz") r2 = kwargs.get("reads2_suffix", "_R2.fastq.gz") sfx = kwargs.get("suffix_len") or len(r1) - v = self.validate_reads_filenames(input_dir, platform, r1, r2, sfx) + v = self.validate_reads_filenames(work_input_dir, platform, r1, r2, sfx) if not v.get("valid"): raise ValueError(f"Reads 文件验证失败: {v.get('error')}") sfx = v.get("suggested_suffix_len", sfx) - command += ["--reads1", r1, "--reads2", r2, "--suffix_len", str(sfx)] + base_cmd += ["--reads1", r1, "--reads2", r2, "--suffix_len", str(sfx)] elif platform in ("pacbio", "oxford"): r = kwargs.get("reads1_suffix", ".fastq.gz") gsize = kwargs.get("genome_size", "6.07m") sfx = kwargs.get("suffix_len") or len(r) - v = self.validate_reads_filenames(input_dir, platform, r, None, sfx) + v = self.validate_reads_filenames(work_input_dir, platform, r, None, sfx) if not v.get("valid"): raise ValueError(f"Reads 文件验证失败: {v.get('error')}") sfx = v.get("suggested_suffix_len", sfx) - command += ["--reads1", r, "--genomeSize", gsize, "--suffix_len", str(sfx)] + base_cmd += ["--reads1", r, "--genomeSize", gsize, "--suffix_len", str(sfx)] elif platform == "hybrid": short1 = kwargs.get("short1") short2 = kwargs.get("short2") @@ -190,9 +223,9 @@ class DockerContainerManager: if not all([short1, short2, long]): raise ValueError("hybrid 需要 short1/short2/long 三个完整文件名") for fn in (short1, short2, long): - if not (input_dir / fn).exists(): + if not (work_input_dir / fn).exists(): raise ValueError(f"文件不存在: {fn}") - command += [ + base_cmd += [ "--short1", short1, "--short2", @@ -204,19 +237,23 @@ class DockerContainerManager: ] if kwargs.get("assemble_only"): - command.append("--assemble_only") + base_cmd.append("--assemble_only") + # 2) 只挂载输出目录(含 input_files)与日志目录 volumes = { - str(input_dir.resolve()): {"bind": "/data/input", "mode": "ro"}, str(output_dir.resolve()): {"bind": "/workspace", "mode": "rw"}, str(log_dir.resolve()): {"bind": "/data/logs", "mode": "rw"}, } logger.info("开始 BtToxin_Digger 分析...") + + final_cmd = base_cmd + working_dir = "/workspace" + result = self.run_command_in_container( - command=command, + command=final_cmd, volumes=volumes, - working_dir="/workspace", + working_dir=working_dir, name=f"bttoxin_digger_{int(time.time())}", ) @@ -270,6 +307,8 @@ class DockerContainerManager: ) -> Dict[str, Any]: assert self._client is not None try: + # 注意:docker SDK 在 detach=False 时返回的是日志字节串,而非容器对象。 + # 这里统一以 detach=True 运行,然后等待并抓取日志,最后按需删除容器。 container = self._client.containers.run( image=self.image, command=command, @@ -278,13 +317,12 @@ class DockerContainerManager: working_dir=working_dir, platform=self.platform, name=name, - detach=detach, - remove=False, # 等获取日志后再删 + user="0:0", # 以 root 运行,避免挂载目录权限问题 + detach=True, + remove=False, # 获取日志后再删 stdout=True, stderr=True, ) - if detach: - return {"success": True, "container_id": container.id, "status": "running"} exit_info = container.wait() code = exit_info.get("StatusCode", 1) logs = container.logs().decode("utf-8", errors="ignore") @@ -312,12 +350,18 @@ class DockerContainerManager: cmd: List[str] = [cli, "run", "--rm" if remove and not detach else ""] cmd = [c for c in cmd if c] cmd += ["--platform", self.platform] + # 以 root 运行,避免权限问题 + cmd += ["--user", "0:0"] if name: cmd += ["--name", name] for host, spec in volumes.items(): bind = spec.get("bind") mode = spec.get("mode", "rw") - cmd += ["-v", f"{host}:{bind}:{mode}"] + # Podman(Linux) 下附加 :Z 处理 SELinux 标注;其他平台保持不变 + mount_mode = mode + if self._engine == "podman-cli" and os.name == "posix" and sys.platform.startswith("linux"): + mount_mode = f"{mode},Z" + cmd += ["-v", f"{host}:{bind}:{mount_mode}"] for k, v in (environment or {}).items(): cmd += ["-e", f"{k}={v}"] cmd += ["-w", working_dir, self.image]