use anyhow::{bail, Context, Result};
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::thread;
use crate::progress::{SyncProgress, ToolType};
use crate::sync_stats::SyncStats;
pub struct RsyncWrapper {
extra_args: Vec<String>,
progress_manager: Option<Arc<SyncProgress>>,
}
impl RsyncWrapper {
pub fn new(extra_args: Vec<String>) -> Self {
Self {
extra_args,
progress_manager: None,
}
}
pub fn with_progress(mut self, progress_manager: Arc<SyncProgress>) -> Self {
self.progress_manager = Some(progress_manager);
self
}
pub fn execute(&self, source: &Path, destination: &Path) -> Result<SyncStats> {
let mut cmd = Command::new("rsync");
for arg in &self.extra_args {
cmd.arg(arg);
}
if self.progress_manager.is_some() {
cmd.arg("--info=progress2");
}
let source_str = if source.is_dir() {
format!("{}/", source.display())
} else {
source.display().to_string()
};
cmd.arg(&source_str);
cmd.arg(destination);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().context("Failed to execute rsync")?;
let stats = Arc::new(SyncStats::default());
if let Some(stdout) = child.stdout.take() {
if let Some(ref progress_manager) = self.progress_manager {
let progress_manager = Arc::clone(progress_manager);
thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines() {
if let Ok(line) = line {
progress_manager.update_from_tool_output(&line, ToolType::Rsync);
}
}
});
} else {
thread::spawn(move || {
let reader = BufReader::new(stdout);
for _ in reader.lines() {}
});
}
}
let status = child.wait().context("Failed to wait for rsync")?;
if !status.success() {
bail!("rsync failed with status: {}", status);
}
Ok(Arc::try_unwrap(stats).unwrap_or_else(|arc| (*arc).clone()))
}
}
#[cfg(target_os = "windows")]
pub struct RobocopyWrapper {
extra_args: Vec<String>,
progress_manager: Option<Arc<SyncProgress>>,
}
#[cfg(target_os = "windows")]
impl RobocopyWrapper {
pub fn new(extra_args: Vec<String>) -> Self {
Self {
extra_args,
progress_manager: None,
}
}
pub fn with_progress(mut self, progress_manager: Arc<SyncProgress>) -> Self {
self.progress_manager = Some(progress_manager);
self
}
pub fn execute(&self, source: &Path, destination: &Path) -> Result<SyncStats> {
let mut cmd = Command::new("robocopy");
cmd.arg(source);
cmd.arg(destination);
for arg in &self.extra_args {
cmd.arg(arg);
}
cmd.arg("/NP"); cmd.arg("/BYTES");
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().context("Failed to execute robocopy")?;
let stats = Arc::new(SyncStats::default());
let stats_clone = Arc::clone(&stats);
if let Some(stdout) = child.stdout.take() {
if let Some(ref progress_manager) = self.progress_manager {
let progress_manager = Arc::clone(progress_manager);
thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines() {
if let Ok(line) = line {
progress_manager.update_from_tool_output(&line, ToolType::Robocopy);
Self::parse_file_line(&line, &stats_clone);
}
}
});
} else {
thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines() {
if let Ok(line) = line {
Self::parse_file_line(&line, &stats_clone);
}
}
});
}
}
let status = child.wait().context("Failed to wait for robocopy")?;
match status.code() {
Some(0) | Some(1) | Some(2) | Some(3) => {
}
Some(code) if code >= 8 => {
bail!("robocopy failed with error code: {}", code);
}
_ => {
bail!("robocopy failed with unknown status");
}
}
Ok(Arc::try_unwrap(stats).unwrap_or_else(|arc| (*arc).clone()))
}
fn parse_file_line(line: &str, stats: &Arc<SyncStats>) -> bool {
if line.contains("New File") || line.contains("Newer") || line.contains("Older") {
let parts: Vec<&str> = line.split_whitespace().collect();
for part in &parts {
if let Ok(size) = part.parse::<u64>() {
stats.add_bytes_transferred(size);
return true;
}
}
}
false
}
}
pub struct NativeToolExecutor {
dry_run: bool,
}
impl NativeToolExecutor {
pub fn new(dry_run: bool) -> Self {
Self { dry_run }
}
#[cfg(unix)]
pub fn run_rsync(
&self,
source: &Path,
destination: &Path,
args: Vec<String>,
progress_manager: Option<Arc<SyncProgress>>,
) -> Result<SyncStats> {
if self.dry_run {
println!(
"Would execute: rsync {} {} {}",
args.join(" "),
source.display(),
destination.display()
);
return Ok(SyncStats::default());
}
if !Self::is_rsync_available() {
eprintln!("Warning: rsync not found, falling back to built-in implementation");
return self.fallback_to_builtin(source, destination, progress_manager.clone());
}
let mut wrapper = RsyncWrapper::new(args);
let progress_manager_clone = progress_manager.clone();
if let Some(pm) = progress_manager {
wrapper = wrapper.with_progress(pm);
}
match wrapper.execute(source, destination) {
Ok(stats) => Ok(stats),
Err(e) => {
eprintln!("Warning: rsync failed ({e}), falling back to built-in implementation");
self.fallback_to_builtin(source, destination, progress_manager_clone)
}
}
}
#[cfg(target_os = "windows")]
pub fn run_robocopy(
&self,
source: &Path,
destination: &Path,
args: Vec<String>,
progress_manager: Option<Arc<SyncProgress>>,
) -> Result<SyncStats> {
if self.dry_run {
println!(
"Would execute: robocopy {} {} {}",
source.display(),
destination.display(),
args.join(" ")
);
return Ok(SyncStats::default());
}
if !Self::is_robocopy_available() {
eprintln!("Warning: robocopy not found, falling back to built-in implementation");
return self.fallback_to_builtin(source, destination, progress_manager.clone());
}
let mut wrapper = RobocopyWrapper::new(args);
let progress_manager_clone = progress_manager.clone();
if let Some(pm) = progress_manager {
wrapper = wrapper.with_progress(pm);
}
match wrapper.execute(source, destination) {
Ok(stats) => Ok(stats),
Err(e) => {
eprintln!(
"Warning: robocopy failed ({e}), falling back to built-in implementation"
);
self.fallback_to_builtin(source, destination, progress_manager_clone)
}
}
}
#[cfg(unix)]
pub fn is_rsync_available() -> bool {
Command::new("rsync")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(target_os = "windows")]
pub fn is_robocopy_available() -> bool {
Command::new("robocopy")
.arg("/?")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn fallback_to_builtin(
&self,
source: &Path,
destination: &Path,
progress_manager: Option<Arc<SyncProgress>>,
) -> Result<SyncStats> {
use crate::options::SyncOptions;
use crate::parallel_sync::{ParallelSyncConfig, ParallelSyncer};
println!("Using built-in RoboSync implementation...");
let options = SyncOptions {
dry_run: self.dry_run,
show_progress: !progress_manager.is_none(),
..Default::default()
};
let config = ParallelSyncConfig::default();
let syncer = ParallelSyncer::new(config);
if let Some(pm) = progress_manager {
pm.set_current_file("Running built-in sync implementation");
}
syncer.synchronize_with_options(source.to_path_buf(), destination.to_path_buf(), options)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(unix)]
fn test_rsync_available() {
let available = NativeToolExecutor::is_rsync_available();
println!("rsync available: {}", available);
}
#[test]
#[cfg(target_os = "windows")]
#[ignore] fn test_robocopy_available() {
assert!(NativeToolExecutor::is_robocopy_available());
}
}