feat(files): POSIX paths, elevation, recursive chmod, access hints and docs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-06-01 19:54:18 +08:00
parent 1cfac23c01
commit d93c518a14
79 changed files with 7784 additions and 394 deletions
+131 -1
View File
@@ -9,12 +9,17 @@ W2: Replaces paramiko pool.py with asyncssh throughout the codebase.
import asyncio
import logging
import secrets
import shlex
import time
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Optional, Dict
from typing import AsyncIterator, Dict, Optional
import asyncssh
from server.domain.models import Server
from server.utils.files_elevation import FilesElevation
from server.utils.posix_paths import posix_dirname
from server.infrastructure.database.crypto import decrypt_value
logger = logging.getLogger("nexus.asyncssh_pool")
@@ -256,6 +261,131 @@ class AsyncSSHPool:
ssh_pool = AsyncSSHPool()
@asynccontextmanager
async def sftp_session(conn: asyncssh.SSHClientConnection) -> AsyncIterator[asyncssh.SFTPClient]:
"""Open an SFTP client on a pooled SSH connection (asyncssh 2.x: start_sftp_client)."""
async with await conn.start_sftp_client() as sftp:
yield sftp
async def sftp_write_bytes(sftp: asyncssh.SFTPClient, remote_path: str, data: bytes) -> None:
"""Write in-memory bytes to a remote file (asyncssh put() only accepts local paths)."""
async with sftp.open(remote_path, "wb") as remote_file:
await remote_file.write(data)
def _sftp_permission_denied(exc: Exception) -> bool:
if isinstance(exc, asyncssh.PermissionDenied):
return True
if isinstance(exc, asyncssh.SFTPError):
# SSH_FX_PERMISSION_DENIED = 3
if getattr(exc, "code", None) == 3:
return True
return "permission denied" in str(exc).lower()
async def _run_tee_stdin(
conn: asyncssh.SSHClientConnection,
command: str,
data: bytes,
) -> asyncssh.SSHCompletedProcess:
"""Feed raw bytes to stdin; default SSH session encoding expects str, not bytes."""
return await conn.run(command, input=data, encoding=None, check=False)
async def _ensure_remote_parent_dir(
conn: asyncssh.SSHClientConnection,
remote_path: str,
*,
use_sudo: bool,
) -> None:
parent = posix_dirname(remote_path)
if not parent or parent == "/":
return
prefix = "sudo -n " if use_sudo else ""
await conn.run(f"{prefix}mkdir -p {shlex.quote(parent)}", check=False)
async def _ssh_write_bytes_via_tee(
conn: asyncssh.SSHClientConnection,
remote_path: str,
data: bytes,
*,
use_sudo: bool,
) -> None:
"""Shell write fallback when SFTP cannot open the file (e.g. file not writable but directory is)."""
prefix = "sudo -n " if use_sudo else ""
await _ensure_remote_parent_dir(conn, remote_path, use_sudo=use_sudo)
direct = await _run_tee_stdin(
conn,
f"{prefix}tee {shlex.quote(remote_path)}",
data,
)
if direct.exit_status == 0:
return
tmp_path = f"/tmp/nexus-write-{secrets.token_hex(12)}"
staged = await _run_tee_stdin(
conn,
f"{prefix}tee {shlex.quote(tmp_path)}",
data,
)
if staged.exit_status != 0:
stderr = (staged.stderr or staged.stdout or direct.stderr or "")[:500]
raise PermissionError(stderr or "tee 写入失败")
moved = await conn.run(
f"{prefix}mv -f {shlex.quote(tmp_path)} {shlex.quote(remote_path)}",
check=False,
)
if moved.exit_status != 0:
await conn.run(f"{prefix}rm -f {shlex.quote(tmp_path)}", check=False)
stderr = (moved.stderr or "")[:500]
raise PermissionError(stderr or "无法覆盖目标文件(目录可能不可写)")
def _sudo_attempt_order(elevation: FilesElevation | None) -> tuple[bool, ...]:
mode = elevation if elevation is not None else FilesElevation.AUTO
if mode == FilesElevation.OFF:
return (False,)
if mode == FilesElevation.ALWAYS:
return (True,)
return (False, True)
async def remote_write_bytes(
conn: asyncssh.SSHClientConnection,
remote_path: str,
data: bytes,
*,
elevation: FilesElevation | None = None,
) -> None:
"""Write bytes to a remote path: SFTP first, then SSH tee / tmp+mv (incl. optional sudo -n)."""
eff = elevation if elevation is not None else FilesElevation.AUTO
skip_sftp = eff == FilesElevation.ALWAYS
if not skip_sftp:
try:
async with await conn.start_sftp_client() as sftp:
await sftp_write_bytes(sftp, remote_path, data)
return
except (asyncssh.PermissionDenied, asyncssh.SFTPError) as exc:
if not _sftp_permission_denied(exc):
raise
last_error: Exception | None = None
for use_sudo in _sudo_attempt_order(eff):
try:
await _ssh_write_bytes_via_tee(conn, remote_path, data, use_sudo=use_sudo)
return
except PermissionError as exc:
last_error = exc
reason = str(last_error) if last_error else "Permission denied"
raise asyncssh.SFTPError(3, reason)
async def exec_ssh_command(server: Server, command: str, timeout: int = 30) -> dict:
"""Execute a command via SSH using the asyncssh connection pool.