From f2b966b1392d7820441687eacf696c959602c534 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sun, 19 Oct 2025 11:31:43 -0700 Subject: [PATCH] remote: Use SFTP over SCP for uploading files and directories (#40510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #37322 Uses SFTP if available, otherwise falls back to SCP for uploading files and directories to remote. This fixes an issue on older macOS versions where outdated SCP can throw an ambiguous target error. Release Notes: - Fixed an issue where extensions wouldn’t work when SSHing into a remote from older macOS versions. --- crates/remote/src/transport/ssh.rs | 167 ++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index d547f18e3307e008fb6db6fe2ecdf9480a73f334..e119e3a2edbd166990076820bf8056821555fde8 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -170,35 +170,44 @@ impl RemoteConnection for SshRemoteConnection { dest_path: RemotePathBuf, cx: &App, ) -> Task> { - let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command, false) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg("-C") - .arg("-r") - .arg(&src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path - )) - .output(); + let dest_path_str = dest_path.to_string(); + let src_path_display = src_path.display().to_string(); + + let mut sftp_command = self.build_sftp_command(); + let mut scp_command = + self.build_scp_command(&src_path, &dest_path_str, Some(&["-C", "-r"])); cx.background_spawn(async move { - let output = output.await?; + if Self::is_sftp_available().await { + log::debug!("using SFTP for directory upload"); + let mut child = sftp_command.spawn()?; + if let Some(mut stdin) = child.stdin.take() { + use futures::AsyncWriteExt; + let sftp_batch = format!("put -r {} {}\n", src_path.display(), dest_path_str); + stdin.write_all(sftp_batch.as_bytes()).await?; + drop(stdin); + } + + let output = child.output().await?; + anyhow::ensure!( + output.status.success(), + "failed to upload directory via SFTP {} -> {}: {}", + src_path_display, + dest_path_str, + String::from_utf8_lossy(&output.stderr) + ); + + return Ok(()); + } + + log::debug!("using SCP for directory upload"); + let output = scp_command.output().await?; anyhow::ensure!( output.status.success(), - "failed to upload directory {} -> {}: {}", - src_path.display(), - dest_path, + "failed to upload directory via SCP {} -> {}: {}", + src_path_display, + dest_path_str, String::from_utf8_lossy(&output.stderr) ); @@ -643,36 +652,92 @@ impl SshRemoteConnection { Ok(()) } - async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); + fn build_scp_command( + &self, + src_path: &Path, + dest_path_str: &str, + args: Option<&[&str]>, + ) -> process::Command { let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command, false) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg(src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path.display(self.path_style()) - )) - .output() - .await?; + self.socket.ssh_options(&mut command, false).args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), + ); + if let Some(args) = args { + command.args(args); + } + command.arg(src_path).arg(format!( + "{}:{}", + self.socket.connection_options.scp_url(), + dest_path_str + )); + command + } - anyhow::ensure!( - output.status.success(), - "failed to upload file {} -> {}: {}", - src_path.display(), - dest_path.display(self.path_style()), - String::from_utf8_lossy(&output.stderr) + fn build_sftp_command(&self) -> process::Command { + let mut command = util::command::new_smol_command("sftp"); + self.socket.ssh_options(&mut command, false).args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), ); - Ok(()) + command.arg("-b").arg("-"); + command.arg(self.socket.connection_options.scp_url()); + command.stdin(Stdio::piped()); + command + } + + async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> { + log::debug!("uploading file {:?} to {:?}", src_path, dest_path); + + let dest_path_str = dest_path.display(self.path_style()); + + if Self::is_sftp_available().await { + log::debug!("using SFTP for file upload"); + let mut command = self.build_sftp_command(); + let sftp_batch = format!("put {} {}\n", src_path.display(), dest_path_str); + + let mut child = command.spawn()?; + if let Some(mut stdin) = child.stdin.take() { + use futures::AsyncWriteExt; + stdin.write_all(sftp_batch.as_bytes()).await?; + drop(stdin); + } + + let output = child.output().await?; + anyhow::ensure!( + output.status.success(), + "failed to upload file via SFTP {} -> {}: {}", + src_path.display(), + dest_path_str, + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) + } else { + log::debug!("using SCP for file upload"); + let mut command = self.build_scp_command(src_path, &dest_path_str, None); + let output = command.output().await?; + + anyhow::ensure!( + output.status.success(), + "failed to upload file via SCP {} -> {}: {}", + src_path.display(), + dest_path_str, + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) + } + } + + async fn is_sftp_available() -> bool { + which::which("sftp").is_ok() } }