1use crate::config::SshConfig;
16use crate::remotes::remote;
17
18use std::io;
19use std::io::prelude::*;
20use std::io::Write;
21
22use std::iter::once;
23use std::path::{Path, PathBuf};
24
25use std::fmt;
26use std::string::String;
27
28use log::warn;
29
30use tokio::fs;
31use tokio::fs::File;
32use tokio::io::AsyncReadExt;
33
34use async_trait::async_trait;
35
36use std::process::{Command, Stdio};
37use which::which;
38
39#[derive(Debug)]
40pub enum Error {
41 InvalidPrivateKey(String),
42 CommandNotFound(which::Error),
43 RuntimeError(io::Error),
44}
45
46impl From<which::Error> for Error {
47 fn from(error: which::Error) -> Self {
48 Error::CommandNotFound(error)
49 }
50}
51
52impl From<io::Error> for Error {
53 fn from(error: io::Error) -> Self {
54 Error::RuntimeError(error)
55 }
56}
57
58impl std::error::Error for Error {}
59
60impl fmt::Display for Error {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Error::CommandNotFound(error) => write!(f, "Command not found: {}", error),
64 Error::InvalidPrivateKey(msg) => write!(f, "Invalid private key: {}", msg),
65 Error::RuntimeError(error) => write!(f, "Error while reading/writing: {}", error),
66 }
67 }
68}
69
70#[derive(Clone)]
71pub struct Ssh {
72 remote_name: String,
73 config: SshConfig,
74 ssh_cmd: PathBuf,
75 rsync_cmd: PathBuf,
76 ssh_args: Vec<String>,
77}
78
79impl Ssh {
80 pub async fn new(config: SshConfig, remote_name: &str) -> Result<Ssh, Error> {
81 let ssh_cmd = which("ssh")?;
82
83 let private_key = shellexpand::tilde(&config.private_key).to_string();
84 let private_key = PathBuf::from(private_key);
85 if !private_key.exists() {
86 return Err(Error::InvalidPrivateKey(format!(
87 "Private key {} does not exist.",
88 private_key.display(),
89 )));
90 }
91 let private_key_file = fs::read_to_string(&private_key).await?;
92
93 if private_key_file.contains("Proc-Type") && private_key_file.contains("ENCRYPTED") {
94 return Err(Error::InvalidPrivateKey(format!(
95 "Private key {} is encrypted with a passphrase. \
96 A key without passphrase is required",
97 private_key.display()
98 )));
99 }
100
101 let port = format!("{}", config.port);
102 let host = format!("{}@{}", config.username, config.host);
103 let mut args = vec![format!("-p{}", port), host, String::from("true")];
104
105 let output = Command::new(&ssh_cmd).args(&args).output();
106 if output.is_err() {
107 return Err(Error::RuntimeError(io::Error::other(format!(
108 "ssh connection to {}@{}:{} failed with error: {}",
109 config.username,
110 config.host,
111 config.port,
112 output.err().unwrap(),
113 ))));
114 }
115
116 let output = output.unwrap();
117 let stdout = String::from_utf8(output.stdout).unwrap();
118 let stderr = String::from_utf8(output.stderr).unwrap();
119
120 if stdout.is_empty() && stderr.contains("true") {
121 warn!(
131 "Connection to {}@{}:{} succeded, but received: {}",
132 config.username, config.host, config.port, stderr
133 );
134 } else {
135 let status = Command::new(&ssh_cmd)
139 .args(&args)
140 .stdout(Stdio::null())
141 .stderr(Stdio::null())
142 .status();
143 if status.is_err() {
144 return Err(Error::RuntimeError(status.err().unwrap()));
145 }
146
147 let status = status.unwrap();
148
149 if !status.success() {
150 return Err(Error::RuntimeError(io::Error::other(format!(
151 "ssh connection to {}@{}:{} failed with status: {}",
152 config.username,
153 config.host,
154 config.port,
155 status.code().unwrap(),
156 ))));
157 }
158 }
159
160 let rsync_cmd = which("rsync")?;
161 args.remove(args.iter().position(|x| x == "true").unwrap()); let ssh_args = args.iter().map(|s| s.to_string()).collect();
163 Ok(Ssh {
164 remote_name: String::from(remote_name),
165 config,
166 ssh_cmd,
167 rsync_cmd,
168 ssh_args,
169 })
170 }
171}
172
173#[async_trait]
174impl remote::Remote for Ssh {
175 fn name(&self) -> String {
176 self.remote_name.clone()
177 }
178
179 async fn enumerate(&self, remote_path: &Path) -> Result<Vec<String>, remote::Error> {
180 let remote_path = remote_path.to_str().unwrap();
181 let mut ssh = Command::new(&self.ssh_cmd)
187 .args(
188 self.ssh_args
189 .iter()
190 .chain(once(&format!("find {}/*", remote_path))),
191 )
192 .stdin(Stdio::null())
193 .stdout(Stdio::piped())
194 .stderr(Stdio::null())
195 .spawn()?;
196
197 let status = ssh.wait()?;
198
199 if status.success() {
200 let stdout = ssh.stdout.as_mut().unwrap();
201 let mut output = String::new();
202 stdout.read_to_string(&mut output).unwrap();
203 return Ok(output.split_whitespace().map(|s| s.to_string()).collect());
204 }
205
206 Err(remote::Error::LocalError(io::Error::other(format!(
207 "Error during ls {} on remote host",
208 remote_path
209 ))))
210 }
211
212 async fn delete(&self, remote_path: &Path) -> Result<(), remote::Error> {
213 let remote_path = remote_path.to_str().unwrap();
214 let mut ssh = Command::new(&self.ssh_cmd)
216 .args(
217 self.ssh_args
218 .iter()
219 .chain(once(&format!("rm -r {}", remote_path))),
220 )
221 .stdin(Stdio::null())
222 .stdout(Stdio::null())
223 .stderr(Stdio::null())
224 .spawn()?;
225
226 let status = ssh.wait()?;
227
228 if status.success() {
229 return Ok(());
230 }
231
232 Err(remote::Error::LocalError(io::Error::other(format!(
233 "Error during rm -r {} on remote host",
234 remote_path
235 ))))
236 }
237
238 async fn upload_file(&self, path: &Path, remote_path: &Path) -> Result<(), remote::Error> {
239 let mut content: Vec<u8> = vec![];
241 let mut file = File::open(path).await?;
242 file.read_to_end(&mut content).await?;
243 let remote_path = remote_path.to_str().unwrap();
244
245 let mut ssh = Command::new(&self.ssh_cmd)
247 .args(
248 self.ssh_args
249 .iter()
250 .chain(once(&format!("cat > {}", remote_path))),
251 )
252 .stdin(Stdio::piped())
253 .stdout(Stdio::piped())
254 .stderr(Stdio::piped())
255 .spawn()?;
256
257 {
258 let stdin = ssh.stdin.as_mut().unwrap();
259 stdin.write_all(&content)?;
262 }
263 let status = ssh.wait()?;
266
267 if !status.success() {
268 let stdout = ssh.stdout.as_mut().unwrap();
269 let stderr = ssh.stderr.as_mut().unwrap();
270 let mut errlog = String::new();
271 stderr.read_to_string(&mut errlog).unwrap();
272 let mut outlog = String::new();
273 stdout.read_to_string(&mut outlog).unwrap();
274
275 let message = format!(
276 "Failure while executing ssh command.\n\
277 Stderr: {}\nStdout: {}",
278 errlog, outlog
279 );
280 return Err(remote::Error::LocalError(io::Error::other(message)));
281 }
282 Ok(())
283 }
284
285 async fn upload_file_compressed(
286 &self,
287 path: &Path,
288 remote_path: &Path,
289 ) -> Result<(), remote::Error> {
290 let compressed_bytes = self.compress_file(path).await?;
292 let remote_path = self.remote_compressed_file_path(remote_path);
293
294 let mut ssh = Command::new(&self.ssh_cmd)
296 .stdin(Stdio::piped())
297 .stdout(Stdio::null())
298 .args(
299 self.ssh_args
300 .iter()
301 .chain(once(&format!("cat > {} ", remote_path.display()))),
302 )
303 .spawn()?;
304 ssh.stdin.as_mut().unwrap().write_all(&compressed_bytes)?;
305 let status = ssh.wait()?;
306 if !status.success() {
307 return Err(remote::Error::LocalError(io::Error::other(
308 "Failure while executing ssh command",
309 )));
310 }
311 Ok(())
312 }
313
314 async fn upload_folder(
315 &self,
316 paths: &[PathBuf],
317 remote_path: &Path,
318 ) -> Result<(), remote::Error> {
319 let mut local_prefix = paths.iter().min_by(|a, b| a.cmp(b)).unwrap();
320 let single_location = paths.len() <= 1;
325 let parent: PathBuf;
326 if !single_location {
327 parent = local_prefix.parent().unwrap().to_path_buf();
328 local_prefix = &parent;
329 }
330
331 let remote_path = remote_path.to_str().unwrap();
332 let dest = format!(
333 "{}@{}:{}",
334 self.config.username, self.config.host, remote_path
335 );
336 let src = local_prefix.to_str().unwrap();
337 let ssh_port_opt = format!(r#"ssh -p {}"#, self.config.port);
338 let args = vec!["-az", "-e", &ssh_port_opt, src, &dest, "--delete"];
341
342 let status = Command::new(&self.rsync_cmd)
343 .stderr(Stdio::null())
344 .stdout(Stdio::null())
345 .args(&args)
346 .status()?;
347
348 if !status.success() {
349 return Err(remote::Error::LocalError(io::Error::other(
350 "Failed to execute rsync trought ssh command",
351 )));
352 }
353
354 Ok(())
355 }
356
357 async fn upload_folder_compressed(
358 &self,
359 path: &Path,
360 remote_path: &Path,
361 ) -> Result<(), remote::Error> {
362 if !path.is_dir() {
363 return Err(remote::Error::NotADirectory);
364 }
365
366 let remote_path = self.remote_archive_path(remote_path);
367 let compressed_folder = self.compress_folder(path).await?;
368
369 self.upload_file(compressed_folder.path(), &remote_path)
370 .await
371 }
372}