//! SSH backend implementation. //! //! Uses the [`openssh`] crate (which shells out to the system's OpenSSH //! binary) for session management and [`openssh_sftp_client`] for the SFTP //! subsystem when available. //! //! **SFTP fast-path** (Phase 2): file read/write/delete operations use the //! SFTP subsystem for efficient binary-safe transfer without base64 encoding //! or shell argument limits. If the remote host does not support the SFTP //! subsystem, the backend falls back transparently to exec-based operations //! (`cat`, `base64`, `rm`, etc.). //! //! **Exec path** (always available): `list`, `stat`, `exec`, and `check` //! are implemented via remote command execution, which gives structured //! output (GNU `stat --format=…`) and works on any POSIX remote. //! //! This gives us: //! //! - Full `~/.ssh/config` support //! - SSH agent forwarding //! - `ControlMaster` multiplexing (fast subsequent operations) //! - Key management delegated entirely to the user's existing setup //! - Efficient binary file transfers via SFTP //! - Graceful fallback when SFTP is unavailable use async_trait::async_trait; use bytes::Bytes; use openssh::{KnownHosts, Session, SessionBuilder}; use openssh_sftp_client::{Sftp, SftpOptions}; use std::path::Path; use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; use super::{Backend, DirEntry, EntryKind, ExecResult, Metadata}; use crate::errors::{TrampError, TrampResult}; // --------------------------------------------------------------------------- // SSH backend // --------------------------------------------------------------------------- /// An SSH backend backed by a live [`openssh::Session`] and an optional /// [`Sftp`] channel for efficient file I/O. /// /// The session is wrapped in `Arc` so it can be shared between the exec /// path and the SFTP subsystem (via `Sftp::from_clonable_session`). pub struct SshBackend { session: Arc, /// SFTP channel — `None` if the remote doesn't support the SFTP /// subsystem or if initialisation failed. sftp: Option, host: String, } impl SshBackend { /// Open a new SSH connection to `host`, optionally as `user` and/or on a /// non-default `port`. /// /// After the SSH session is established, the backend attempts to open an /// SFTP channel. If that fails (e.g. the server has disabled the SFTP /// subsystem), the backend continues with exec-only mode — no error is /// raised. pub async fn connect(host: &str, user: Option<&str>, port: Option) -> TrampResult { let mut builder = SessionBuilder::default(); builder.known_hosts_check(KnownHosts::Accept); if let Some(user) = user { builder.user(user.to_string()); } if let Some(port) = port { builder.port(port); } let session = builder .connect(host) .await .map_err(|e| TrampError::ConnectionFailed { host: host.to_string(), reason: e.to_string(), })?; let session = Arc::new(session); // Try to open an SFTP channel. This is best-effort — if it fails // we fall back to exec-based file I/O. let sftp = Sftp::from_clonable_session(session.clone(), SftpOptions::default()) .await .ok(); Ok(Self { session, sftp, host: host.to_string(), }) } /// Whether this backend has an active SFTP channel. #[allow(dead_code)] pub fn has_sftp(&self) -> bool { self.sftp.is_some() } /// Get a reference to the underlying SSH session (wrapped in `Arc`). /// /// Used by the VFS layer to pass the session to the agent deployment /// module without creating a new connection. pub fn session(&self) -> &Arc { &self.session } /// Get a reference to the SFTP channel, if available. /// /// Used by the VFS layer for agent binary uploads. pub fn sftp(&self) -> Option<&Sftp> { self.sftp.as_ref() } // ----------------------------------------------------------------------- // Exec helpers (unchanged from the original implementation) // ----------------------------------------------------------------------- /// Run a command via the SSH session and return its collected output. async fn run(&self, program: &str, args: &[&str]) -> TrampResult { let mut cmd = self.session.command(program); for arg in args { cmd.arg(arg); } let output = cmd .output() .await .map_err(|e| TrampError::from_ssh(&self.host, e))?; Ok(ExecResult { stdout: Bytes::from(output.stdout), stderr: Bytes::from(output.stderr), exit_code: output.status.code().unwrap_or(-1), }) } /// Run a shell snippet (`sh -c '