d93c518a14
Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
4.1 KiB
Python
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
|