sync_rs/
sync.rs

1use anyhow::{Context, Result};
2use std::process::Command;
3
4fn check_rsync_version() -> Result<()> {
5    let output = Command::new("rsync")
6        .arg("--version")
7        .output()
8        .context("Failed to execute rsync --version")?;
9
10    if !output.status.success() {
11        anyhow::bail!("Failed to get rsync version");
12    }
13
14    let version_output = String::from_utf8_lossy(&output.stdout);
15    
16    // Parse version from output like "rsync  version 3.2.7  protocol version 31"
17    let version_line = version_output
18        .lines()
19        .next()
20        .context("No version information found")?;
21    
22    let version_str = version_line
23        .split_whitespace()
24        .nth(2)
25        .context("Could not parse rsync version")?;
26    
27    let major_version = version_str
28        .split('.')
29        .next()
30        .and_then(|v| v.parse::<u32>().ok())
31        .context("Could not parse major version number")?;
32    
33    if major_version < 3 {
34        anyhow::bail!(
35            "rsync version {} is not supported. Please upgrade to version > 3.0",
36            version_str
37        );
38    }
39    
40    Ok(())
41}
42
43pub fn get_remote_home(remote_host: &str) -> Result<String> {
44    let output = Command::new("ssh")
45        .arg(remote_host)
46        .arg("echo $HOME")
47        .output()
48        .context("Failed to get remote home directory")?;
49
50    if !output.status.success() {
51        anyhow::bail!(
52            "SSH command failed: {}",
53            String::from_utf8_lossy(&output.stderr)
54        );
55    }
56
57    let home = String::from_utf8(output.stdout)?.trim().to_string();
58
59    if home.is_empty() {
60        anyhow::bail!("Remote home directory is empty");
61    }
62
63    Ok(home)
64}
65
66pub fn sync_directory(
67    source: &str,
68    destination: &str,
69    filter: Option<&str>,
70    delete: bool,
71) -> Result<()> {
72    // Ensure rsync version is greater than 3
73    check_rsync_version()?;
74    
75    let mut cmd = Command::new("rsync");
76    cmd.args(["-azP"]);
77
78    if delete {
79        cmd.args(["--delete"]);
80    }
81
82    if let Some(f) = filter {
83        // Handle multiple filters separated by commas
84        for filter_rule in f.split(',') {
85            cmd.args(["--filter", filter_rule.trim()]);
86        }
87    }
88
89    cmd.args([source, destination]);
90
91    let status = cmd.status().context("Failed to execute rsync command")?;
92
93    if !status.success() {
94        anyhow::bail!("rsync failed with exit code: {:?}", status.code());
95    }
96
97    Ok(())
98}
99
100pub fn execute_ssh_command(host: &str, command: &str) -> Result<()> {
101    let status = Command::new("ssh")
102        .arg(host)
103        .arg(command)
104        .status()
105        .context("Failed to execute SSH command")?;
106
107    if !status.success() {
108        anyhow::bail!("SSH command failed with exit code: {:?}", status.code());
109    }
110
111    Ok(())
112}
113
114pub fn open_remote_shell(host: &str, directory: &str) -> Result<()> {
115    let status = Command::new("ssh")
116        .arg("-t") // Force pseudo-terminal allocation for interactive shell
117        .arg(host)
118        .arg(format!("cd {} && exec $SHELL -l", directory))
119        .status()
120        .context("Failed to open remote shell")?;
121
122    if !status.success() {
123        anyhow::bail!("Remote shell exited with code: {:?}", status.code());
124    }
125
126    Ok(())
127}