Files
Nexus/server/utils/posix_paths.py
T

123 lines
4.3 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_"
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 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)