Files
Nexus/server/infrastructure/ssh/remote_archive.py
T

129 lines
4.1 KiB
Python

"""Remote tar/zip compress & decompress via SSH with permission/sudo fallbacks."""
from __future__ import annotations
import posixpath
import secrets
import shlex
from server.domain.models import Server
from server.infrastructure.ssh.remote_shell import exec_ssh_command_with_fallback
def _common_parent_dir(paths: list[str]) -> str:
normalized = [posixpath.normpath(p) for p in paths]
if len(normalized) == 1:
p = normalized[0]
return posixpath.dirname(p) if not p.endswith("/") else p.rstrip("/")
parts_list = [p.strip("/").split("/") for p in normalized if p.startswith("/")]
if not parts_list:
return "/"
common: list[str] = []
for segment_lists in zip(*parts_list):
if len(set(segment_lists)) == 1:
common.append(segment_lists[0])
else:
break
if not common:
return "/"
return "/" + "/".join(common)
def _tar_member_names(paths: list[str], work_dir: str) -> list[str]:
members: list[str] = []
work = work_dir.rstrip("/") or "/"
for raw in paths:
p = posixpath.normpath(raw)
if p == work or p.rstrip("/") == work:
members.append(".")
continue
prefix = work + "/"
if p.startswith(prefix):
members.append(p[len(prefix) :])
else:
members.append(p.lstrip("/"))
# unique preserve order
seen: set[str] = set()
out: list[str] = []
for m in members:
if m not in seen:
seen.add(m)
out.append(m)
return out
def tar_compress_commands(paths: list[str], dest: str) -> list[str]:
"""Build tar commands: direct write, then /tmp staging + mv."""
dest_norm = posixpath.normpath(dest)
work_dir = _common_parent_dir(paths)
members = _tar_member_names(paths, work_dir)
if not members:
raise ValueError("没有可压缩的文件")
work_q = shlex.quote(work_dir)
members_q = " ".join(shlex.quote(m) for m in members)
dest_q = shlex.quote(dest_norm)
dest_parent = posixpath.dirname(dest_norm) or "/"
dest_base = posixpath.basename(dest_norm)
commands: list[str] = []
if dest_parent == work_dir:
commands.append(f"tar -czf {shlex.quote(dest_base)} -C {work_q} {members_q}")
else:
commands.append(f"tar -czf {dest_q} -C {work_q} {members_q}")
tmp = f"/tmp/nexus-compress-{secrets.token_hex(8)}.tar.gz"
commands.append(
f"tar -czf {shlex.quote(tmp)} -C {work_q} {members_q} && mv -f {shlex.quote(tmp)} {dest_q}"
)
return commands
def zip_compress_commands(paths: list[str], dest: str) -> list[str]:
dest_norm = posixpath.normpath(dest)
work_dir = _common_parent_dir(paths)
members = _tar_member_names(paths, work_dir)
work_q = shlex.quote(work_dir)
members_q = " ".join(shlex.quote(m) for m in members)
dest_q = shlex.quote(dest_norm)
dest_parent = posixpath.dirname(dest_norm) or "/"
dest_base = posixpath.basename(dest_norm)
dest_arg = shlex.quote(dest_base) if dest_parent == work_dir else dest_q
inner = f"cd {work_q} && zip -qr {dest_arg} {members_q}"
tmp = f"/tmp/nexus-compress-{secrets.token_hex(8)}.zip"
staged = (
f"cd {work_q} && zip -qr {shlex.quote(tmp)} {members_q} "
f"&& mv -f {shlex.quote(tmp)} {dest_q}"
)
return [inner, staged]
async def run_remote_compress(
server: Server,
paths: list[str],
dest: str,
fmt: str,
*,
timeout: int = 120,
) -> dict:
"""Try compress commands until one succeeds (with sudo fallback per command)."""
if fmt == "tar.gz":
commands = tar_compress_commands(paths, dest)
elif fmt == "zip":
commands = zip_compress_commands(paths, dest)
else:
return {
"exit_code": -1,
"stderr": f"不支持的格式: {fmt}",
"stdout": "",
"status": "error",
}
last = {"exit_code": -1, "stderr": "", "stdout": "", "status": "failed"}
for cmd in commands:
last = await exec_ssh_command_with_fallback(server, cmd, timeout=timeout)
if last.get("exit_code") == 0:
return last
return last