8c86b50d09
服务器页拆分未设路径列表、行内展开详情;API target_path_unset 可选过滤。 命令栏取消 Shell 语法彩色,ANSI 仅保留 xterm SSH 输出区。 Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
5.4 KiB
Python
159 lines
5.4 KiB
Python
"""Linux/POSIX path helpers for remote SSH paths and Nexus-host Unix paths.
|
|
|
|
Use this module whenever manipulating path *strings* for Linux targets.
|
|
Do not use ``os.path`` for remote paths — on Windows dev hosts ``os.path.join``
|
|
produces backslashes and breaks SSH/SFTP commands.
|
|
|
|
For real filesystem I/O on the Nexus host (``open``, ``os.scandir``), keep
|
|
``os.path.realpath`` / ``os.walk`` after normalizing with :func:`to_posix`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import posixpath
|
|
|
|
UPLOAD_STAGING_PREFIX = "/tmp/nexus_upload_"
|
|
|
|
FORBIDDEN_NEXUS_SOURCE_PREFIXES = (
|
|
"/etc",
|
|
"/root",
|
|
"/home",
|
|
"/var",
|
|
"/boot",
|
|
"/dev",
|
|
"/proc",
|
|
"/sys",
|
|
"/run",
|
|
"/srv",
|
|
)
|
|
|
|
|
|
class PosixPathError(ValueError):
|
|
"""Invalid or unsafe POSIX path."""
|
|
|
|
|
|
def to_posix(path: str) -> str:
|
|
"""Normalize slashes to forward slash (does not resolve ``..``)."""
|
|
return (path or "").strip().replace("\\", "/")
|
|
|
|
|
|
def reject_windows_separators(path: str, *, field: str = "path") -> None:
|
|
if "\\" in path or "\0" in path:
|
|
raise PosixPathError(f"{field} 必须使用 Linux 路径(不允许反斜杠)")
|
|
|
|
|
|
def normalize_remote_abs_path(path: str) -> str:
|
|
"""Validate and normalize a remote absolute path (SSH/SFTP)."""
|
|
raw_in = (path or "").strip()
|
|
reject_windows_separators(raw_in, field="path")
|
|
raw = to_posix(raw_in)
|
|
if not raw.startswith("/"):
|
|
raise PosixPathError("路径必须为绝对路径")
|
|
parts = [p for p in raw.split("/") if p and p != "."]
|
|
if ".." in parts:
|
|
raise PosixPathError("路径不允许包含 '..'")
|
|
normalized = "/" + "/".join(parts) if parts else "/"
|
|
if len(normalized) > 1 and normalized.endswith("/"):
|
|
normalized = normalized.rstrip("/")
|
|
return normalized or "/"
|
|
|
|
|
|
def posix_join(*parts: str) -> str:
|
|
"""Join path segments using POSIX rules (safe on any dev OS)."""
|
|
if not parts:
|
|
return "/"
|
|
acc = to_posix(parts[0])
|
|
for part in parts[1:]:
|
|
seg = to_posix(part)
|
|
if not seg or seg == "/":
|
|
continue
|
|
acc = posixpath.join(acc, seg.lstrip("/"))
|
|
return posixpath.normpath(acc) or "/"
|
|
|
|
|
|
def posix_basename(path: str) -> str:
|
|
return posixpath.basename(to_posix(path.rstrip("/")))
|
|
|
|
|
|
def posix_dirname(path: str) -> str:
|
|
parent = posixpath.dirname(to_posix(path.rstrip("/")))
|
|
return parent or "/"
|
|
|
|
|
|
def remote_join(directory: str, name: str) -> str:
|
|
"""Join remote directory + file name (both logical Linux paths)."""
|
|
base = normalize_remote_abs_path(directory)
|
|
segment = to_posix(name).strip("/")
|
|
reject_windows_separators(segment, field="name")
|
|
if ".." in segment.split("/"):
|
|
raise PosixPathError("名称不能包含 '..'")
|
|
if not segment:
|
|
return base
|
|
return posix_join(base, segment)
|
|
|
|
|
|
def resolve_nexus_host_path(path: str) -> str:
|
|
"""Resolve a path on the Nexus host filesystem (Linux deployment)."""
|
|
posix = to_posix(path)
|
|
reject_windows_separators(posix, field="path")
|
|
return os.path.realpath(posix)
|
|
|
|
|
|
def resolve_nexus_push_source_path(path: str) -> str:
|
|
"""Resolve and validate a Nexus host push source directory."""
|
|
stripped = path.strip()
|
|
if ".." in to_posix(stripped).split("/"):
|
|
raise PosixPathError("路径不允许包含 '..'")
|
|
real_path = resolve_nexus_host_path(stripped)
|
|
for prefix in FORBIDDEN_NEXUS_SOURCE_PREFIXES:
|
|
if real_path == prefix or real_path.startswith(prefix + "/"):
|
|
raise PosixPathError(f"不允许使用系统路径: {prefix}")
|
|
if not os.path.isdir(real_path):
|
|
raise PosixPathError(f"源路径不存在: {real_path}")
|
|
return real_path
|
|
|
|
|
|
def ensure_under_nexus_upload(path: str) -> str:
|
|
"""Resolve *path* and ensure it stays under ``/tmp/nexus_upload_*``."""
|
|
real = resolve_nexus_host_path(path)
|
|
if not to_posix(real).startswith(UPLOAD_STAGING_PREFIX):
|
|
raise PosixPathError("仅允许访问上传临时目录")
|
|
return real
|
|
|
|
|
|
def assert_zip_member_safe(extract_dir: str, member_name: str) -> str:
|
|
"""Zip-slip check; returns normalized member path under *extract_dir*."""
|
|
base = normalize_remote_abs_path(extract_dir)
|
|
member = to_posix(member_name).lstrip("/")
|
|
if not member or member == ".":
|
|
return base
|
|
if member == ".." or member.startswith("../") or "/../" in f"/{member}/":
|
|
raise PosixPathError(f"ZIP 包含非法路径: {member_name}")
|
|
candidate = posixpath.normpath(posixpath.join(base, member))
|
|
if candidate != base and not candidate.startswith(base + "/"):
|
|
raise PosixPathError(f"ZIP 包含非法路径: {member_name}")
|
|
return candidate
|
|
|
|
|
|
def is_path_under_prefix(resolved: str, prefix: str) -> bool:
|
|
"""True if *resolved* is *prefix* or a child (POSIX string compare after norm)."""
|
|
r = normalize_remote_abs_path(to_posix(resolved))
|
|
p = normalize_remote_abs_path(to_posix(prefix))
|
|
return r == p or r.startswith(p + "/")
|
|
|
|
|
|
def normalize_remote_dest(path: str | None, *, default: str = "/tmp/sync") -> str:
|
|
"""Normalize remote target directory (push/rsync/diagnose); uses *default* when empty."""
|
|
raw = (path or "").strip() or default
|
|
return normalize_remote_abs_path(raw)
|
|
|
|
|
|
def is_unset_target_path(path: str | None) -> bool:
|
|
"""True when push target path is empty or still the BT default placeholder.
|
|
|
|
``/www/wwwroot/`` (trailing slash) is *not* unset — only exact ``/www/wwwroot`` after trim.
|
|
"""
|
|
p = (path or "").strip()
|
|
return p == "" or p == "/www/wwwroot"
|