use crate::{error::DeployError, known_hosts, report::Reporter};
use anyhow::anyhow;
use std::{
io::Write,
path::{Path, PathBuf},
process::Command,
};
use tempfile::NamedTempFile;
pub trait Transport {
fn ship(&self, source: &Path, reporter: &dyn Reporter) -> Result<(), DeployError>;
}
pub struct RsyncTransport {
host: String,
remote_path: String,
identity_file: PathBuf,
}
impl RsyncTransport {
pub fn new(host: String, remote_path: String, identity_file: PathBuf) -> Self {
Self {
host,
remote_path,
identity_file,
}
}
}
impl Transport for RsyncTransport {
fn ship(&self, source: &Path, reporter: &dyn Reporter) -> Result<(), DeployError> {
let mut known_hosts_file = NamedTempFile::new()
.map_err(|e| anyhow!("Failed to create temp known_hosts file: {e}"))?;
writeln!(known_hosts_file, "{}", known_hosts::for_host(&self.host))
.map_err(|e| anyhow!("Failed to write known_hosts: {e}"))?;
let ssh_command = format!(
"ssh -i {identity} \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile={known_hosts} \
-o IdentitiesOnly=yes \
-o PasswordAuthentication=no \
-o BatchMode=yes",
identity = self.identity_file.to_string_lossy(),
known_hosts = known_hosts_file.path().display(),
);
let source_str = source.to_string_lossy();
let source_with_slash = if source_str.ends_with('/') {
source_str.into_owned()
} else {
format!("{source_str}/")
};
let remote_with_slash = if self.remote_path.ends_with('/') {
self.remote_path.clone()
} else {
format!("{}/", self.remote_path)
};
let destination = format!("git@{}:{}", self.host, remote_with_slash);
reporter.step_start(&format!("Syncing {source_with_slash} -> {destination}"));
let output = Command::new("rsync")
.args([
"-a",
"--exclude=.git",
"--exclude=.smb",
"-e",
&ssh_command,
&source_with_slash,
&destination,
])
.output();
drop(known_hosts_file);
match output {
Ok(result) if result.status.success() => {
for line in String::from_utf8_lossy(&result.stderr).lines() {
reporter.remote_line(line);
}
reporter.step_done("Deployment complete via rsync.");
Ok(())
}
Ok(result) => {
let code = result.status.code().unwrap_or(-1);
reporter.step_fail(&format!("rsync exited with status {code}"));
for line in String::from_utf8_lossy(&result.stderr).lines() {
reporter.remote_line(line);
}
Err(DeployError::Other(anyhow!(
"rsync exited with status {code}"
)))
}
Err(e) => {
reporter.step_fail(&format!("Failed to launch rsync: {e}. Is rsync installed?"));
Err(DeployError::Other(anyhow!("Failed to launch rsync: {e}")))
}
}
}
}