smbcloud_deploy/
transport.rs1use crate::{error::DeployError, known_hosts, report::Reporter};
9use anyhow::anyhow;
10use std::{
11 io::Write,
12 path::{Path, PathBuf},
13 process::Command,
14};
15use tempfile::NamedTempFile;
16
17pub trait Transport {
19 fn ship(&self, source: &Path, reporter: &dyn Reporter) -> Result<(), DeployError>;
21}
22
23pub struct RsyncTransport {
29 host: String,
30 remote_path: String,
31 identity_file: PathBuf,
32}
33
34impl RsyncTransport {
35 pub fn new(host: String, remote_path: String, identity_file: PathBuf) -> Self {
36 Self {
37 host,
38 remote_path,
39 identity_file,
40 }
41 }
42}
43
44impl Transport for RsyncTransport {
45 fn ship(&self, source: &Path, reporter: &dyn Reporter) -> Result<(), DeployError> {
46 let mut known_hosts_file = NamedTempFile::new()
50 .map_err(|e| anyhow!("Failed to create temp known_hosts file: {e}"))?;
51 writeln!(known_hosts_file, "{}", known_hosts::for_host(&self.host))
52 .map_err(|e| anyhow!("Failed to write known_hosts: {e}"))?;
53
54 let ssh_command = format!(
57 "ssh -i {identity} \
58 -o StrictHostKeyChecking=yes \
59 -o UserKnownHostsFile={known_hosts} \
60 -o IdentitiesOnly=yes \
61 -o PasswordAuthentication=no \
62 -o BatchMode=yes",
63 identity = self.identity_file.to_string_lossy(),
64 known_hosts = known_hosts_file.path().display(),
65 );
66
67 let source_str = source.to_string_lossy();
70 let source_with_slash = if source_str.ends_with('/') {
71 source_str.into_owned()
72 } else {
73 format!("{source_str}/")
74 };
75 let remote_with_slash = if self.remote_path.ends_with('/') {
76 self.remote_path.clone()
77 } else {
78 format!("{}/", self.remote_path)
79 };
80 let destination = format!("git@{}:{}", self.host, remote_with_slash);
81
82 reporter.step_start(&format!("Syncing {source_with_slash} -> {destination}"));
83
84 let output = Command::new("rsync")
86 .args([
87 "-a",
88 "--exclude=.git",
89 "--exclude=.smb",
90 "-e",
91 &ssh_command,
92 &source_with_slash,
93 &destination,
94 ])
95 .output();
96 drop(known_hosts_file);
97
98 match output {
99 Ok(result) if result.status.success() => {
100 for line in String::from_utf8_lossy(&result.stderr).lines() {
101 reporter.remote_line(line);
102 }
103 reporter.step_done("Deployment complete via rsync.");
104 Ok(())
105 }
106 Ok(result) => {
107 let code = result.status.code().unwrap_or(-1);
108 reporter.step_fail(&format!("rsync exited with status {code}"));
109 for line in String::from_utf8_lossy(&result.stderr).lines() {
110 reporter.remote_line(line);
111 }
112 Err(DeployError::Other(anyhow!(
113 "rsync exited with status {code}"
114 )))
115 }
116 Err(e) => {
117 reporter.step_fail(&format!("Failed to launch rsync: {e}. Is rsync installed?"));
118 Err(DeployError::Other(anyhow!("Failed to launch rsync: {e}")))
119 }
120 }
121 }
122}