rusync 0.7.2

Minimalist rsync clone in Rust
Documentation
use std::fs;
use std::io;
#[cfg(unix)]
use std::os::unix;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

use filetime::FileTime;
use tempfile::TempDir;

use rusync::progress::ProgressInfo;

fn assert_same_contents(a: &Path, b: &Path) {
    assert!(a.exists(), "{:?} does not exist", a);
    assert!(b.exists(), "{:?} does not exist", b);
    let status = Command::new("diff")
        .args(&[a, b])
        .status()
        .expect("Failed to execute process");
    assert!(status.success(), "{:?} and {:?} differ", a, b)
}

#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
    let metadata = std::fs::metadata(&path)
        .unwrap_or_else(|e| panic!("Could not get metadata of {:?}: {}", path, e));
    let permissions = metadata.permissions();
    let mode = permissions.mode();
    mode & 0o111 != 0
}

#[cfg(unix)]
fn assert_executable(path: &Path) {
    assert!(
        is_executable(path),
        "{:?} does not appear to be executable",
        path
    );
}

#[cfg(unix)]
fn assert_not_executable(path: &Path) {
    assert!(!is_executable(path), "{:?} appears to be executable", path);
}

fn setup_test(tmp_path: &Path) -> (PathBuf, PathBuf) {
    let src_path = tmp_path.join("src");
    let dest_path = tmp_path.join("dest");
    let status = Command::new("cp")
        .args(&["-R", "tests/data", &src_path.to_string_lossy()])
        .status()
        .expect("Failed to start cp process");
    assert!(status.success(), "could not copy test data");
    (src_path, dest_path)
}

fn make_recent(path: &Path) -> io::Result<()> {
    let metadata = fs::metadata(&path)?;
    let atime = FileTime::from_last_access_time(&metadata);
    let mtime = FileTime::from_last_modification_time(&metadata);
    let mut epoch = mtime.unix_seconds();
    epoch += 1;
    let mtime = FileTime::from_unix_time(epoch, 0);
    filetime::set_file_times(&path, atime, mtime)?;
    Ok(())
}

struct DummyProgressInfo {}
impl ProgressInfo for DummyProgressInfo {}

fn new_test_syncer(src: &Path, dest: &Path) -> rusync::Syncer {
    let dummy_progress_info = DummyProgressInfo {};
    let options = rusync::SyncOptions {
        preserve_permissions: true,
    };
    rusync::Syncer::new(src, dest, options, Box::new(dummy_progress_info))
}

#[test]
fn fresh_copy() -> Result<(), std::io::Error> {
    let tmp_dir = TempDir::new()?;
    let (src_path, dest_path) = setup_test(tmp_dir.path());
    let syncer = new_test_syncer(&src_path, &dest_path);
    let outcome = syncer.sync();
    assert!(outcome.is_ok());

    let src_top = src_path.join("top.txt");
    let dest_top = dest_path.join("top.txt");
    assert_same_contents(&src_top, &dest_top);

    Ok(())
}

#[test]
fn skip_up_to_date_files() -> Result<(), std::io::Error> {
    let tmp_dir = TempDir::new()?;
    let (src_path, dest_path) = setup_test(tmp_dir.path());
    let syncer = new_test_syncer(&src_path, &dest_path);

    let stats = syncer.sync().unwrap();
    assert_eq!(stats.up_to_date, 0);

    let src_top_txt = src_path.join("top.txt");
    make_recent(&src_top_txt)?;
    let syncer = new_test_syncer(&src_path, &dest_path);

    let stats = syncer.sync().unwrap();
    assert_eq!(stats.copied, 1);
    Ok(())
}

#[test]
#[cfg(unix)]
fn preserve_permissions() -> Result<(), std::io::Error> {
    let tmp_dir = TempDir::new()?;
    let (src_path, dest_path) = setup_test(tmp_dir.path());
    let syncer = new_test_syncer(&src_path, &dest_path);
    syncer.sync().unwrap();

    let dest_exe = &dest_path.join("a_dir/foo.exe");
    assert_executable(dest_exe);
    Ok(())
}

#[test]
#[cfg(unix)]
fn do_not_preserve_permissions() -> Result<(), std::io::Error> {
    let tmp_dir = TempDir::new()?;
    let (src_path, dest_path) = setup_test(tmp_dir.path());
    let options = rusync::SyncOptions {
        preserve_permissions: false,
    };
    let syncer = rusync::Syncer::new(
        &src_path,
        &dest_path,
        options,
        Box::new(DummyProgressInfo {}),
    );
    syncer.sync().unwrap();

    let dest_exe = &dest_path.join("a_dir/foo.exe");
    assert_not_executable(dest_exe);
    Ok(())
}

#[test]
fn rewrite_partially_written_files() -> Result<(), std::io::Error> {
    let tmp_dir = TempDir::new()?;
    let (src_path, dest_path) = setup_test(tmp_dir.path());
    let src_top = src_path.join("top.txt");
    let expected = fs::read_to_string(&src_top)?;

    let syncer = new_test_syncer(&src_path, &dest_path);
    syncer.sync().unwrap();
    let dest_top = dest_path.join("top.txt");
    // Corrupt the dest/top.txt
    fs::write(&dest_top, "this is")?;

    let syncer = new_test_syncer(&src_path, &dest_path);
    syncer.sync().unwrap();
    let actual = fs::read_to_string(&dest_top)?;
    assert_eq!(actual, expected);
    Ok(())
}

#[test]
fn dest_read_only() -> Result<(), std::io::Error> {
    let tmp_dir = TempDir::new()?;
    let (src_path, dest_path) = setup_test(tmp_dir.path());
    fs::create_dir_all(&dest_path)?;

    let dest_top = dest_path.join("top.txt");
    fs::write(&dest_top, "this is read only")?;

    let mut perms = fs::metadata(&dest_top)?.permissions();
    perms.set_readonly(true);
    fs::set_permissions(&dest_top, perms)?;

    let src_top = src_path.join("top.txt");
    make_recent(&src_top)?;

    let syncer = new_test_syncer(&src_path, &dest_path);
    let result = syncer.sync().unwrap();
    assert_eq!(result.errors, 1);
    Ok(())
}

#[test]
#[cfg(unix)]
fn broken_link_in_src() -> Result<(), std::io::Error> {
    let tmp_dir = TempDir::new()?;
    let (src_path, dest_path) = setup_test(tmp_dir.path());
    let src_broken_link = &src_path.join("broken");
    unix::fs::symlink("no-such", &src_broken_link)?;

    let syncer = new_test_syncer(&src_path, &dest_path);
    let result = syncer.sync();

    let dest_broken_link = &dest_path.join("broken");
    assert!(!dest_broken_link.exists());
    assert_eq!(dest_broken_link.read_link()?.to_string_lossy(), "no-such");
    assert!(result.is_ok());
    Ok(())
}